Index: trac/versioncontrol/svn_fs.py
===================================================================
--- trac/versioncontrol/svn_fs.py	(revision 8199)
+++ trac/versioncontrol/svn_fs.py	(working copy)
@@ -60,7 +60,8 @@
 from trac.versioncontrol.cache import CachedRepository
 from trac.versioncontrol.svn_authz import SubversionAuthorizer
 from trac.versioncontrol.web_ui.browser import IPropertyRenderer
-from trac.util import embedded_numbers
+from trac.versioncontrol.web_ui.changeset import IPropertyDiffRenderer
+from trac.util import Ranges, embedded_numbers, to_ranges
 from trac.util.text import exception_to_unicode, to_unicode
 from trac.util.translation import _
 from trac.util.datefmt import utc
@@ -308,7 +309,7 @@
 
 
 class SubversionPropertyRenderer(Component):
-    implements(IPropertyRenderer)
+    implements(IPropertyRenderer, IPropertyDiffRenderer)
 
     def __init__(self):
         self._externals_map = {}
@@ -323,7 +324,7 @@
         if name == 'svn:externals':
             return self._render_externals(props[name])
         elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'):
-            return self._render_mergeinfo(props[name])
+            return self._render_mergeinfo(name, mode, context, props)
         elif name == 'svn:needs-lock':
             return self._render_needslock(context)
 
@@ -389,17 +390,166 @@
         return tag.ul([tag.li(tag.a(label, href=href, title=title))
                        for label, href, title in externals_data])
 
-    def _render_mergeinfo(self, prop):
-        prop = prop.rsplit(':', 1)
-        if len(prop) == 2:
-            prop[1] = prop[1].replace(',', u',\u200b')
-        return ':'.join(prop)
+    def _get_blocked_revs(self, props, name, path):
+        """Return the revisions blocked from merging for the given property
+        name and path.
+        """
+        if name == 'svnmerge-integrated':
+            prop = props.get('svnmerge-blocked', '')
+        else:
+            return ""
+        for line in prop.splitlines():
+            try:
+                p, revs = line.split(':', 1)
+                if p.strip('/') == path:
+                    return revs
+            except Exception:
+                pass
+        return ""
 
+    def _render_mergeinfo(self, name, mode, context, props):
+        """Parse svn:mergeinfo and svnmerge-* properties, converting branch
+        names to links and providing links to the revision log for merged
+        and eligible revisions.
+        """
+        has_eligible = name in ('svnmerge-integrated', 'svn:mergeinfo')
+        no_revisions = tag.span(_('revisions'), title=_('No revisions'))
+        no_eligible = tag.span(_('eligible'), title=_('No eligible revisions'))
+        visible_rows = []
+        hidden_rows = []
+        repos = self.env.get_repository()
+        for line in props[name].splitlines():
+            try:
+                path, revs = line.split(':', 1)
+                spath = path.strip('/')
+                revs = revs.strip()
+                if 'LOG_VIEW' in context.perm('source', spath):
+                    row = [tag.a(path, title=_('View dir'),
+                                 href=context.href.browser(spath,
+                                                rev=context.resource.version))]
+                    if revs:
+                        label = (_('merged'), _('blocked'))[
+                                name.endswith('blocked')]
+                        row.append(self._get_revs_link(label, context, spath, 
+                                                       revs))
+                    else:
+                        row.append(no_revisions)
+                    try:
+                        node = repos.get_node(spath, 
+                                              context.resource.version)
+                    except NoSuchNode:
+                        node = None
+                    if node and has_eligible:
+                        eligible = set()
+                        for (p, rev, chg) in node.get_history():
+                            if p != spath:
+                                break
+                            eligible.add(rev)
+                        eligible -= set(Ranges(revs))
+                        blocked = self._get_blocked_revs(props, name, spath)
+                        eligible -= set(Ranges(blocked))
+                        if eligible:
+                            eligible = to_ranges(eligible)
+                            row.append(tag.a(_('eligible'),
+                                       title=eligible.replace(',', ', '),
+                                       href=context.href.log(spath,
+                                                             revs=eligible)))
+                        else:
+                            row.append(no_eligible)
+                    if node:
+                        visible_rows.append(tag.td(each) for each in row)
+                    else:
+                        hidden_rows.append(tag.td(e) for e in line.split(':'))
+                else:
+                    revs = revs.replace(',', u',\u200b')
+                    visible_rows.append(tag.td(path),
+                                        tag.td(revs,
+                                        colspan=has_eligible and 2 or None))
+            except Exception, e:
+                self.log.warning('Rendering of property %s failed: %s', name,
+                                 exception_to_unicode(e))
+                rows.append(tag.td(line, colspan=has_eligible and 3 or 2))
+        vtable = tag.table(tag.tbody(tag.tr(each) for each in visible_rows))
+        htable = tag.table(tag.tbody(tag.tr(each) for each in hidden_rows), 
+                           style='display: none', id="oldmerged")
+        ttable = tag.table(tag.tbody(tag.tr(tag.td(
+            tag.a(_('Toggle deleted branches'), 
+                  href="#toggleoldmerged", id="toggleoldmerged"))))),
+        return tag.div(vtable, ttable, htable)
+    
     def _render_needslock(self, context):
         return tag.img(src=context.href.chrome('common/lock-locked.png'),
                        alt="needs lock", title="needs lock")
 
+    def _get_revs_link(self, label, context, spath, revs):
+        if ',' in revs or '-' in revs:
+            revs_href = context.href.log(spath, revs=revs)
+        else:
+            revs_href = context.href.changeset(revs, spath)
+        return tag.a(label, title=revs.replace(',', ', '), href=revs_href)
 
+    
+    # IPropertyDiffRenderer methods
+
+    def match_property_diff(self, name):
+        return (name == 'svn:mergeinfo' or name.startswith('svnmerge-')) and \
+                4 or 0
+
+    def render_property_diff(self, name, old_context, old_props,
+                             new_context, new_props, options):
+        # build 3 columns table showing modifications on merge sources
+        # || source (added|modified|removed) || added revs || removed revs ||
+        nothing_added = tag.span('-', title=_('nothing added'))
+        nothing_removed = tag.span('-', title=_('nothing removed'))
+        def parse_sources(props):
+            sources = {}
+            for line in props[name].splitlines():
+                path, revs = line.split(':', 1)
+                spath = path.strip('/')
+                revs = revs.strip()
+                sources[spath] = set(Ranges(revs))
+            return sources
+        old_sources = parse_sources(old_props)
+        new_sources = parse_sources(new_props)
+        # go through new sources, detect modified ones or added ones
+        modified_and_added_sources = []
+        for spath, new_revs in new_sources.iteritems():
+            if spath in old_sources:
+                old_revs = old_sources.pop(spath)
+                status = ''
+            else:
+                old_revs = set()
+                status = _(' (added)')
+            def revs_link(revs, context):
+                if revs:
+                    revs = to_ranges(revs)
+                    return self._get_revs_link(revs.replace(',', u',\u200b'),
+                            context, spath, revs)
+            source_href = new_context.href.browser(spath,
+                                              rev=new_context.resource.version)
+            modified_and_added_sources.append([
+                tag.a(spath, status, title=_('View dir'), href=source_href),
+                revs_link(new_revs - old_revs, new_context) or nothing_added,
+                revs_link(old_revs - new_revs, old_context) or nothing_removed,
+            ]) 
+        # go through remaining old sources, those were deleted
+        removed_sources = []
+        for spath, old_revs in old_sources.iteritems():
+            removed_sources.append(
+                    [tag.a(spath, _(' (removed)'),
+                           title=_('View dir'),
+                           href=old_context.href.browser(spath,
+                                            rev=old_context.resource.version)),
+                     nothing_added,
+                     nothing_removed])
+        return tag.div(
+            tag.table(
+                tag.thead(tag.th(c) for c in [name, _("added"), _("removed")]),
+                tag.tbody([
+                    tag.tr([tag.td(col) for col in row]) 
+                    for row in modified_and_added_sources + removed_sources])))
+
+
 class SubversionRepository(Repository):
     """Repository implementation based on the svn.fs API."""
 
Index: trac/versioncontrol/templates/browser.html
===================================================================
--- trac/versioncontrol/templates/browser.html	(revision 8199)
+++ trac/versioncontrol/templates/browser.html	(working copy)
@@ -14,6 +14,10 @@
     <script type="text/javascript" src="${chrome.htdocs_location}js/folding.js"></script>
     <script type="text/javascript">
       jQuery(document).ready(function($) {
+        $("#toggleoldmerged").click(function() {
+                  $("#oldmerged").slideToggle("fast");
+                  return false;
+        });
         $("#jumploc input").hide();
         $("#jumploc select").change(function () {
           this.parentNode.parentNode.submit();

