예제 #1
0
def diff_blocks(fromlines,
                tolines,
                context=None,
                tabwidth=8,
                ignore_blank_lines=0,
                ignore_case=0,
                ignore_space_changes=0):
    """Return an array that is adequate for adding to the data dictionary

    See `get_filtered_hunks` for the parameter descriptions.

    See also the diff_div.html template.
    """

    type_map = {
        'replace': 'mod',
        'delete': 'rem',
        'insert': 'add',
        'equal': 'unmod'
    }

    space_re = re.compile(' ( +)|^ ')

    def htmlify(match):
        div, mod = divmod(len(match.group(0)), 2)
        return div * '  ' + mod * ' '

    def markup_intraline_changes(opcodes):
        for tag, i1, i2, j1, j2 in opcodes:
            if tag == 'replace' and i2 - i1 == j2 - j1:
                for i in range(i2 - i1):
                    fromline, toline = fromlines[i1 + i], tolines[j1 + i]
                    (start, end) = get_change_extent(fromline, toline)
                    if start != 0 or end != 0:
                        last = end + len(fromline)
                        fromlines[i1 + i] = (fromline[:start] + '\0' +
                                             fromline[start:last] + '\1' +
                                             fromline[last:])
                        last = end + len(toline)
                        tolines[j1 + i] = (toline[:start] + '\0' +
                                           toline[start:last] + '\1' +
                                           toline[last:])
            yield tag, i1, i2, j1, j2

    changes = []
    for group in get_filtered_hunks(fromlines, tolines, context,
                                    ignore_blank_lines, ignore_case,
                                    ignore_space_changes):
        blocks = []
        last_tag = None
        for tag, i1, i2, j1, j2 in markup_intraline_changes(group):
            if tag != last_tag:
                blocks.append({
                    'type': type_map[tag],
                    'base': {
                        'offset': i1,
                        'lines': []
                    },
                    'changed': {
                        'offset': j1,
                        'lines': []
                    }
                })
            if tag == 'equal':
                for line in fromlines[i1:i2]:
                    line = line.expandtabs(tabwidth)
                    line = space_re.sub(htmlify, escape(line, quotes=False))
                    blocks[-1]['base']['lines'].append(Markup(unicode(line)))
                for line in tolines[j1:j2]:
                    line = line.expandtabs(tabwidth)
                    line = space_re.sub(htmlify, escape(line, quotes=False))
                    blocks[-1]['changed']['lines'].append(Markup(
                        unicode(line)))
            else:
                if tag in ('replace', 'delete'):
                    for line in fromlines[i1:i2]:
                        line = expandtabs(line, tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<del>'.join([
                            space_re.sub(htmlify, seg)
                            for seg in line.split('\0')
                        ])
                        line = line.replace('\1', '</del>')
                        blocks[-1]['base']['lines'].append(
                            Markup(unicode(line)))
                if tag in ('replace', 'insert'):
                    for line in tolines[j1:j2]:
                        line = expandtabs(line, tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<ins>'.join([
                            space_re.sub(htmlify, seg)
                            for seg in line.split('\0')
                        ])
                        line = line.replace('\1', '</ins>')
                        blocks[-1]['changed']['lines'].append(
                            Markup(unicode(line)))
        changes.append(blocks)
    return changes
예제 #2
0
    def _diff_to_hdf(self, difflines, tabwidth):
        """
        Translate a diff file into something suitable for inclusion in HDF.
        The result is [(filename, revname_old, revname_new, changes)],
        where changes has the same format as the result of
        `trac.versioncontrol.diff.hdf_diff`.

        If the diff cannot be parsed, this method returns None.
        """
        def _markup_intraline_change(fromlines, tolines):
            from trac.versioncontrol.diff import get_change_extent
            for i in xrange(len(fromlines)):
                fr, to = fromlines[i], tolines[i]
                (start, end) = get_change_extent(fr, to)
                if start != 0 or end != 0:
                    last = end + len(fr)
                    fromlines[i] = fr[:start] + '\0' + fr[start:last] + \
                                   '\1' + fr[last:]
                    last = end + len(to)
                    tolines[i] = to[:start] + '\0' + to[start:last] + \
                                 '\1' + to[last:]

        space_re = re.compile(' ( +)|^ ')

        def htmlify(match):
            div, mod = divmod(len(match.group(0)), 2)
            return Markup(div * '&nbsp; ' + mod * '&nbsp;')

        comments = []
        changes = []
        lines = iter(difflines)
        try:
            line = next(lines)
            while True:
                oldpath = oldrev = newpath = newrev = ''
                oldinfo = newinfo = []
                binary = False

                # consume preamble, storing free lines in comments
                # (also detect the special case of git binary patches)
                if not line.startswith('--- '):
                    if not line.startswith('Index: ') and line != '=' * 67:
                        comments.append(line)
                    if line == "GIT binary patch":
                        binary = True
                        diffcmd_line = comments[0]  # diff --git a/... b/,,,
                        oldpath, newpath = diffcmd_line.split()[-2:]
                        if any(c.startswith('new file') for c in comments):
                            oldpath = '/dev/null'
                        if any(c.startswith('deleted file') for c in comments):
                            newpath = '/dev/null'
                        oldinfo = ['', oldpath]
                        newinfo = ['', newpath]
                        index = [c for c in comments if c.startswith('index ')]
                        if index:  # index 8f****78..1e****5c
                            oldrev, newrev = index[0].split()[-1].split('..')
                            oldinfo.append(oldrev)
                            newinfo.append(newrev)
                        line = next(lines)
                        while line:
                            comments.append(line)
                            line = next(lines)
                    else:
                        line = next(lines)
                        continue

                if not oldinfo and not newinfo:
                    # Base filename/version from '--- <file> [rev]'
                    oldinfo = line.split(None, 2)
                    if len(oldinfo) > 1:
                        oldpath = oldinfo[1]
                        if len(oldinfo) > 2:
                            oldrev = oldinfo[2]

                    # Changed filename/version from '+++ <file> [rev]'
                    line = next(lines)
                    if not line.startswith('+++ '):
                        self.log.debug('expected +++ after ---, got %s', line)
                        return None

                    newinfo = line.split(None, 2)
                    if len(newinfo) > 1:
                        newpath = newinfo[1]
                        if len(newinfo) > 2:
                            newrev = newinfo[2]

                shortrev = ('old', 'new')
                if oldpath or newpath:
                    sep = re.compile(r'([/.~\\])')
                    commonprefix = ''.join(
                        os.path.commonprefix(
                            [sep.split(newpath),
                             sep.split(oldpath)]))
                    commonsuffix = ''.join(
                        os.path.commonprefix([
                            sep.split(newpath)[::-1],
                            sep.split(oldpath)[::-1]
                        ])[::-1])
                    if len(commonprefix) > len(commonsuffix):
                        common = commonprefix
                    elif commonsuffix:
                        common = commonsuffix.lstrip('/')
                        a = oldpath[:-len(commonsuffix)]
                        b = newpath[:-len(commonsuffix)]
                        if len(a) < 4 and len(b) < 4:
                            shortrev = (a, b)
                    elif oldpath == '/dev/null':
                        common = _("new file %(new)s",
                                   new=newpath.lstrip('b/'))
                        shortrev = ('-', '+')
                    elif newpath == '/dev/null':
                        common = _("deleted file %(deleted)s",
                                   deleted=oldpath.lstrip('a/'))
                        shortrev = ('+', '-')
                    else:
                        common = '(a) %s vs. (b) %s' % (oldpath, newpath)
                        shortrev = ('a', 'b')
                else:
                    common = ''

                groups = []
                groups_title = []
                changes.append({
                    'change': 'edit',
                    'props': [],
                    'comments': '\n'.join(comments),
                    'binary': binary,
                    'diffs': groups,
                    'diffs_title': groups_title,
                    'old': {
                        'path': common,
                        'rev': ' '.join(oldinfo[1:]),
                        'shortrev': shortrev[0]
                    },
                    'new': {
                        'path': common,
                        'rev': ' '.join(newinfo[1:]),
                        'shortrev': shortrev[1]
                    }
                })
                comments = []
                line = next(lines)
                while line:
                    # "@@ -333,10 +329,8 @@" or "@@ -1 +1 @@ [... title ...]"
                    r = re.match(
                        r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@'
                        '(.*)', line)
                    if not r:
                        break
                    blocks = []
                    groups.append(blocks)
                    fromline, fromend, toline, toend = \
                            [int(x or 1) for x in r.groups()[:4]]
                    groups_title.append(r.group(5))
                    last_type = extra = None

                    fromend += fromline
                    toend += toline
                    line = next(lines)
                    while fromline < fromend or toline < toend or extra:

                        # First character is the command
                        command = ' '
                        if line:
                            command, line = line[0], line[1:]
                        # Make a new block?
                        if (command == ' ') != last_type:
                            last_type = command == ' '
                            kind = 'unmod' if last_type else 'mod'
                            block = {
                                'type': kind,
                                'base': {
                                    'offset': fromline - 1,
                                    'lines': []
                                },
                                'changed': {
                                    'offset': toline - 1,
                                    'lines': []
                                }
                            }
                            blocks.append(block)
                        else:
                            block = blocks[-1]
                        if command == ' ':
                            sides = ['base', 'changed']
                        elif command == '+':
                            last_side = 'changed'
                            sides = [last_side]
                        elif command == '-':
                            last_side = 'base'
                            sides = [last_side]
                        elif command == '\\' and last_side:
                            meta = block[last_side].setdefault('meta', {})
                            meta[len(block[last_side]['lines'])] = True
                            sides = [last_side]
                        elif command == '@':  # ill-formed patch
                            groups_title[-1] = "%s (%s)" % (
                                groups_title[-1],
                                _("this hunk was shorter than expected"))
                            line = '@' + line
                            break
                        else:
                            self.log.debug('expected +, - or \\, got %s',
                                           command)
                            return None
                        for side in sides:
                            if side == 'base':
                                fromline += 1
                            else:
                                toline += 1
                            block[side]['lines'].append(line)
                        line = next(lines)
                        extra = line and line[0] == '\\'
        except StopIteration:
            pass

        # Go through all groups/blocks and mark up intraline changes, and
        # convert to html
        for o in changes:
            for group in o['diffs']:
                for b in group:
                    base, changed = b['base'], b['changed']
                    f, t = base['lines'], changed['lines']
                    if b['type'] == 'mod':
                        if len(f) == 0:
                            b['type'] = 'add'
                        elif len(t) == 0:
                            b['type'] = 'rem'
                        elif len(f) == len(t):
                            _markup_intraline_change(f, t)
                    for i in xrange(len(f)):
                        line = expandtabs(f[i], tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<del>'.join(
                            space_re.sub(htmlify, seg)
                            for seg in line.split('\0'))
                        line = line.replace('\1', '</del>')
                        f[i] = Markup(line)
                        if 'meta' in base and i in base['meta']:
                            f[i] = Markup('<em>%s</em>') % f[i]
                    for i in xrange(len(t)):
                        line = expandtabs(t[i], tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<ins>'.join(
                            space_re.sub(htmlify, seg)
                            for seg in line.split('\0'))
                        line = line.replace('\1', '</ins>')
                        t[i] = Markup(line)
                        if 'meta' in changed and i in changed['meta']:
                            t[i] = Markup('<em>%s</em>') % t[i]
        return changes
예제 #3
0
파일: patch.py 프로젝트: dinhkhanh/trac
    def _diff_to_hdf(self, difflines, tabwidth):
        """
        Translate a diff file into something suitable for inclusion in HDF.
        The result is [(filename, revname_old, revname_new, changes)],
        where changes has the same format as the result of
        `trac.versioncontrol.diff.hdf_diff`.

        If the diff cannot be parsed, this method returns None.
        """
        def _markup_intraline_change(fromlines, tolines):
            from trac.versioncontrol.diff import get_change_extent
            for i in xrange(len(fromlines)):
                fr, to = fromlines[i], tolines[i]
                (start, end) = get_change_extent(fr, to)
                if start != 0 or end != 0:
                    last = end+len(fr)
                    fromlines[i] = fr[:start] + '\0' + fr[start:last] + \
                                   '\1' + fr[last:]
                    last = end+len(to)
                    tolines[i] = to[:start] + '\0' + to[start:last] + \
                                 '\1' + to[last:]

        import re
        space_re = re.compile(' ( +)|^ ')
        def htmlify(match):
            div, mod = divmod(len(match.group(0)), 2)
            return div * '&nbsp; ' + mod * '&nbsp;'

        comments = []
        changes = []
        lines = iter(difflines)
        try:
            line = lines.next()
            while True:
                oldpath = oldrev = newpath = newrev = ''
                oldinfo = newinfo = []
                binary = False

                # consume preample, storing free lines in comments
                # (also detect the special case of git binary patches)
                if not line.startswith('--- '):
                    if not line.startswith('Index: ') and line != '=' * 67:
                        comments.append(line)
                    if line == "GIT binary patch":
                        binary = True
                        diffcmd_line = comments[0] # diff --git a/... b/,,,
                        oldpath, newpath = diffcmd_line.split()[-2:]
                        if any(c.startswith('new file') for c in comments):
                            oldpath = '/dev/null'
                        if any(c.startswith('deleted file') for c in comments):
                            newpath = '/dev/null'
                        oldinfo = ['', oldpath]
                        newinfo = ['', newpath]
                        index = [c for c in comments if c.startswith('index ')]
                        if index: # index 8f****78..1e****5c
                            oldrev, newrev = index[0].split()[-1].split('..')
                            oldinfo.append(oldrev)
                            newinfo.append(newrev)
                        line = lines.next()
                        while line:
                            comments.append(line)
                            line = lines.next()
                    else:
                        line = lines.next()
                        continue

                if not oldinfo and not newinfo:
                    # Base filename/version from '--- <file> [rev]'
                    oldinfo = line.split(None, 2)
                    if len(oldinfo) > 1:
                        oldpath = oldinfo[1]
                        if len(oldinfo) > 2:
                            oldrev = oldinfo[2]

                    # Changed filename/version from '+++ <file> [rev]'
                    line = lines.next()
                    if not line.startswith('+++ '):
                        self.log.debug('expected +++ after ---, got ' + line)
                        return None

                    newinfo = line.split(None, 2)
                    if len(newinfo) > 1:
                        newpath = newinfo[1]
                        if len(newinfo) > 2:
                            newrev = newinfo[2]

                shortrev = ('old', 'new')
                if oldpath or newpath:
                    sep = re.compile(r'([/.~\\])')
                    commonprefix = ''.join(os.path.commonprefix(
                        [sep.split(newpath), sep.split(oldpath)]))
                    commonsuffix = ''.join(os.path.commonprefix(
                        [sep.split(newpath)[::-1],
                         sep.split(oldpath)[::-1]])[::-1])
                    if len(commonprefix) > len(commonsuffix):
                        common = commonprefix
                    elif commonsuffix:
                        common = commonsuffix.lstrip('/')
                        a = oldpath[:-len(commonsuffix)]
                        b = newpath[:-len(commonsuffix)]
                        if len(a) < 4 and len(b) < 4:
                            shortrev = (a, b)
                    elif oldpath == '/dev/null':
                        common = _("new file %(new)s",
                                   new=newpath.lstrip('b/'))
                        shortrev = ('-', '+')
                    elif newpath == '/dev/null':
                        common = _("deleted file %(deleted)s", 
                                   deleted=oldpath.lstrip('a/'))
                        shortrev = ('+', '-')
                    else:
                        common = '(a) %s vs. (b) %s' % (oldpath, newpath)
                        shortrev = ('a', 'b')
                else:
                    common = ''

                groups = []
                groups_title = []
                changes.append({'change': 'edit', 'props': [],
                                'comments': '\n'.join(comments),
                                'binary': binary,
                                'diffs': groups, 
                                'diffs_title': groups_title,
                                'old': {'path': common,
                                        'rev': ' '.join(oldinfo[1:]),
                                        'shortrev': shortrev[0]},
                                'new': {'path': common,
                                        'rev': ' '.join(newinfo[1:]),
                                        'shortrev': shortrev[1]}})
                comments = []
                line = lines.next()
                while line:
                    # "@@ -333,10 +329,8 @@" or "@@ -1 +1 @@ [... title ...]"
                    r = re.match(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@'
                                  '(.*)', line)
                    if not r:
                        break
                    blocks = []
                    groups.append(blocks)
                    fromline, fromend, toline, toend = \
                            [int(x or 1) for x in r.groups()[:4]]
                    groups_title.append(r.group(5))
                    last_type = extra = None

                    fromend += fromline
                    toend += toline
                    line = lines.next()
                    while fromline < fromend or toline < toend or extra:

                        # First character is the command
                        command = ' '
                        if line:
                            command, line = line[0], line[1:]
                        # Make a new block?
                        if (command == ' ') != last_type:
                            last_type = command == ' '
                            kind = 'unmod' if last_type else 'mod'
                            block = {'type': kind,
                                     'base': {'offset': fromline - 1,
                                              'lines': []},
                                     'changed': {'offset': toline - 1,
                                                 'lines': []}}
                            blocks.append(block)
                        else:
                            block = blocks[-1]
                        if command == ' ':
                            sides = ['base', 'changed']
                        elif command == '+':
                            last_side = 'changed'
                            sides = [last_side]
                        elif command == '-':
                            last_side = 'base'
                            sides = [last_side]
                        elif command == '\\' and last_side:
                            meta = block[last_side].setdefault('meta', {})
                            meta[len(block[last_side]['lines'])] = True
                            sides = [last_side]
                        elif command == '@': # ill-formed patch
                            groups_title[-1] = "%s (%s)" % (
                                groups_title[-1],
                                _("this hunk was shorter than expected"))
                            line = '@'+line
                            break
                        else:
                            self.log.debug('expected +, - or \\, got '+command)
                            return None
                        for side in sides:
                            if side == 'base':
                                fromline += 1
                            else:
                                toline += 1
                            block[side]['lines'].append(line)
                        line = lines.next()
                        extra = line and line[0] == '\\'
        except StopIteration:
            pass

        # Go through all groups/blocks and mark up intraline changes, and
        # convert to html
        for o in changes:
            for group in o['diffs']:
                for b in group:
                    base, changed = b['base'], b['changed']
                    f, t = base['lines'], changed['lines']
                    if b['type'] == 'mod':
                        if len(f) == 0:
                            b['type'] = 'add'
                        elif len(t) == 0:
                            b['type'] = 'rem'
                        elif len(f) == len(t):
                            _markup_intraline_change(f, t)
                    for i in xrange(len(f)):
                        line = expandtabs(f[i], tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<del>'.join([space_re.sub(htmlify, seg)
                                             for seg in line.split('\0')])
                        line = line.replace('\1', '</del>')
                        f[i] = Markup(line)
                        if 'meta' in base and i in base['meta']:
                            f[i] = Markup('<em>%s</em>') % f[i]
                    for i in xrange(len(t)):
                        line = expandtabs(t[i], tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<ins>'.join([space_re.sub(htmlify, seg)
                                             for seg in line.split('\0')])
                        line = line.replace('\1', '</ins>')
                        t[i] = Markup(line)
                        if 'meta' in changed and i in changed['meta']:
                            t[i] = Markup('<em>%s</em>') % t[i]
        return changes
예제 #4
0
파일: text.py 프로젝트: skshel123/trac
 def test_tabstops(self):
     self.assertEqual('        ', expandtabs('       \t'))
     self.assertEqual('                ', expandtabs('\t\t'))
예제 #5
0
파일: text.py 프로젝트: skshel123/trac
 def test_empty(self):
     x = expandtabs('', ignoring='\0')
     self.assertEqual('', x)
예제 #6
0
파일: diff.py 프로젝트: trac-ja/trac-ja
def diff_blocks(fromlines, tolines, context=None, tabwidth=8,
                ignore_blank_lines=0, ignore_case=0, ignore_space_changes=0):
    """Return an array that is adequate for adding to the data dictionary

    See `get_filtered_hunks` for the parameter descriptions.

    See also the diff_div.html template.
    """

    type_map = {'replace': 'mod', 'delete': 'rem', 'insert': 'add',
                'equal': 'unmod'}

    space_re = re.compile(' ( +)|^ ')
    def htmlify(match):
        div, mod = divmod(len(match.group(0)), 2)
        return div * '&nbsp; ' + mod * '&nbsp;'

    def markup_intraline_changes(opcodes):
        for tag, i1, i2, j1, j2 in opcodes:
            if tag == 'replace' and i2 - i1 == j2 - j1:
                for i in range(i2 - i1):
                    fromline, toline = fromlines[i1 + i], tolines[j1 + i]
                    (start, end) = get_change_extent(fromline, toline)
                    if start != 0 or end != 0:
                        last = end + len(fromline)
                        fromlines[i1 + i] = (
                            fromline[:start] + '\0' + fromline[start:last] + 
                            '\1' + fromline[last:])
                        last = end+len(toline)
                        tolines[j1 + i] = (
                            toline[:start] + '\0' + toline[start:last] +
                            '\1' + toline[last:])
            yield tag, i1, i2, j1, j2

    changes = []
    for group in get_filtered_hunks(fromlines, tolines, context, 
                                    ignore_blank_lines, ignore_case,
                                    ignore_space_changes):
        blocks = []
        last_tag = None
        for tag, i1, i2, j1, j2 in markup_intraline_changes(group):
            if tag != last_tag:
                blocks.append({'type': type_map[tag],
                               'base': {'offset': i1, 'lines': []},
                               'changed': {'offset': j1, 'lines': []}})
            if tag == 'equal':
                for line in fromlines[i1:i2]:
                    line = line.expandtabs(tabwidth)
                    line = space_re.sub(htmlify, escape(line, quotes=False))
                    blocks[-1]['base']['lines'].append(Markup(unicode(line)))
                for line in tolines[j1:j2]:
                    line = line.expandtabs(tabwidth)
                    line = space_re.sub(htmlify, escape(line, quotes=False))
                    blocks[-1]['changed']['lines'].append(Markup(unicode(line)))
            else:
                if tag in ('replace', 'delete'):
                    for line in fromlines[i1:i2]:
                        line = expandtabs(line, tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<del>'.join([space_re.sub(htmlify, seg)
                                             for seg in line.split('\0')])
                        line = line.replace('\1', '</del>')
                        blocks[-1]['base']['lines'].append(
                            Markup(unicode(line)))
                if tag in ('replace', 'insert'):
                    for line in tolines[j1:j2]:
                        line = expandtabs(line, tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<ins>'.join([space_re.sub(htmlify, seg)
                                             for seg in line.split('\0')])
                        line = line.replace('\1', '</ins>')
                        blocks[-1]['changed']['lines'].append(
                            Markup(unicode(line)))
        changes.append(blocks)
    return changes
예제 #7
0
파일: text.py 프로젝트: skshel123/trac
 def test_ingoring(self):
     x = expandtabs('\0\t', ignoring='\0')
     self.assertEqual('\0        ', x)
예제 #8
0
 def test_ingoring(self):
     x = expandtabs("\0\t", ignoring="\0")
     self.assertEquals("\0        ", x)
예제 #9
0
 def test_tabstops(self):
     self.assertEquals("        ", expandtabs("       \t"))
     self.assertEquals("                ", expandtabs("\t\t"))
예제 #10
0
 def test_empty(self):
     x = expandtabs("", ignoring="\0")
     self.assertEquals("", x)
예제 #11
0
파일: text.py 프로젝트: thimalk/bloodhound
 def test_tabstops(self):
     self.assertEquals('        ', expandtabs('       \t'))
     self.assertEquals('                ', expandtabs('\t\t'))
예제 #12
0
파일: text.py 프로젝트: thimalk/bloodhound
 def test_ingoring(self):
     x = expandtabs('\0\t', ignoring='\0')
     self.assertEquals('\0        ', x)
예제 #13
0
파일: text.py 프로젝트: thimalk/bloodhound
 def test_empty(self):
     x = expandtabs('', ignoring='\0')
     self.assertEquals('', x)
예제 #14
0
    def _diff_to_hdf(self, difflines, tabwidth):
        """
        Translate a diff file into something suitable for inclusion in HDF.
        The result is [(filename, revname_old, revname_new, changes)],
        where changes has the same format as the result of
        `trac.versioncontrol.diff.hdf_diff`.

        If the diff cannot be parsed, this method returns None.
        """
        def _markup_intraline_change(fromlines, tolines):
            from trac.versioncontrol.diff import _get_change_extent
            for i in xrange(len(fromlines)):
                fr, to = fromlines[i], tolines[i]
                (start, end) = _get_change_extent(fr, to)
                if start != 0 or end != 0:
                    last = end+len(fr)
                    fromlines[i] = fr[:start] + '\0' + fr[start:last] + \
                                   '\1' + fr[last:]
                    last = end+len(to)
                    tolines[i] = to[:start] + '\0' + to[start:last] + \
                                 '\1' + to[last:]

        import re
        space_re = re.compile(' ( +)|^ ')
        def htmlify(match):
            div, mod = divmod(len(match.group(0)), 2)
            return div * '&nbsp; ' + mod * '&nbsp;'

        comments = []
        changes = []
        lines = iter(difflines)
        try:
            line = lines.next()
            while True:
                if not line.startswith('--- '):
                    if not line.startswith('Index: ') and line != '='*67:
                        comments.append(line)
                    line = lines.next()
                    continue

                oldpath = oldrev = newpath = newrev = ''

                # Base filename/version
                oldinfo = line.split(None, 2)
                if len(oldinfo) > 1:
                    oldpath = oldinfo[1]
                    if len(oldinfo) > 2:
                        oldrev = oldinfo[2]

                # Changed filename/version
                line = lines.next()
                if not line.startswith('+++ '):
                    self.log.debug('expected +++ after ---, got '+line)
                    return None

                newinfo = line.split(None, 2)
                if len(newinfo) > 1:
                    newpath = newinfo[1]
                    if len(newinfo) > 2:
                        newrev = newinfo[2]

                shortrev = ('old', 'new')
                if oldpath or newpath:
                    sep = re.compile(r'([/.~\\])')
                    commonprefix = ''.join(os.path.commonprefix(
                        [sep.split(newpath), sep.split(oldpath)]))
                    commonsuffix = ''.join(os.path.commonprefix(
                        [sep.split(newpath)[::-1],
                         sep.split(oldpath)[::-1]])[::-1])
                    if len(commonprefix) > len(commonsuffix):
                        common = commonprefix
                    elif commonsuffix:
                        common = commonsuffix.lstrip('/')
                        a = oldpath[:-len(commonsuffix)]
                        b = newpath[:-len(commonsuffix)]
                        if len(a) < 4 and len(b) < 4:
                            shortrev = (a, b)
                    else:
                        common = '(a) %s vs. (b) %s' % (oldpath, newpath)
                        shortrev = ('a', 'b')
                else:
                    common = ''

                groups = []
                changes.append({'change': 'edit', 'props': [],
                                'comments': '\n'.join(comments),
                                'diffs': groups,
                                'old': {'path': common,
                                        'rev': ' '.join(oldinfo[1:]),
                                        'shortrev': shortrev[0]},
                                'new': {'path': common,
                                        'rev': ' '.join(newinfo[1:]),
                                        'shortrev': shortrev[1]}})
                comments = []
                line = lines.next()
                while line:
                    # "@@ -333,10 +329,8 @@" or "@@ -1 +1 @@"
                    r = re.match(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@',
                                 line)
                    if not r:
                        break
                    blocks = []
                    groups.append(blocks)
                    fromline, fromend, toline, toend = [int(x or 1)
                                                        for x in r.groups()]
                    last_type = last_change = extra = None

                    fromend += fromline
                    toend += toline
                    line = lines.next()
                    while fromline < fromend or toline < toend or extra:

                        # First character is the command
                        command = ' '
                        if line:
                            command, line = line[0], line[1:]
                        # Make a new block?
                        if (command == ' ') != last_type:
                            last_type = command == ' '
                            kind = last_type and 'unmod' or 'mod'
                            block = {'type': kind,
                                     'base': {'offset': fromline - 1,
                                              'lines': []},
                                     'changed': {'offset': toline - 1,
                                                 'lines': []}}
                            blocks.append(block)
                        else:
                            block = blocks[-1]
                        if command == ' ':
                            sides = ['base', 'changed']
                        elif command == '+':
                            last_side = 'changed'
                            sides = [last_side]
                        elif command == '-':
                            last_side = 'base'
                            sides = [last_side]
                        elif command == '\\' and last_side:
                            meta = block[last_side].setdefault('meta', {})
                            meta[len(block[last_side]['lines'])] = True
                            sides = [last_side]
                        else:
                            self.log.debug('expected +, - or \\, got '+command)
                            return None
                        for side in sides:
                            if side == 'base':
                                fromline += 1
                            else:
                                toline += 1
                            block[side]['lines'].append(line)
                        line = lines.next()
                        extra = line and line[0] == '\\'
        except StopIteration:
            pass

        # Go through all groups/blocks and mark up intraline changes, and
        # convert to html
        for o in changes:
            for group in o['diffs']:
                for b in group:
                    base, changed = b['base'], b['changed']
                    f, t = base['lines'], changed['lines']
                    if b['type'] == 'mod':
                        if len(f) == 0:
                            b['type'] = 'add'
                        elif len(t) == 0:
                            b['type'] = 'rem'
                        elif len(f) == len(t):
                            _markup_intraline_change(f, t)
                    for i in xrange(len(f)):
                        line = expandtabs(f[i], tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<del>'.join([space_re.sub(htmlify, seg)
                                             for seg in line.split('\0')])
                        line = line.replace('\1', '</del>')
                        f[i] = Markup(line)
                        if 'meta' in base and i in base['meta']:
                            f[i] = Markup('<em>%s</em>') % f[i]
                    for i in xrange(len(t)):
                        line = expandtabs(t[i], tabwidth, '\0\1')
                        line = escape(line, quotes=False)
                        line = '<ins>'.join([space_re.sub(htmlify, seg)
                                             for seg in line.split('\0')])
                        line = line.replace('\1', '</ins>')
                        t[i] = Markup(line)
                        if 'meta' in changed and i in changed['meta']:
                            t[i] = Markup('<em>%s</em>') % t[i]
        return changes