diffs: replace compare controller with new html based diffs:
* side/side + unified support * redesign of diff changes/operations * added button to see file before the change * auto collapses large diffs refs #4232
This commit is contained in:
parent
067756822f
commit
12ebe4382a
15 changed files with 3405 additions and 149 deletions
|
|
@ -14,6 +14,8 @@ permission notice:
|
|||
file:licenses/tornado_license.txt
|
||||
Copyright (c) 2015 - pygments-markdown-lexer
|
||||
file:licenses/pygments_markdown_lexer_license.txt
|
||||
Copyright 2006 - diff_match_patch
|
||||
file:licenses/diff_match_patch_license.txt
|
||||
|
||||
All licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
|||
14
licenses/diff_match_patch_license.txt
Normal file
14
licenses/diff_match_patch_license.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
Copyright 2006 Google Inc.
|
||||
http://code.google.com/p/google-diff-match-patch/
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -31,13 +31,14 @@ from pylons.i18n.translation import _
|
|||
|
||||
from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
|
||||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.lib import diffs
|
||||
from rhodecode.lib import diffs, codeblocks
|
||||
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
|
||||
from rhodecode.lib.base import BaseRepoController, render
|
||||
from rhodecode.lib.utils import safe_str
|
||||
from rhodecode.lib.utils2 import safe_unicode, str2bool
|
||||
from rhodecode.lib.vcs.exceptions import (
|
||||
EmptyRepositoryError, RepositoryError, RepositoryRequirementError)
|
||||
EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
|
||||
NodeDoesNotExistError)
|
||||
from rhodecode.model.db import Repository, ChangesetStatus
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -78,7 +79,7 @@ class CompareController(BaseRepoController):
|
|||
def index(self, repo_name):
|
||||
c.compare_home = True
|
||||
c.commit_ranges = []
|
||||
c.files = []
|
||||
c.diffset = None
|
||||
c.limited_diff = False
|
||||
source_repo = c.rhodecode_db_repo.repo_name
|
||||
target_repo = request.GET.get('target_repo', source_repo)
|
||||
|
|
@ -239,28 +240,23 @@ class CompareController(BaseRepoController):
|
|||
commit1=source_commit, commit2=target_commit,
|
||||
path1=source_path, path=target_path)
|
||||
diff_processor = diffs.DiffProcessor(
|
||||
txtdiff, format='gitdiff', diff_limit=diff_limit,
|
||||
txtdiff, format='newdiff', diff_limit=diff_limit,
|
||||
file_limit=file_limit, show_full_diff=c.fulldiff)
|
||||
_parsed = diff_processor.prepare()
|
||||
|
||||
c.limited_diff = False
|
||||
if isinstance(_parsed, diffs.LimitedDiffContainer):
|
||||
c.limited_diff = True
|
||||
def _node_getter(commit):
|
||||
""" Returns a function that returns a node for a commit or None """
|
||||
def get_node(fname):
|
||||
try:
|
||||
return commit.get_node(fname)
|
||||
except NodeDoesNotExistError:
|
||||
return None
|
||||
return get_node
|
||||
|
||||
c.files = []
|
||||
c.changes = {}
|
||||
c.lines_added = 0
|
||||
c.lines_deleted = 0
|
||||
for f in _parsed:
|
||||
st = f['stats']
|
||||
if not st['binary']:
|
||||
c.lines_added += st['added']
|
||||
c.lines_deleted += st['deleted']
|
||||
fid = h.FID('', f['filename'])
|
||||
c.files.append([fid, f['operation'], f['filename'], f['stats'], f])
|
||||
htmldiff = diff_processor.as_html(
|
||||
enable_comments=False, parsed_lines=[f])
|
||||
c.changes[fid] = [f['operation'], f['filename'], htmldiff, f]
|
||||
c.diffset = codeblocks.DiffSet(
|
||||
source_node_getter=_node_getter(source_commit),
|
||||
target_node_getter=_node_getter(target_commit),
|
||||
).render_patchset(_parsed, source_ref, target_ref)
|
||||
|
||||
c.preview_mode = merge
|
||||
|
||||
|
|
|
|||
|
|
@ -19,13 +19,17 @@
|
|||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
import logging
|
||||
import difflib
|
||||
from itertools import groupby
|
||||
|
||||
from pygments import lex
|
||||
from pygments.formatters.html import _get_ttype_class as pygment_token_class
|
||||
from rhodecode.lib.helpers import get_lexer_for_filenode, html_escape
|
||||
from rhodecode.lib.helpers import (
|
||||
get_lexer_for_filenode, get_lexer_safe, html_escape)
|
||||
from rhodecode.lib.utils2 import AttributeDict
|
||||
from rhodecode.lib.vcs.nodes import FileNode
|
||||
from rhodecode.lib.diff_match_patch import diff_match_patch
|
||||
from rhodecode.lib.diffs import LimitedDiffContainer
|
||||
from pygments.lexers import get_lexer_by_name
|
||||
|
||||
plain_text_lexer = get_lexer_by_name(
|
||||
|
|
@ -38,7 +42,7 @@ log = logging.getLogger()
|
|||
def filenode_as_lines_tokens(filenode, lexer=None):
|
||||
lexer = lexer or get_lexer_for_filenode(filenode)
|
||||
log.debug('Generating file node pygment tokens for %s, %s', lexer, filenode)
|
||||
tokens = tokenize_string(filenode.content, get_lexer_for_filenode(filenode))
|
||||
tokens = tokenize_string(filenode.content, lexer)
|
||||
lines = split_token_stream(tokens, split_string='\n')
|
||||
rv = list(lines)
|
||||
return rv
|
||||
|
|
@ -146,7 +150,11 @@ def render_tokenstream(tokenstream):
|
|||
result.append(u'<%s>' % op_tag)
|
||||
|
||||
escaped_text = html_escape(token_text)
|
||||
escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
|
||||
|
||||
# TODO: dan: investigate showing hidden characters like space/nl/tab
|
||||
# escaped_text = escaped_text.replace(' ', '<sp> </sp>')
|
||||
# escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
|
||||
# escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
|
||||
|
||||
result.append(escaped_text)
|
||||
|
||||
|
|
@ -212,3 +220,416 @@ def rollup_tokenstream(tokenstream):
|
|||
ops.append((token_op, ''.join(text_buffer)))
|
||||
result.append((token_class, ops))
|
||||
return result
|
||||
|
||||
|
||||
def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
|
||||
"""
|
||||
Converts a list of (token_class, token_text) tuples to a list of
|
||||
(token_class, token_op, token_text) tuples where token_op is one of
|
||||
('ins', 'del', '')
|
||||
|
||||
:param old_tokens: list of (token_class, token_text) tuples of old line
|
||||
:param new_tokens: list of (token_class, token_text) tuples of new line
|
||||
:param use_diff_match_patch: boolean, will use google's diff match patch
|
||||
library which has options to 'smooth' out the character by character
|
||||
differences making nicer ins/del blocks
|
||||
"""
|
||||
|
||||
old_tokens_result = []
|
||||
new_tokens_result = []
|
||||
|
||||
similarity = difflib.SequenceMatcher(None,
|
||||
''.join(token_text for token_class, token_text in old_tokens),
|
||||
''.join(token_text for token_class, token_text in new_tokens)
|
||||
).ratio()
|
||||
|
||||
if similarity < 0.6: # return, the blocks are too different
|
||||
for token_class, token_text in old_tokens:
|
||||
old_tokens_result.append((token_class, '', token_text))
|
||||
for token_class, token_text in new_tokens:
|
||||
new_tokens_result.append((token_class, '', token_text))
|
||||
return old_tokens_result, new_tokens_result, similarity
|
||||
|
||||
token_sequence_matcher = difflib.SequenceMatcher(None,
|
||||
[x[1] for x in old_tokens],
|
||||
[x[1] for x in new_tokens])
|
||||
|
||||
for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
|
||||
# check the differences by token block types first to give a more
|
||||
# nicer "block" level replacement vs character diffs
|
||||
|
||||
if tag == 'equal':
|
||||
for token_class, token_text in old_tokens[o1:o2]:
|
||||
old_tokens_result.append((token_class, '', token_text))
|
||||
for token_class, token_text in new_tokens[n1:n2]:
|
||||
new_tokens_result.append((token_class, '', token_text))
|
||||
elif tag == 'delete':
|
||||
for token_class, token_text in old_tokens[o1:o2]:
|
||||
old_tokens_result.append((token_class, 'del', token_text))
|
||||
elif tag == 'insert':
|
||||
for token_class, token_text in new_tokens[n1:n2]:
|
||||
new_tokens_result.append((token_class, 'ins', token_text))
|
||||
elif tag == 'replace':
|
||||
# if same type token blocks must be replaced, do a diff on the
|
||||
# characters in the token blocks to show individual changes
|
||||
|
||||
old_char_tokens = []
|
||||
new_char_tokens = []
|
||||
for token_class, token_text in old_tokens[o1:o2]:
|
||||
for char in token_text:
|
||||
old_char_tokens.append((token_class, char))
|
||||
|
||||
for token_class, token_text in new_tokens[n1:n2]:
|
||||
for char in token_text:
|
||||
new_char_tokens.append((token_class, char))
|
||||
|
||||
old_string = ''.join([token_text for
|
||||
token_class, token_text in old_char_tokens])
|
||||
new_string = ''.join([token_text for
|
||||
token_class, token_text in new_char_tokens])
|
||||
|
||||
char_sequence = difflib.SequenceMatcher(
|
||||
None, old_string, new_string)
|
||||
copcodes = char_sequence.get_opcodes()
|
||||
obuffer, nbuffer = [], []
|
||||
|
||||
if use_diff_match_patch:
|
||||
dmp = diff_match_patch()
|
||||
dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
|
||||
reps = dmp.diff_main(old_string, new_string)
|
||||
dmp.diff_cleanupEfficiency(reps)
|
||||
|
||||
a, b = 0, 0
|
||||
for op, rep in reps:
|
||||
l = len(rep)
|
||||
if op == 0:
|
||||
for i, c in enumerate(rep):
|
||||
obuffer.append((old_char_tokens[a+i][0], '', c))
|
||||
nbuffer.append((new_char_tokens[b+i][0], '', c))
|
||||
a += l
|
||||
b += l
|
||||
elif op == -1:
|
||||
for i, c in enumerate(rep):
|
||||
obuffer.append((old_char_tokens[a+i][0], 'del', c))
|
||||
a += l
|
||||
elif op == 1:
|
||||
for i, c in enumerate(rep):
|
||||
nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
|
||||
b += l
|
||||
else:
|
||||
for ctag, co1, co2, cn1, cn2 in copcodes:
|
||||
if ctag == 'equal':
|
||||
for token_class, token_text in old_char_tokens[co1:co2]:
|
||||
obuffer.append((token_class, '', token_text))
|
||||
for token_class, token_text in new_char_tokens[cn1:cn2]:
|
||||
nbuffer.append((token_class, '', token_text))
|
||||
elif ctag == 'delete':
|
||||
for token_class, token_text in old_char_tokens[co1:co2]:
|
||||
obuffer.append((token_class, 'del', token_text))
|
||||
elif ctag == 'insert':
|
||||
for token_class, token_text in new_char_tokens[cn1:cn2]:
|
||||
nbuffer.append((token_class, 'ins', token_text))
|
||||
elif ctag == 'replace':
|
||||
for token_class, token_text in old_char_tokens[co1:co2]:
|
||||
obuffer.append((token_class, 'del', token_text))
|
||||
for token_class, token_text in new_char_tokens[cn1:cn2]:
|
||||
nbuffer.append((token_class, 'ins', token_text))
|
||||
|
||||
old_tokens_result.extend(obuffer)
|
||||
new_tokens_result.extend(nbuffer)
|
||||
|
||||
return old_tokens_result, new_tokens_result, similarity
|
||||
|
||||
|
||||
class DiffSet(object):
|
||||
"""
|
||||
An object for parsing the diff result from diffs.DiffProcessor and
|
||||
adding highlighting, side by side/unified renderings and line diffs
|
||||
"""
|
||||
|
||||
HL_REAL = 'REAL' # highlights using original file, slow
|
||||
HL_FAST = 'FAST' # highlights using just the line, fast but not correct
|
||||
# in the case of multiline code
|
||||
HL_NONE = 'NONE' # no highlighting, fastest
|
||||
|
||||
def __init__(self, highlight_mode=HL_REAL,
|
||||
source_node_getter=lambda filename: None,
|
||||
target_node_getter=lambda filename: None,
|
||||
source_nodes=None, target_nodes=None,
|
||||
max_file_size_limit=150 * 1024, # files over this size will
|
||||
# use fast highlighting
|
||||
):
|
||||
|
||||
self.highlight_mode = highlight_mode
|
||||
self.highlighted_filenodes = {}
|
||||
self.source_node_getter = source_node_getter
|
||||
self.target_node_getter = target_node_getter
|
||||
self.source_nodes = source_nodes or {}
|
||||
self.target_nodes = target_nodes or {}
|
||||
|
||||
|
||||
self.max_file_size_limit = max_file_size_limit
|
||||
|
||||
def render_patchset(self, patchset, source_ref=None, target_ref=None):
|
||||
diffset = AttributeDict(dict(
|
||||
lines_added=0,
|
||||
lines_deleted=0,
|
||||
changed_files=0,
|
||||
files=[],
|
||||
limited_diff=isinstance(patchset, LimitedDiffContainer),
|
||||
source_ref=source_ref,
|
||||
target_ref=target_ref,
|
||||
))
|
||||
for patch in patchset:
|
||||
filediff = self.render_patch(patch)
|
||||
filediff.diffset = diffset
|
||||
diffset.files.append(filediff)
|
||||
diffset.changed_files += 1
|
||||
if not patch['stats']['binary']:
|
||||
diffset.lines_added += patch['stats']['added']
|
||||
diffset.lines_deleted += patch['stats']['deleted']
|
||||
|
||||
return diffset
|
||||
|
||||
_lexer_cache = {}
|
||||
def _get_lexer_for_filename(self, filename):
|
||||
# cached because we might need to call it twice for source/target
|
||||
if filename not in self._lexer_cache:
|
||||
self._lexer_cache[filename] = get_lexer_safe(filepath=filename)
|
||||
return self._lexer_cache[filename]
|
||||
|
||||
def render_patch(self, patch):
|
||||
log.debug('rendering diff for %r' % patch['filename'])
|
||||
|
||||
source_filename = patch['original_filename']
|
||||
target_filename = patch['filename']
|
||||
|
||||
source_lexer = plain_text_lexer
|
||||
target_lexer = plain_text_lexer
|
||||
|
||||
if not patch['stats']['binary']:
|
||||
if self.highlight_mode == self.HL_REAL:
|
||||
if (source_filename and patch['operation'] in ('D', 'M')
|
||||
and source_filename not in self.source_nodes):
|
||||
self.source_nodes[source_filename] = (
|
||||
self.source_node_getter(source_filename))
|
||||
|
||||
if (target_filename and patch['operation'] in ('A', 'M')
|
||||
and target_filename not in self.target_nodes):
|
||||
self.target_nodes[target_filename] = (
|
||||
self.target_node_getter(target_filename))
|
||||
|
||||
elif self.highlight_mode == self.HL_FAST:
|
||||
source_lexer = self._get_lexer_for_filename(source_filename)
|
||||
target_lexer = self._get_lexer_for_filename(target_filename)
|
||||
|
||||
source_file = self.source_nodes.get(source_filename, source_filename)
|
||||
target_file = self.target_nodes.get(target_filename, target_filename)
|
||||
|
||||
source_filenode, target_filenode = None, None
|
||||
|
||||
# TODO: dan: FileNode.lexer works on the content of the file - which
|
||||
# can be slow - issue #4289 explains a lexer clean up - which once
|
||||
# done can allow caching a lexer for a filenode to avoid the file lookup
|
||||
if isinstance(source_file, FileNode):
|
||||
source_filenode = source_file
|
||||
source_lexer = source_file.lexer
|
||||
if isinstance(target_file, FileNode):
|
||||
target_filenode = target_file
|
||||
target_lexer = target_file.lexer
|
||||
|
||||
source_file_path, target_file_path = None, None
|
||||
|
||||
if source_filename != '/dev/null':
|
||||
source_file_path = source_filename
|
||||
if target_filename != '/dev/null':
|
||||
target_file_path = target_filename
|
||||
|
||||
source_file_type = source_lexer.name
|
||||
target_file_type = target_lexer.name
|
||||
|
||||
op_hunks = patch['chunks'][0]
|
||||
hunks = patch['chunks'][1:]
|
||||
|
||||
filediff = AttributeDict({
|
||||
'source_file_path': source_file_path,
|
||||
'target_file_path': target_file_path,
|
||||
'source_filenode': source_filenode,
|
||||
'target_filenode': target_filenode,
|
||||
'hunks': [],
|
||||
'source_file_type': target_file_type,
|
||||
'target_file_type': source_file_type,
|
||||
'patch': patch,
|
||||
'source_mode': patch['stats']['old_mode'],
|
||||
'target_mode': patch['stats']['new_mode'],
|
||||
'limited_diff': isinstance(patch, LimitedDiffContainer),
|
||||
'diffset': self,
|
||||
})
|
||||
|
||||
for hunk in hunks:
|
||||
hunkbit = self.parse_hunk(hunk, source_file, target_file)
|
||||
hunkbit.filediff = filediff
|
||||
filediff.hunks.append(hunkbit)
|
||||
return filediff
|
||||
|
||||
def parse_hunk(self, hunk, source_file, target_file):
|
||||
result = AttributeDict(dict(
|
||||
source_start=hunk['source_start'],
|
||||
source_length=hunk['source_length'],
|
||||
target_start=hunk['target_start'],
|
||||
target_length=hunk['target_length'],
|
||||
section_header=hunk['section_header'],
|
||||
lines=[],
|
||||
))
|
||||
before, after = [], []
|
||||
|
||||
for line in hunk['lines']:
|
||||
if line['action'] == 'unmod':
|
||||
result.lines.extend(
|
||||
self.parse_lines(before, after, source_file, target_file))
|
||||
after.append(line)
|
||||
before.append(line)
|
||||
elif line['action'] == 'add':
|
||||
after.append(line)
|
||||
elif line['action'] == 'del':
|
||||
before.append(line)
|
||||
elif line['action'] == 'context-old':
|
||||
before.append(line)
|
||||
elif line['action'] == 'context-new':
|
||||
after.append(line)
|
||||
|
||||
result.lines.extend(
|
||||
self.parse_lines(before, after, source_file, target_file))
|
||||
result.unified = self.as_unified(result.lines)
|
||||
result.sideside = result.lines
|
||||
return result
|
||||
|
||||
def parse_lines(self, before_lines, after_lines, source_file, target_file):
|
||||
# TODO: dan: investigate doing the diff comparison and fast highlighting
|
||||
# on the entire before and after buffered block lines rather than by
|
||||
# line, this means we can get better 'fast' highlighting if the context
|
||||
# allows it - eg.
|
||||
# line 4: """
|
||||
# line 5: this gets highlighted as a string
|
||||
# line 6: """
|
||||
|
||||
lines = []
|
||||
while before_lines or after_lines:
|
||||
before, after = None, None
|
||||
before_tokens, after_tokens = None, None
|
||||
|
||||
if before_lines:
|
||||
before = before_lines.pop(0)
|
||||
if after_lines:
|
||||
after = after_lines.pop(0)
|
||||
|
||||
original = AttributeDict()
|
||||
modified = AttributeDict()
|
||||
|
||||
if before:
|
||||
before_tokens = self.get_line_tokens(
|
||||
line_text=before['line'], line_number=before['old_lineno'],
|
||||
file=source_file)
|
||||
original.lineno = before['old_lineno']
|
||||
original.content = before['line']
|
||||
original.action = self.action_to_op(before['action'])
|
||||
|
||||
if after:
|
||||
after_tokens = self.get_line_tokens(
|
||||
line_text=after['line'], line_number=after['new_lineno'],
|
||||
file=target_file)
|
||||
modified.lineno = after['new_lineno']
|
||||
modified.content = after['line']
|
||||
modified.action = self.action_to_op(after['action'])
|
||||
|
||||
|
||||
# diff the lines
|
||||
if before_tokens and after_tokens:
|
||||
o_tokens, m_tokens, similarity = tokens_diff(before_tokens, after_tokens)
|
||||
original.content = render_tokenstream(o_tokens)
|
||||
modified.content = render_tokenstream(m_tokens)
|
||||
elif before_tokens:
|
||||
original.content = render_tokenstream(
|
||||
[(x[0], '', x[1]) for x in before_tokens])
|
||||
elif after_tokens:
|
||||
modified.content = render_tokenstream(
|
||||
[(x[0], '', x[1]) for x in after_tokens])
|
||||
|
||||
lines.append(AttributeDict({
|
||||
'original': original,
|
||||
'modified': modified,
|
||||
}))
|
||||
|
||||
return lines
|
||||
|
||||
def get_line_tokens(self, line_text, line_number, file=None):
|
||||
filenode = None
|
||||
filename = None
|
||||
|
||||
if isinstance(file, basestring):
|
||||
filename = file
|
||||
elif isinstance(file, FileNode):
|
||||
filenode = file
|
||||
filename = file.unicode_path
|
||||
|
||||
if self.highlight_mode == self.HL_REAL and filenode:
|
||||
if line_number and file.size < self.max_file_size_limit:
|
||||
return self.get_tokenized_filenode_line(file, line_number)
|
||||
|
||||
if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
|
||||
lexer = self._get_lexer_for_filename(filename)
|
||||
return list(tokenize_string(line_text, lexer))
|
||||
|
||||
return list(tokenize_string(line_text, plain_text_lexer))
|
||||
|
||||
def get_tokenized_filenode_line(self, filenode, line_number):
|
||||
|
||||
if filenode not in self.highlighted_filenodes:
|
||||
tokenized_lines = filenode_as_lines_tokens(filenode, filenode.lexer)
|
||||
self.highlighted_filenodes[filenode] = tokenized_lines
|
||||
return self.highlighted_filenodes[filenode][line_number - 1]
|
||||
|
||||
def action_to_op(self, action):
|
||||
return {
|
||||
'add': '+',
|
||||
'del': '-',
|
||||
'unmod': ' ',
|
||||
'context-old': ' ',
|
||||
'context-new': ' ',
|
||||
}.get(action, action)
|
||||
|
||||
def as_unified(self, lines):
|
||||
""" Return a generator that yields the lines of a diff in unified order """
|
||||
def generator():
|
||||
buf = []
|
||||
for line in lines:
|
||||
|
||||
if buf and not line.original or line.original.action == ' ':
|
||||
for b in buf:
|
||||
yield b
|
||||
buf = []
|
||||
|
||||
if line.original:
|
||||
if line.original.action == ' ':
|
||||
yield (line.original.lineno, line.modified.lineno,
|
||||
line.original.action, line.original.content)
|
||||
continue
|
||||
|
||||
if line.original.action == '-':
|
||||
yield (line.original.lineno, None,
|
||||
line.original.action, line.original.content)
|
||||
|
||||
if line.modified.action == '+':
|
||||
buf.append((
|
||||
None, line.modified.lineno,
|
||||
line.modified.action, line.modified.content))
|
||||
continue
|
||||
|
||||
if line.modified:
|
||||
yield (None, line.modified.lineno,
|
||||
line.modified.action, line.modified.content)
|
||||
|
||||
for b in buf:
|
||||
yield b
|
||||
|
||||
return generator()
|
||||
|
|
|
|||
1919
rhodecode/lib/diff_match_patch.py
Normal file
1919
rhodecode/lib/diff_match_patch.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -180,6 +180,8 @@ class Action(object):
|
|||
UNMODIFIED = 'unmod'
|
||||
|
||||
CONTEXT = 'context'
|
||||
CONTEXT_OLD = 'context-old'
|
||||
CONTEXT_NEW = 'context-new'
|
||||
|
||||
|
||||
class DiffProcessor(object):
|
||||
|
|
@ -227,7 +229,7 @@ class DiffProcessor(object):
|
|||
self._parser = self._parse_gitdiff
|
||||
else:
|
||||
self.differ = self._highlight_line_udiff
|
||||
self._parser = self._parse_udiff
|
||||
self._parser = self._new_parse_gitdiff
|
||||
|
||||
def _copy_iterator(self):
|
||||
"""
|
||||
|
|
@ -491,9 +493,181 @@ class DiffProcessor(object):
|
|||
|
||||
return diff_container(sorted(_files, key=sorter))
|
||||
|
||||
def _parse_udiff(self, inline_diff=True):
|
||||
raise NotImplementedError()
|
||||
|
||||
# FIXME: NEWDIFFS: dan: this replaces the old _escaper function
|
||||
def _process_line(self, string):
|
||||
"""
|
||||
Process a diff line, checks the diff limit
|
||||
|
||||
:param string:
|
||||
"""
|
||||
|
||||
self.cur_diff_size += len(string)
|
||||
|
||||
if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
|
||||
raise DiffLimitExceeded('Diff Limit Exceeded')
|
||||
|
||||
return safe_unicode(string)
|
||||
|
||||
# FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
|
||||
def _new_parse_gitdiff(self, inline_diff=True):
|
||||
_files = []
|
||||
diff_container = lambda arg: arg
|
||||
for chunk in self._diff.chunks():
|
||||
head = chunk.header
|
||||
log.debug('parsing diff %r' % head)
|
||||
|
||||
diff = imap(self._process_line, chunk.diff.splitlines(1))
|
||||
raw_diff = chunk.raw
|
||||
limited_diff = False
|
||||
exceeds_limit = False
|
||||
# if 'empty_file_to_modify_and_rename' in head['a_path']:
|
||||
# 1/0
|
||||
op = None
|
||||
stats = {
|
||||
'added': 0,
|
||||
'deleted': 0,
|
||||
'binary': False,
|
||||
'old_mode': None,
|
||||
'new_mode': None,
|
||||
'ops': {},
|
||||
}
|
||||
if head['old_mode']:
|
||||
stats['old_mode'] = head['old_mode']
|
||||
if head['new_mode']:
|
||||
stats['new_mode'] = head['new_mode']
|
||||
if head['b_mode']:
|
||||
stats['new_mode'] = head['b_mode']
|
||||
|
||||
if head['deleted_file_mode']:
|
||||
op = OPS.DEL
|
||||
stats['binary'] = True
|
||||
stats['ops'][DEL_FILENODE] = 'deleted file'
|
||||
|
||||
elif head['new_file_mode']:
|
||||
op = OPS.ADD
|
||||
stats['binary'] = True
|
||||
stats['old_mode'] = None
|
||||
stats['new_mode'] = head['new_file_mode']
|
||||
stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
|
||||
else: # modify operation, can be copy, rename or chmod
|
||||
|
||||
# CHMOD
|
||||
if head['new_mode'] and head['old_mode']:
|
||||
op = OPS.MOD
|
||||
stats['binary'] = True
|
||||
stats['ops'][CHMOD_FILENODE] = (
|
||||
'modified file chmod %s => %s' % (
|
||||
head['old_mode'], head['new_mode']))
|
||||
|
||||
# RENAME
|
||||
if head['rename_from'] != head['rename_to']:
|
||||
op = OPS.MOD
|
||||
stats['binary'] = True
|
||||
stats['renamed'] = (head['rename_from'], head['rename_to'])
|
||||
stats['ops'][RENAMED_FILENODE] = (
|
||||
'file renamed from %s to %s' % (
|
||||
head['rename_from'], head['rename_to']))
|
||||
# COPY
|
||||
if head.get('copy_from') and head.get('copy_to'):
|
||||
op = OPS.MOD
|
||||
stats['binary'] = True
|
||||
stats['copied'] = (head['copy_from'], head['copy_to'])
|
||||
stats['ops'][COPIED_FILENODE] = (
|
||||
'file copied from %s to %s' % (
|
||||
head['copy_from'], head['copy_to']))
|
||||
|
||||
# If our new parsed headers didn't match anything fallback to
|
||||
# old style detection
|
||||
if op is None:
|
||||
if not head['a_file'] and head['b_file']:
|
||||
op = OPS.ADD
|
||||
stats['binary'] = True
|
||||
stats['new_file'] = True
|
||||
stats['ops'][NEW_FILENODE] = 'new file'
|
||||
|
||||
elif head['a_file'] and not head['b_file']:
|
||||
op = OPS.DEL
|
||||
stats['binary'] = True
|
||||
stats['ops'][DEL_FILENODE] = 'deleted file'
|
||||
|
||||
# it's not ADD not DELETE
|
||||
if op is None:
|
||||
op = OPS.MOD
|
||||
stats['binary'] = True
|
||||
stats['ops'][MOD_FILENODE] = 'modified file'
|
||||
|
||||
# a real non-binary diff
|
||||
if head['a_file'] or head['b_file']:
|
||||
try:
|
||||
raw_diff, chunks, _stats = self._new_parse_lines(diff)
|
||||
stats['binary'] = False
|
||||
stats['added'] = _stats[0]
|
||||
stats['deleted'] = _stats[1]
|
||||
# explicit mark that it's a modified file
|
||||
if op == OPS.MOD:
|
||||
stats['ops'][MOD_FILENODE] = 'modified file'
|
||||
exceeds_limit = len(raw_diff) > self.file_limit
|
||||
|
||||
# changed from _escaper function so we validate size of
|
||||
# each file instead of the whole diff
|
||||
# diff will hide big files but still show small ones
|
||||
# from my tests, big files are fairly safe to be parsed
|
||||
# but the browser is the bottleneck
|
||||
if not self.show_full_diff and exceeds_limit:
|
||||
raise DiffLimitExceeded('File Limit Exceeded')
|
||||
|
||||
except DiffLimitExceeded:
|
||||
diff_container = lambda _diff: \
|
||||
LimitedDiffContainer(
|
||||
self.diff_limit, self.cur_diff_size, _diff)
|
||||
|
||||
exceeds_limit = len(raw_diff) > self.file_limit
|
||||
limited_diff = True
|
||||
chunks = []
|
||||
|
||||
else: # GIT format binary patch, or possibly empty diff
|
||||
if head['bin_patch']:
|
||||
# we have operation already extracted, but we mark simply
|
||||
# it's a diff we wont show for binary files
|
||||
stats['ops'][BIN_FILENODE] = 'binary diff hidden'
|
||||
chunks = []
|
||||
|
||||
if chunks and not self.show_full_diff and op == OPS.DEL:
|
||||
# if not full diff mode show deleted file contents
|
||||
# TODO: anderson: if the view is not too big, there is no way
|
||||
# to see the content of the file
|
||||
chunks = []
|
||||
|
||||
chunks.insert(0, [{
|
||||
'old_lineno': '',
|
||||
'new_lineno': '',
|
||||
'action': Action.CONTEXT,
|
||||
'line': msg,
|
||||
} for _op, msg in stats['ops'].iteritems()
|
||||
if _op not in [MOD_FILENODE]])
|
||||
|
||||
original_filename = safe_unicode(head['a_path'])
|
||||
_files.append({
|
||||
'original_filename': original_filename,
|
||||
'filename': safe_unicode(head['b_path']),
|
||||
'old_revision': head['a_blob_id'],
|
||||
'new_revision': head['b_blob_id'],
|
||||
'chunks': chunks,
|
||||
'raw_diff': safe_unicode(raw_diff),
|
||||
'operation': op,
|
||||
'stats': stats,
|
||||
'exceeds_limit': exceeds_limit,
|
||||
'is_limited_diff': limited_diff,
|
||||
})
|
||||
|
||||
|
||||
sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
|
||||
OPS.DEL: 2}.get(info['operation'])
|
||||
|
||||
return diff_container(sorted(_files, key=sorter))
|
||||
|
||||
# FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
|
||||
def _parse_lines(self, diff):
|
||||
"""
|
||||
Parse the diff an return data for the template.
|
||||
|
|
@ -588,6 +762,107 @@ class DiffProcessor(object):
|
|||
pass
|
||||
return ''.join(raw_diff), chunks, stats
|
||||
|
||||
# FIXME: NEWDIFFS: dan: this replaces _parse_lines
|
||||
def _new_parse_lines(self, diff):
|
||||
"""
|
||||
Parse the diff an return data for the template.
|
||||
"""
|
||||
|
||||
lineiter = iter(diff)
|
||||
stats = [0, 0]
|
||||
chunks = []
|
||||
raw_diff = []
|
||||
|
||||
try:
|
||||
line = lineiter.next()
|
||||
|
||||
while line:
|
||||
raw_diff.append(line)
|
||||
match = self._chunk_re.match(line)
|
||||
|
||||
if not match:
|
||||
break
|
||||
|
||||
gr = match.groups()
|
||||
(old_line, old_end,
|
||||
new_line, new_end) = [int(x or 1) for x in gr[:-1]]
|
||||
|
||||
lines = []
|
||||
hunk = {
|
||||
'section_header': gr[-1],
|
||||
'source_start': old_line,
|
||||
'source_length': old_end,
|
||||
'target_start': new_line,
|
||||
'target_length': new_end,
|
||||
'lines': lines,
|
||||
}
|
||||
chunks.append(hunk)
|
||||
|
||||
old_line -= 1
|
||||
new_line -= 1
|
||||
|
||||
context = len(gr) == 5
|
||||
old_end += old_line
|
||||
new_end += new_line
|
||||
|
||||
line = lineiter.next()
|
||||
|
||||
while old_line < old_end or new_line < new_end:
|
||||
command = ' '
|
||||
if line:
|
||||
command = line[0]
|
||||
|
||||
affects_old = affects_new = False
|
||||
|
||||
# ignore those if we don't expect them
|
||||
if command in '#@':
|
||||
continue
|
||||
elif command == '+':
|
||||
affects_new = True
|
||||
action = Action.ADD
|
||||
stats[0] += 1
|
||||
elif command == '-':
|
||||
affects_old = True
|
||||
action = Action.DELETE
|
||||
stats[1] += 1
|
||||
else:
|
||||
affects_old = affects_new = True
|
||||
action = Action.UNMODIFIED
|
||||
|
||||
if not self._newline_marker.match(line):
|
||||
old_line += affects_old
|
||||
new_line += affects_new
|
||||
lines.append({
|
||||
'old_lineno': affects_old and old_line or '',
|
||||
'new_lineno': affects_new and new_line or '',
|
||||
'action': action,
|
||||
'line': self._clean_line(line, command)
|
||||
})
|
||||
raw_diff.append(line)
|
||||
|
||||
line = lineiter.next()
|
||||
|
||||
if self._newline_marker.match(line):
|
||||
# we need to append to lines, since this is not
|
||||
# counted in the line specs of diff
|
||||
if affects_old:
|
||||
action = Action.CONTEXT_OLD
|
||||
elif affects_new:
|
||||
action = Action.CONTEXT_NEW
|
||||
else:
|
||||
raise Exception('invalid context for no newline')
|
||||
|
||||
lines.append({
|
||||
'old_lineno': None,
|
||||
'new_lineno': None,
|
||||
'action': action,
|
||||
'line': self._clean_line(line, command)
|
||||
})
|
||||
|
||||
except StopIteration:
|
||||
pass
|
||||
return ''.join(raw_diff), chunks, stats
|
||||
|
||||
def _safe_id(self, idstring):
|
||||
"""Make a string safe for including in an id attribute.
|
||||
|
||||
|
|
|
|||
|
|
@ -32,11 +32,13 @@ class GitDiff(base.Diff):
|
|||
_header_re = re.compile(r"""
|
||||
#^diff[ ]--git
|
||||
[ ]"?a/(?P<a_path>.+?)"?[ ]"?b/(?P<b_path>.+?)"?\n
|
||||
(?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
|
||||
^rename[ ]from[ ](?P<rename_from>[^\r\n]+)\n
|
||||
^rename[ ]to[ ](?P<rename_to>[^\r\n]+)(?:\n|$))?
|
||||
(?:^old[ ]mode[ ](?P<old_mode>\d+)\n
|
||||
^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
|
||||
(?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
|
||||
(?:^rename[ ]from[ ](?P<rename_from>[^\r\n]+)\n
|
||||
^rename[ ]to[ ](?P<rename_to>[^\r\n]+)(?:\n|$))?
|
||||
(?:^copy[ ]from[ ](?P<copy_from>[^\r\n]+)\n
|
||||
^copy[ ]to[ ](?P<copy_to>[^\r\n]+)(?:\n|$))?
|
||||
(?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
|
||||
(?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
|
||||
(?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
|
||||
|
|
|
|||
|
|
@ -188,6 +188,14 @@ input[type="button"] {
|
|||
padding: @padding * 1.2;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: inline-block;
|
||||
.btn {
|
||||
float: left;
|
||||
margin: 0 0 0 -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
|
|
|||
|
|
@ -646,15 +646,211 @@ pre.literal-block, .codehilite pre{
|
|||
|
||||
@cb-line-height: 18px;
|
||||
@cb-line-code-padding: 10px;
|
||||
@cb-text-padding: 5px;
|
||||
|
||||
table.cb {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 10px;
|
||||
@diff-pill-padding: 2px 7px;
|
||||
|
||||
input.diff-collapse-state {
|
||||
display: none;
|
||||
|
||||
&:checked + .diff { /* file diff is collapsed */
|
||||
.cb {
|
||||
display: none
|
||||
}
|
||||
.diff-collapse-indicator {
|
||||
border-width: 9px 0 9px 15.6px;
|
||||
border-color: transparent transparent transparent #ccc;
|
||||
}
|
||||
.diff-menu {
|
||||
display: none;
|
||||
}
|
||||
margin: -1px 0 0 0;
|
||||
}
|
||||
|
||||
&+ .diff { /* file diff is expanded */
|
||||
.diff-collapse-indicator {
|
||||
border-width: 15.6px 9px 0 9px;
|
||||
border-color: #ccc transparent transparent transparent;
|
||||
}
|
||||
.diff-menu {
|
||||
display: block;
|
||||
}
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
.diff {
|
||||
border: 1px solid @grey5;
|
||||
|
||||
/* START OVERRIDES */
|
||||
.code-highlight {
|
||||
border: none; // TODO: remove this border from the global
|
||||
// .code-highlight, it doesn't belong there
|
||||
}
|
||||
label {
|
||||
margin: 0; // TODO: remove this margin definition from global label
|
||||
// it doesn't belong there - if margin on labels
|
||||
// are needed for a form they should be defined
|
||||
// in the form's class
|
||||
}
|
||||
/* END OVERRIDES */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.diff-anchor {
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover {
|
||||
.diff-anchor {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.diff-collapse-indicator {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
float: left;
|
||||
margin: 2px 2px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.diff-heading {
|
||||
background: @grey7;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.diff-heading:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
.diff-heading:hover {
|
||||
background: #e1e9f4 !important;
|
||||
}
|
||||
|
||||
.diff-menu {
|
||||
float: right;
|
||||
a, button {
|
||||
padding: 5px;
|
||||
display: block;
|
||||
float: left
|
||||
}
|
||||
}
|
||||
.diff-pill {
|
||||
display: block;
|
||||
float: left;
|
||||
padding: @diff-pill-padding;
|
||||
}
|
||||
.diff-pill-group {
|
||||
.diff-pill {
|
||||
opacity: .8;
|
||||
&:first-child {
|
||||
border-radius: @border-radius 0 0 @border-radius;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 @border-radius @border-radius 0;
|
||||
}
|
||||
&:only-child {
|
||||
border-radius: @border-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
.diff-pill {
|
||||
&[op="name"] {
|
||||
background: none;
|
||||
color: @grey2;
|
||||
opacity: 1;
|
||||
color: white;
|
||||
}
|
||||
&[op="limited"] {
|
||||
background: @grey2;
|
||||
color: white;
|
||||
}
|
||||
&[op="binary"] {
|
||||
background: @color7;
|
||||
color: white;
|
||||
}
|
||||
&[op="modified"] {
|
||||
background: @alert1;
|
||||
color: white;
|
||||
}
|
||||
&[op="renamed"] {
|
||||
background: @color4;
|
||||
color: white;
|
||||
}
|
||||
&[op="mode"] {
|
||||
background: @grey3;
|
||||
color: white;
|
||||
}
|
||||
&[op="symlink"] {
|
||||
background: @color8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&[op="added"] { /* added lines */
|
||||
background: @alert1;
|
||||
color: white;
|
||||
}
|
||||
&[op="deleted"] { /* deleted lines */
|
||||
background: @alert2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&[op="created"] { /* created file */
|
||||
background: @alert1;
|
||||
color: white;
|
||||
}
|
||||
&[op="removed"] { /* deleted file */
|
||||
background: @color5;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.diff-collapse-button, .diff-expand-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.diff-collapse-button {
|
||||
display: inline;
|
||||
}
|
||||
.diff-expand-button {
|
||||
display: none;
|
||||
}
|
||||
.diff-collapsed .diff-collapse-button {
|
||||
display: none;
|
||||
}
|
||||
.diff-collapsed .diff-expand-button {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
table.cb {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
.cb-text {
|
||||
padding: @cb-text-padding;
|
||||
}
|
||||
.cb-hunk {
|
||||
padding: @cb-text-padding;
|
||||
}
|
||||
.cb-expand {
|
||||
display: none;
|
||||
}
|
||||
.cb-collapse {
|
||||
display: inline;
|
||||
}
|
||||
&.cb-collapsed {
|
||||
.cb-line {
|
||||
display: none;
|
||||
}
|
||||
.cb-expand {
|
||||
display: inline;
|
||||
}
|
||||
.cb-collapse {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* intentionally general selector since .cb-line-selected must override it
|
||||
and they both use !important since the td itself may have a random color
|
||||
|
|
@ -663,18 +859,45 @@ table.cb {
|
|||
.cb-line-fresh .cb-content {
|
||||
background: white !important;
|
||||
}
|
||||
.cb-warning {
|
||||
background: #fff4dd;
|
||||
}
|
||||
|
||||
tr.cb-annotate {
|
||||
border-top: 1px solid #eee;
|
||||
&.cb-diff-sideside {
|
||||
td {
|
||||
&.cb-content {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&+ .cb-line {
|
||||
tr {
|
||||
&.cb-annotate {
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
&+ .cb-line {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
&+ .cb-line {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
&+ .cb-line {
|
||||
border-top: none;
|
||||
&.cb-hunk {
|
||||
font-family: @font-family-monospace;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
|
||||
td {
|
||||
&:first-child {
|
||||
background: #edf2f9;
|
||||
}
|
||||
&:last-child {
|
||||
background: #f4f7fb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -686,9 +909,14 @@ table.cb {
|
|||
&.cb-content {
|
||||
font-size: 12.35px;
|
||||
|
||||
&.cb-line-selected .cb-code {
|
||||
background: @comment-highlight-color !important;
|
||||
}
|
||||
|
||||
span.cb-code {
|
||||
line-height: @cb-line-height;
|
||||
padding-left: @cb-line-code-padding;
|
||||
padding-right: @cb-line-code-padding;
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
font-family: @font-family-monospace;
|
||||
|
|
@ -714,14 +942,38 @@ table.cb {
|
|||
a {
|
||||
display: block;
|
||||
padding-right: @cb-line-code-padding;
|
||||
padding-left: @cb-line-code-padding;
|
||||
line-height: @cb-line-height;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&.cb-content {
|
||||
&.cb-line-selected .cb-code {
|
||||
background: @comment-highlight-color !important;
|
||||
&.cb-empty {
|
||||
background: @grey7;
|
||||
}
|
||||
|
||||
ins {
|
||||
color: black;
|
||||
background: #a6f3a6;
|
||||
text-decoration: none;
|
||||
}
|
||||
del {
|
||||
color: black;
|
||||
background: #f8cbcb;
|
||||
text-decoration: none;
|
||||
}
|
||||
&.cb-addition {
|
||||
background: #ecffec;
|
||||
|
||||
&.blob-lineno {
|
||||
background: #ddffdd;
|
||||
}
|
||||
}
|
||||
&.cb-deletion {
|
||||
background: #ffecec;
|
||||
|
||||
&.blob-lineno {
|
||||
background: #ffdddd;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -221,14 +221,32 @@ var formatSelect2SelectionRefs = function(commit_ref){
|
|||
};
|
||||
|
||||
// takes a given html element and scrolls it down offset pixels
|
||||
function offsetScroll(element, offset){
|
||||
setTimeout(function(){
|
||||
function offsetScroll(element, offset) {
|
||||
setTimeout(function() {
|
||||
var location = element.offset().top;
|
||||
// some browsers use body, some use html
|
||||
$('html, body').animate({ scrollTop: (location - offset) });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// scroll an element `percent`% from the top of page in `time` ms
|
||||
function scrollToElement(element, percent, time) {
|
||||
percent = (percent === undefined ? 25 : percent);
|
||||
time = (time === undefined ? 100 : time);
|
||||
|
||||
var $element = $(element);
|
||||
var elOffset = $element.offset().top;
|
||||
var elHeight = $element.height();
|
||||
var windowHeight = $(window).height();
|
||||
var offset = elOffset;
|
||||
if (elHeight < windowHeight) {
|
||||
offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
|
||||
}
|
||||
setTimeout(function() {
|
||||
$('html, body').animate({ scrollTop: offset});
|
||||
}, time);
|
||||
}
|
||||
|
||||
/**
|
||||
* global hooks after DOM is loaded
|
||||
*/
|
||||
|
|
@ -418,6 +436,10 @@ $(document).ready(function() {
|
|||
var result = splitDelimitedHash(location.hash);
|
||||
var loc = result.loc;
|
||||
if (loc.length > 1) {
|
||||
|
||||
var highlightable_line_tds = [];
|
||||
|
||||
// source code line format
|
||||
var page_highlights = loc.substring(
|
||||
loc.indexOf('#') + 1).split('L');
|
||||
|
||||
|
|
@ -442,33 +464,27 @@ $(document).ready(function() {
|
|||
for (pos in h_lines) {
|
||||
var line_td = $('td.cb-lineno#L' + h_lines[pos]);
|
||||
if (line_td.length) {
|
||||
line_td.addClass('cb-line-selected'); // line number td
|
||||
line_td.next().addClass('cb-line-selected'); // line content
|
||||
highlightable_line_tds.push(line_td);
|
||||
}
|
||||
}
|
||||
var first_line_td = $('td.cb-lineno#L' + h_lines[0]);
|
||||
if (first_line_td.length) {
|
||||
var elOffset = first_line_td.offset().top;
|
||||
var elHeight = first_line_td.height();
|
||||
var windowHeight = $(window).height();
|
||||
var offset;
|
||||
}
|
||||
|
||||
if (elHeight < windowHeight) {
|
||||
offset = elOffset - ((windowHeight / 4) - (elHeight / 2));
|
||||
}
|
||||
else {
|
||||
offset = elOffset;
|
||||
}
|
||||
$(function() { // let browser scroll to hash first, then
|
||||
// scroll the line to the middle of page
|
||||
setTimeout(function() {
|
||||
$('html, body').animate({ scrollTop: offset });
|
||||
}, 100);
|
||||
});
|
||||
$.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
|
||||
lineno: first_line_td,
|
||||
remainder: result.remainder});
|
||||
}
|
||||
// now check a direct id reference (diff page)
|
||||
if ($(loc).length && $(loc).hasClass('cb-lineno')) {
|
||||
highlightable_line_tds.push($(loc));
|
||||
}
|
||||
$.each(highlightable_line_tds, function (i, $td) {
|
||||
$td.addClass('cb-line-selected'); // line number td
|
||||
$td.next().addClass('cb-line-selected'); // line content
|
||||
});
|
||||
|
||||
if (highlightable_line_tds.length) {
|
||||
var $first_line_td = highlightable_line_tds[0];
|
||||
scrollToElement($first_line_td);
|
||||
$.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
|
||||
lineno: $first_line_td,
|
||||
remainder: result.remainder
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
398
rhodecode/templates/codeblocks/diffs.html
Normal file
398
rhodecode/templates/codeblocks/diffs.html
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
<%def name="diff_line_anchor(filename, line, type)"><%
|
||||
return '%s_%s_%i' % (h.safeid(filename), type, line)
|
||||
%></%def>
|
||||
|
||||
<%def name="action_class(action)"><%
|
||||
return {
|
||||
'-': 'cb-deletion',
|
||||
'+': 'cb-addition',
|
||||
' ': 'cb-context',
|
||||
}.get(action, 'cb-empty')
|
||||
%></%def>
|
||||
|
||||
<%def name="op_class(op_id)"><%
|
||||
return {
|
||||
DEL_FILENODE: 'deletion', # file deleted
|
||||
BIN_FILENODE: 'warning' # binary diff hidden
|
||||
}.get(op_id, 'addition')
|
||||
%></%def>
|
||||
|
||||
<%def name="link_for(**kw)"><%
|
||||
new_args = request.GET.mixed()
|
||||
new_args.update(kw)
|
||||
return h.url('', **new_args)
|
||||
%></%def>
|
||||
|
||||
<%def name="render_diffset(diffset,
|
||||
|
||||
# collapse all file diff entries when there are more than this amount of files in the diff
|
||||
collapse_when_files_over=20,
|
||||
|
||||
# collapse lines in the diff when more than this amount of lines changed in the file diff
|
||||
lines_changed_limit=500,
|
||||
)">
|
||||
<%
|
||||
# TODO: dan: move this to an argument - and set a cookie so that it is saved
|
||||
# default option for future requests
|
||||
diff_mode = request.GET.get('diffmode', 'sideside')
|
||||
if diff_mode not in ('sideside', 'unified'):
|
||||
diff_mode = 'sideside'
|
||||
|
||||
collapse_all = len(diffset.files) > collapse_when_files_over
|
||||
%>
|
||||
|
||||
%if diff_mode == 'sideside':
|
||||
<style>
|
||||
.wrapper {
|
||||
max-width: 1600px !important;
|
||||
}
|
||||
</style>
|
||||
%endif
|
||||
|
||||
% if diffset.limited_diff:
|
||||
<div class="alert alert-warning">
|
||||
${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="cs_files">
|
||||
<div class="cs_files_title">
|
||||
%if diffset.files:
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<a
|
||||
class="btn ${diff_mode == 'sideside' and 'btn-primary'} tooltip"
|
||||
title="${_('View side by side')}"
|
||||
href="${link_for(diffmode='sideside')}">
|
||||
<span>${_('Side by Side')}</span>
|
||||
</a>
|
||||
<a
|
||||
class="btn ${diff_mode == 'unified' and 'btn-primary'} tooltip"
|
||||
title="${_('View unified')}" href="${link_for(diffmode='unified')}">
|
||||
<span>${_('Unified')}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-left">
|
||||
<div class="btn-group">
|
||||
<a
|
||||
class="btn"
|
||||
href="#"
|
||||
onclick="$('input[class=diff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
|
||||
<a
|
||||
class="btn"
|
||||
href="#"
|
||||
onclick="$('input[class=diff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
<h2 style="padding: 5px; text-align: center;">
|
||||
%if diffset.limited_diff:
|
||||
${ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files}}
|
||||
%else:
|
||||
${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
|
||||
'%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
|
||||
%endif
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
%if not diffset.files:
|
||||
<p class="empty_data">${_('No files')}</p>
|
||||
%endif
|
||||
|
||||
<div class="filediffs">
|
||||
%for i, filediff in enumerate(diffset.files):
|
||||
<%
|
||||
lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
|
||||
over_lines_changed_limit = lines_changed > lines_changed_limit
|
||||
%>
|
||||
<input ${collapse_all and 'checked' or ''} class="diff-collapse-state" id="diff-collapse-${i}" type="checkbox">
|
||||
<div
|
||||
class="diff"
|
||||
data-f-path="${filediff['patch']['filename']}"
|
||||
id="a_${h.FID('', filediff['patch']['filename'])}">
|
||||
<label for="diff-collapse-${i}" class="diff-heading">
|
||||
<div class="diff-collapse-indicator"></div>
|
||||
${diff_ops(filediff)}
|
||||
</label>
|
||||
${diff_menu(filediff)}
|
||||
<table class="cb cb-diff-${diff_mode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
|
||||
%if not filediff.hunks:
|
||||
%for op_id, op_text in filediff['patch']['stats']['ops'].items():
|
||||
<tr>
|
||||
<td class="cb-text cb-${op_class(op_id)}" ${diff_mode == 'unified' and 'colspan=3' or 'colspan=4'}>
|
||||
%if op_id == DEL_FILENODE:
|
||||
${_('File was deleted')}
|
||||
%elif op_id == BIN_FILENODE:
|
||||
${_('Binary file hidden')}
|
||||
%else:
|
||||
${op_text}
|
||||
%endif
|
||||
</td>
|
||||
</tr>
|
||||
%endfor
|
||||
%endif
|
||||
%if over_lines_changed_limit:
|
||||
<tr class="cb-warning cb-collapser">
|
||||
<td class="cb-text" ${diff_mode == 'unified' and 'colspan=3' or 'colspan=4'}>
|
||||
${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
|
||||
<a href="#" class="cb-expand"
|
||||
onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
|
||||
</a>
|
||||
<a href="#" class="cb-collapse"
|
||||
onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
%endif
|
||||
%if filediff.patch['is_limited_diff']:
|
||||
<tr class="cb-warning cb-collapser">
|
||||
<td class="cb-text" ${diff_mode == 'unified' and 'colspan=3' or 'colspan=4'}>
|
||||
${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
|
||||
</td>
|
||||
</tr>
|
||||
%endif
|
||||
%for hunk in filediff.hunks:
|
||||
<tr class="cb-hunk">
|
||||
<td ${diff_mode == 'unified' and 'colspan=2' or ''}>
|
||||
## TODO: dan: add ajax loading of more context here
|
||||
## <a href="#">
|
||||
<i class="icon-more"></i>
|
||||
## </a>
|
||||
</td>
|
||||
<td ${diff_mode == 'sideside' and 'colspan=3' or ''}>
|
||||
@@
|
||||
-${hunk.source_start},${hunk.source_length}
|
||||
+${hunk.target_start},${hunk.target_length}
|
||||
${hunk.section_header}
|
||||
</td>
|
||||
</tr>
|
||||
%if diff_mode == 'unified':
|
||||
${render_hunk_lines_unified(hunk)}
|
||||
%elif diff_mode == 'sideside':
|
||||
${render_hunk_lines_sideside(hunk)}
|
||||
%else:
|
||||
<tr class="cb-line">
|
||||
<td>unknown diff mode</td>
|
||||
</tr>
|
||||
%endif
|
||||
%endfor
|
||||
</table>
|
||||
</div>
|
||||
%endfor
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="diff_ops(filediff)">
|
||||
<%
|
||||
stats = filediff['patch']['stats']
|
||||
from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
|
||||
MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
|
||||
%>
|
||||
<span class="diff-pill">
|
||||
%if filediff.source_file_path and filediff.target_file_path:
|
||||
%if filediff.source_file_path != filediff.target_file_path: # file was renamed
|
||||
<strong>${filediff.target_file_path}</strong> ⬅ <del>${filediff.source_file_path}</del>
|
||||
%else:
|
||||
## file was modified
|
||||
<strong>${filediff.source_file_path}</strong>
|
||||
%endif
|
||||
%else:
|
||||
%if filediff.source_file_path:
|
||||
## file was deleted
|
||||
<strong>${filediff.source_file_path}</strong>
|
||||
%else:
|
||||
## file was added
|
||||
<strong>${filediff.target_file_path}</strong>
|
||||
%endif
|
||||
%endif
|
||||
</span>
|
||||
<span class="diff-pill-group" style="float: left">
|
||||
%if filediff.patch['is_limited_diff']:
|
||||
<span class="diff-pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
|
||||
%endif
|
||||
%if RENAMED_FILENODE in stats['ops']:
|
||||
<span class="diff-pill" op="renamed">renamed</span>
|
||||
%endif
|
||||
|
||||
%if NEW_FILENODE in stats['ops']:
|
||||
<span class="diff-pill" op="created">created</span>
|
||||
%if filediff['target_mode'].startswith('120'):
|
||||
<span class="diff-pill" op="symlink">symlink</span>
|
||||
%else:
|
||||
<span class="diff-pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
|
||||
%endif
|
||||
%endif
|
||||
|
||||
%if DEL_FILENODE in stats['ops']:
|
||||
<span class="diff-pill" op="removed">removed</span>
|
||||
%endif
|
||||
|
||||
%if CHMOD_FILENODE in stats['ops']:
|
||||
<span class="diff-pill" op="mode">
|
||||
${nice_mode(filediff['source_mode'])} ➡ ${nice_mode(filediff['target_mode'])}
|
||||
</span>
|
||||
%endif
|
||||
</span>
|
||||
|
||||
<a class="diff-pill diff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">¶</a>
|
||||
|
||||
<span class="diff-pill-group" style="float: right">
|
||||
%if BIN_FILENODE in stats['ops']:
|
||||
<span class="diff-pill" op="binary">binary</span>
|
||||
%if MOD_FILENODE in stats['ops']:
|
||||
<span class="diff-pill" op="modified">modified</span>
|
||||
%endif
|
||||
%endif
|
||||
%if stats['deleted']:
|
||||
<span class="diff-pill" op="deleted">-${stats['deleted']}</span>
|
||||
%endif
|
||||
%if stats['added']:
|
||||
<span class="diff-pill" op="added">+${stats['added']}</span>
|
||||
%endif
|
||||
</span>
|
||||
|
||||
</%def>
|
||||
|
||||
<%def name="nice_mode(filemode)">
|
||||
${filemode.startswith('100') and filemode[3:] or filemode}
|
||||
</%def>
|
||||
|
||||
<%def name="diff_menu(filediff)">
|
||||
<div class="diff-menu">
|
||||
%if filediff.diffset.source_ref:
|
||||
%if filediff.patch['operation'] in ['D', 'M']:
|
||||
<a
|
||||
class="tooltip"
|
||||
href="${h.url('files_home',repo_name=c.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
|
||||
title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
|
||||
>
|
||||
${_('Show file before')}
|
||||
</a>
|
||||
%else:
|
||||
<a
|
||||
disabled
|
||||
class="tooltip"
|
||||
title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
|
||||
>
|
||||
${_('Show file before')}
|
||||
</a>
|
||||
%endif
|
||||
%if filediff.patch['operation'] in ['A', 'M']:
|
||||
<a
|
||||
class="tooltip"
|
||||
href="${h.url('files_home',repo_name=c.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
|
||||
title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
|
||||
>
|
||||
${_('Show file after')}
|
||||
</a>
|
||||
%else:
|
||||
<a
|
||||
disabled
|
||||
class="tooltip"
|
||||
title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
|
||||
>
|
||||
${_('Show file after')}
|
||||
</a>
|
||||
%endif
|
||||
<a
|
||||
class="tooltip"
|
||||
title="${h.tooltip(_('Raw diff'))}"
|
||||
href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
|
||||
>
|
||||
${_('Raw diff')}
|
||||
</a>
|
||||
<a
|
||||
class="tooltip"
|
||||
title="${h.tooltip(_('Download diff'))}"
|
||||
href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
|
||||
>
|
||||
${_('Download diff')}
|
||||
</a>
|
||||
%endif
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
|
||||
<%def name="render_hunk_lines_sideside(hunk)">
|
||||
%for i, line in enumerate(hunk.sideside):
|
||||
<%
|
||||
old_line_anchor, new_line_anchor = None, None
|
||||
if line.original.lineno:
|
||||
old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
|
||||
if line.modified.lineno:
|
||||
new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
|
||||
%>
|
||||
<tr class="cb-line">
|
||||
<td class="cb-lineno ${action_class(line.original.action)}"
|
||||
data-line-number="${line.original.lineno}"
|
||||
%if old_line_anchor:
|
||||
id="${old_line_anchor}"
|
||||
%endif
|
||||
>
|
||||
%if line.original.lineno:
|
||||
<a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
|
||||
%endif
|
||||
</td>
|
||||
<td class="cb-content ${action_class(line.original.action)}"
|
||||
data-line-number="o${line.original.lineno}"
|
||||
><span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
|
||||
</td>
|
||||
<td class="cb-lineno ${action_class(line.modified.action)}"
|
||||
data-line-number="${line.modified.lineno}"
|
||||
%if new_line_anchor:
|
||||
id="${new_line_anchor}"
|
||||
%endif
|
||||
>
|
||||
%if line.modified.lineno:
|
||||
<a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
|
||||
%endif
|
||||
</td>
|
||||
<td class="cb-content ${action_class(line.modified.action)}"
|
||||
data-line-number="n${line.modified.lineno}"
|
||||
>
|
||||
<span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
|
||||
</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</%def>
|
||||
|
||||
|
||||
<%def name="render_hunk_lines_unified(hunk)">
|
||||
%for old_line_no, new_line_no, action, content in hunk.unified:
|
||||
<%
|
||||
old_line_anchor, new_line_anchor = None, None
|
||||
if old_line_no:
|
||||
old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
|
||||
if new_line_no:
|
||||
new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
|
||||
%>
|
||||
<tr class="cb-line">
|
||||
<td class="cb-lineno ${action_class(action)}"
|
||||
data-line-number="${old_line_no}"
|
||||
%if old_line_anchor:
|
||||
id="${old_line_anchor}"
|
||||
%endif
|
||||
>
|
||||
%if old_line_anchor:
|
||||
<a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
|
||||
%endif
|
||||
</td>
|
||||
<td class="cb-lineno ${action_class(action)}"
|
||||
data-line-number="${new_line_no}"
|
||||
%if new_line_anchor:
|
||||
id="${new_line_anchor}"
|
||||
%endif
|
||||
>
|
||||
%if new_line_anchor:
|
||||
<a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
|
||||
%endif
|
||||
</td>
|
||||
<td class="cb-content ${action_class(action)}"
|
||||
data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
|
||||
><span class="cb-code">${action} ${content or '' | n}</span>
|
||||
</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</%def>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/base/base.html"/>
|
||||
<%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
|
||||
|
||||
<%def name="title()">
|
||||
%if c.compare_home:
|
||||
|
|
@ -53,7 +54,7 @@
|
|||
<a id="btn-swap" class="btn btn-primary" href="${c.swap_url}"><i class="icon-refresh"></i> ${_('Swap')}</a>
|
||||
%endif
|
||||
<div id="compare_revs" class="btn btn-primary"><i class ="icon-loop"></i> ${_('Compare Commits')}</div>
|
||||
%if c.files:
|
||||
%if c.diffset and c.diffset.files:
|
||||
<div id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</div>
|
||||
%endif
|
||||
</div>
|
||||
|
|
@ -248,72 +249,7 @@
|
|||
<div id="changeset_compare_view_content">
|
||||
##CS
|
||||
<%include file="compare_commits.html"/>
|
||||
|
||||
## FILES
|
||||
<div class="cs_files_title">
|
||||
<span class="cs_files_expand">
|
||||
<span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
|
||||
</span>
|
||||
<h2>
|
||||
${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="cs_files">
|
||||
%if not c.files:
|
||||
<p class="empty_data">${_('No files')}</p>
|
||||
%endif
|
||||
<table class="compare_view_files">
|
||||
<%namespace name="diff_block" file="/changeset/diff_block.html"/>
|
||||
%for FID, change, path, stats, file in c.files:
|
||||
<tr class="cs_${change} collapse_file" fid="${FID}">
|
||||
<td class="cs_icon_td">
|
||||
<span class="collapse_file_icon" fid="${FID}"></span>
|
||||
</td>
|
||||
<td class="cs_icon_td">
|
||||
<div class="flag_status not_reviewed hidden"></div>
|
||||
</td>
|
||||
<td class="cs_${change}" id="a_${FID}">
|
||||
<div class="node">
|
||||
<a href="#a_${FID}">
|
||||
<i class="icon-file-${change.lower()}"></i>
|
||||
${h.safe_unicode(path)}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
|
||||
<div class="comment-bubble pull-right" data-path="${path}">
|
||||
<i class="icon-comment"></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr fid="${FID}" id="diff_${FID}" class="diff_links">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="cs_${change}">
|
||||
%if c.target_repo.repo_name == c.repo_name:
|
||||
${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.source_ref, c.target_ref, change, file)}
|
||||
%else:
|
||||
## this is slightly different case later, since the target repo can have this
|
||||
## file in target state than the source repo
|
||||
${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.source_ref, c.target_ref, change, file)}
|
||||
%endif
|
||||
</td>
|
||||
<td class="td-actions rc-form">
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="tr_${FID}">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="injected_diff" colspan="2">
|
||||
${diff_block.diff_block_simple([c.changes[FID]])}
|
||||
</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</table>
|
||||
% if c.limited_diff:
|
||||
${diff_block.changeset_message()}
|
||||
% endif
|
||||
${cbdiffs.render_diffset(c.diffset)}
|
||||
</div>
|
||||
%endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ class TestChangesetController(object):
|
|||
response.mustcontain('Added docstrings to vcs.cli') # commit msg
|
||||
response.mustcontain('Changed theme to ADC theme') # commit msg
|
||||
|
||||
self._check_diff_menus(response)
|
||||
self._check_new_diff_menus(response)
|
||||
|
||||
def test_changeset_range(self, backend):
|
||||
self._check_changeset_range(
|
||||
|
|
@ -273,7 +273,7 @@ Added a symlink
|
|||
""" + diffs['svn'],
|
||||
}
|
||||
|
||||
def _check_diff_menus(self, response, right_menu=False):
|
||||
def _check_diff_menus(self, response, right_menu=False,):
|
||||
# diff menus
|
||||
for elem in ['Show File', 'Unified Diff', 'Side-by-side Diff',
|
||||
'Raw Diff', 'Download Diff']:
|
||||
|
|
@ -284,3 +284,16 @@ Added a symlink
|
|||
for elem in ['Ignore whitespace', 'Increase context',
|
||||
'Hide comments']:
|
||||
response.mustcontain(elem)
|
||||
|
||||
|
||||
def _check_new_diff_menus(self, response, right_menu=False,):
|
||||
# diff menus
|
||||
for elem in ['Show file before', 'Show file after',
|
||||
'Raw diff', 'Download diff']:
|
||||
response.mustcontain(elem)
|
||||
|
||||
# right pane diff menus
|
||||
if right_menu:
|
||||
for elem in ['Ignore whitespace', 'Increase context',
|
||||
'Hide comments']:
|
||||
response.mustcontain(elem)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
import mock
|
||||
import pytest
|
||||
import lxml.html
|
||||
|
||||
from rhodecode.lib.vcs.backends.base import EmptyCommit
|
||||
from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
|
||||
|
|
@ -609,9 +610,12 @@ class ComparePage(AssertResponse):
|
|||
"""
|
||||
|
||||
def contains_file_links_and_anchors(self, files):
|
||||
doc = lxml.html.fromstring(self.response.body)
|
||||
for filename, file_id in files:
|
||||
self.contains_one_link(filename, '#' + file_id)
|
||||
self.contains_one_anchor(file_id)
|
||||
diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
|
||||
assert len(diffblock) == 1
|
||||
assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1
|
||||
|
||||
def contains_change_summary(self, files_changed, inserted, deleted):
|
||||
template = (
|
||||
|
|
|
|||
|
|
@ -264,19 +264,19 @@ class TestRenderTokenStream(object):
|
|||
),
|
||||
(
|
||||
[('A', '', u'two\n'), ('A', '', u'lines')],
|
||||
'<span class="A">two<nl>\n</nl>lines</span>',
|
||||
'<span class="A">two\nlines</span>',
|
||||
),
|
||||
(
|
||||
[('A', '', u'\nthree\n'), ('A', '', u'lines')],
|
||||
'<span class="A"><nl>\n</nl>three<nl>\n</nl>lines</span>',
|
||||
'<span class="A">\nthree\nlines</span>',
|
||||
),
|
||||
(
|
||||
[('', '', u'\n'), ('A', '', u'line')],
|
||||
'<span><nl>\n</nl></span><span class="A">line</span>',
|
||||
'<span>\n</span><span class="A">line</span>',
|
||||
),
|
||||
(
|
||||
[('', 'ins', u'\n'), ('A', '', u'line')],
|
||||
'<span><ins><nl>\n</nl></ins></span><span class="A">line</span>',
|
||||
'<span><ins>\n</ins></span><span class="A">line</span>',
|
||||
),
|
||||
(
|
||||
[('A', '', u'hel'), ('A', 'ins', u'lo')],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue