diff --git a/trac/htdocs/css/browser.css b/trac/htdocs/css/browser.css
--- a/trac/htdocs/css/browser.css
+++ b/trac/htdocs/css/browser.css
@@ -174,7 +174,7 @@
  margin: 0 0 .4em 1.6em;
  padding: 0;
 }
-#info .props li { padding: 0; overflow: auto; }
+#info .props > li { padding: 2px 0; overflow: auto; }
 
 /* Styles for the HTML preview */
 #preview { background: #fff; clear: both; margin: 0 }
diff --git a/trac/util/__init__.py b/trac/util/__init__.py
--- a/trac/util/__init__.py
+++ b/trac/util/__init__.py
@@ -542,6 +542,33 @@
         else:
             return 0
 
+def to_ranges(revs):
+    """Converts a list of revisions to a minimal set of ranges.
+    
+    >>> to_ranges([2, 12, 3, 6, 9, 1, 5, 11])
+    '1-3,5-6,9,11-12'
+    >>> to_ranges([])
+    ''
+    """
+    ranges = []
+    begin = end = None
+    def store():
+        if end == begin:
+            ranges.append(str(begin))
+        else:
+            ranges.append('%d-%d' % (begin, end))
+    for rev in sorted(revs):
+        if begin is None:
+            begin = end = rev
+        elif rev == end + 1:
+            end = rev
+        else:
+            store()
+            begin = end = rev
+    if begin is not None:
+        store()
+    return ','.join(ranges)
+
 def content_disposition(type, filename=None):
     """Generate a properly escaped Content-Disposition header"""
     if filename is not None:
diff --git a/trac/versioncontrol/svn_fs.py b/trac/versioncontrol/svn_fs.py
--- a/trac/versioncontrol/svn_fs.py
+++ b/trac/versioncontrol/svn_fs.py
@@ -60,9 +60,10 @@
 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.translation import _, tag_
 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,16 +390,185 @@
         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 _get_node_revs_ref(self, repos, path, version):
+        node = repos.get_node(path, version)
+        for (p, rev, chg) in node.get_history():
+            if p != path:
+                break
+            yield rev
+    
+    def _get_node_revs(self, repos, path, version):
+        node = repos.get_node(path, version)    # Ensure node exists
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT DISTINCT rev FROM node_change "
+                       "WHERE (path = %%s OR path %s) "
+                       "  AND %s <= %%s" % (db.like(), db.cast('rev', 'int')),
+                       (path, db.like_escape(path + '/') + '%', version))
+        revs = list(int(row[0]) for row in cursor)
+        revs.sort()
+        cursor.execute("SELECT rev FROM node_change "
+                       "WHERE path = %%s "
+                       "  AND change_type IN ('A', 'C', 'M') "
+                       "  AND %s <= %%s "
+                       "ORDER BY %s DESC "
+                       "LIMIT 1" % ((db.cast('rev', 'int'),) * 2),
+                       (path, version))
+        created = 0
+        for row in cursor:
+            created = int(row[0])
+        import bisect
+        return revs[bisect.bisect_left(revs, created):]
+        
+    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')
+        revs_label = (_('merged'), _('blocked'))[name.endswith('blocked')]
+        repos = self.env.get_repository()
+        rows = []
+        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))]
+                    row.append(self._get_revs_link(revs_label, context, spath,
+                                                   revs))
+                    if has_eligible:
+                        t = time.time()
+                        eligible = set(self._get_node_revs(repos, spath,
+                                                    context.resource.version))
+                        t = time.time() - t
+                        t_ref = time.time()
+                        eligible_ref = set(self._get_node_revs_ref(repos, spath,
+                                                    context.resource.version))
+                        t_ref = time.time() - t_ref
+                        eligible_error = eligible ^ eligible_ref
+                        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(tag.span(_('eligible'),
+                                       title=_('No eligible revisions')))
+                        row.append(tag("(time=%.3f, time_ref=%.3f, error: %s)"
+                                       % (t, t_ref, ", ".join(str(rev)
+                                                  for rev in eligible_error))))
+                    rows.append(tag.td(each) for each in row)
+                else:
+                    revs = revs.replace(',', u',\u200b')
+                    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))
+        return tag.div(tag.table(tag.tbody(tag.tr(each) for each in rows)))
+    
+    def _get_revs_link(self, label, context, spath, revs):
+        if not revs:
+            return tag.span(label, title=_('No revisions'))
+        elif ',' 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)
+    
     def _render_needslock(self, context):
         return tag.img(src=context.href.chrome('common/lock-locked.png'),
                        alt="needs lock", title="needs lock")
 
+    # 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 ||
+        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
+        blocked = name.endswith('blocked')
+        added_label = [_('merged: '), _('blocked: ')][blocked]
+        removed_lable = [_('un-merged: '), _('un-blocked: ')][blocked]
+        def revs_link(revs, context):
+            if revs:
+                revs = to_ranges(revs)
+                return self._get_revs_link(revs.replace(',', u',\u200b'),
+                                           context, spath, revs)
+        modified_and_added_sources = []
+        for spath, new_revs in new_sources.iteritems():
+            if spath in old_sources:
+                old_revs = old_sources.pop(spath)
+                status = None
+            else:
+                old_revs = set()
+                status = _(' (added)')
+            source_href = new_context.href.browser(spath,
+                                              rev=new_context.resource.version)
+            added = revs_link(new_revs - old_revs, new_context)
+            removed = revs_link(old_revs - new_revs, old_context)
+            if added or removed:
+                modified_and_added_sources.append([
+                    [tag.a(spath, title=_('View dir'), href=source_href),
+                     status],
+                    added and tag(added_label, added),
+                    removed and tag(removed_label, removed)]) 
+        # go through remaining old sources, those were deleted
+        removed_sources = []
+        for spath, old_revs in old_sources.iteritems():
+            removed_sources.append(
+                    tag.a(_("Source %(source)s removed", source=spath),
+                           title=_('View dir'),
+                           href=old_context.href.browser(spath,
+                                            rev=old_context.resource.version)))
+        return tag.li(tag_("Property %(prop)s changed", prop=tag.strong(name)),
+                tag.table(tag.tbody(
+                    [tag.tr(tag.td(dir), tag.td(added), tag.td(removed))
+                     for dir, added, removed in modified_and_added_sources],
+                    [tag.tr(tag.td(dir, colspan=3)) 
+                     for dir in removed_sources]), class_='props'))
+
 
 class SubversionRepository(Repository):
     """Repository implementation based on the svn.fs API."""
@@ -734,7 +904,7 @@
 
 class SubversionNode(Node):
 
-    def __init__(self, path, rev, repos, pool=None, parent=None):
+    def __init__(self, path, rev, repos, pool=None, parent_root=None):
         self.repos = repos
         self.fs_ptr = repos.fs_ptr
         self.authz = repos.authz
@@ -744,8 +914,8 @@
         self._requested_rev = rev
         pool = self.pool()
 
-        if parent and parent._requested_rev == self._requested_rev:
-            self.root = parent.root
+        if parent_root:
+            self.root = parent_root
         else:
             self.root = fs.revision_root(self.fs_ptr, rev, self.pool())
         node_type = fs.check_path(self.root, self._scoped_path_utf8, pool)
@@ -791,7 +961,7 @@
                                                             path.strip('/'))):
                 continue
             yield SubversionNode(path, self._requested_rev, self.repos,
-                                 self.pool, self)
+                                 self.pool, self.root)
 
     def get_history(self, limit=None):
         newer = None # 'newer' is the previously seen history tuple

