Edgewall Software

Ticket #7715: 7715-mergeinfo-improved-mergeinfo-diffs-r8263.patch

File 7715-mergeinfo-improved-mergeinfo-diffs-r8263.patch, 15.5 KB (added by cboos, 3 years ago)

Cumulative patch, 7715-mergeinfo-optimized-eligible-r8263.patch + improve the rendering of mergeinfo diffs on Changeset view

  • trac/htdocs/css/diff.css

     
    186186.diff pre { background: #fff; border: 1px solid #ddd; font-size: 85%; 
    187187  margin: 0; 
    188188} 
     189 
     190/* Override .diff table for property tables */ 
     191.diff table.props { 
     192 border: 0px; 
     193} 
     194.diff table.props td { 
     195 background: inherit; 
     196} 
  • trac/htdocs/css/browser.css

     
    174174 margin: 0 0 .4em 1.6em; 
    175175 padding: 0; 
    176176} 
    177 #info .props li { padding: 0; overflow: auto; } 
     177#info .props > li { padding: 2px 0; overflow: auto; } 
    178178 
    179179/* Styles for the HTML preview */ 
    180180#preview { background: #fff; clear: both; margin: 0 } 
  • trac/versioncontrol/svn_fs.py

     
    6060from trac.versioncontrol.cache import CachedRepository 
    6161from trac.versioncontrol.svn_authz import SubversionAuthorizer 
    6262from trac.versioncontrol.web_ui.browser import IPropertyRenderer 
    63 from trac.util import embedded_numbers 
     63from trac.versioncontrol.web_ui.changeset import IPropertyDiffRenderer 
     64from trac.util import Ranges, embedded_numbers, to_ranges 
    6465from trac.util.text import exception_to_unicode, to_unicode 
    65 from trac.util.translation import _ 
     66from trac.util.translation import _, tag_ 
    6667from trac.util.datefmt import utc 
    6768 
    6869 
     
    308309 
    309310 
    310311class SubversionPropertyRenderer(Component): 
    311     implements(IPropertyRenderer) 
     312    implements(IPropertyRenderer, IPropertyDiffRenderer) 
    312313 
    313314    def __init__(self): 
    314315        self._externals_map = {} 
     
    323324        if name == 'svn:externals': 
    324325            return self._render_externals(props[name]) 
    325326        elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'): 
    326             return self._render_mergeinfo(props[name]) 
     327            return self._render_mergeinfo(name, mode, context, props) 
    327328        elif name == 'svn:needs-lock': 
    328329            return self._render_needslock(context) 
    329330 
     
    389390        return tag.ul([tag.li(tag.a(label, href=href, title=title)) 
    390391                       for label, href, title in externals_data]) 
    391392 
    392     def _render_mergeinfo(self, prop): 
    393         prop = prop.rsplit(':', 1) 
    394         if len(prop) == 2: 
    395             prop[1] = prop[1].replace(',', u',\u200b') 
    396         return ':'.join(prop) 
     393    def _get_blocked_revs(self, props, name, path): 
     394        """Return the revisions blocked from merging for the given property 
     395        name and path. 
     396        """ 
     397        if name == 'svnmerge-integrated': 
     398            prop = props.get('svnmerge-blocked', '') 
     399        else: 
     400            return "" 
     401        for line in prop.splitlines(): 
     402            try: 
     403                p, revs = line.split(':', 1) 
     404                if p.strip('/') == path: 
     405                    return revs 
     406            except Exception: 
     407                pass 
     408        return "" 
    397409 
     410    def _render_mergeinfo(self, name, mode, context, props): 
     411        """Parse svn:mergeinfo and svnmerge-* properties, converting branch 
     412        names to links and providing links to the revision log for merged 
     413        and eligible revisions. 
     414        """ 
     415        has_eligible = name in ('svnmerge-integrated', 'svn:mergeinfo') 
     416        no_revisions = tag.span(_('revisions'), title=_('No revisions')) 
     417        no_eligible = tag.span(_('eligible'), title=_('No eligible revisions')) 
     418        visible_rows = [] 
     419        hidden_rows = [] 
     420        repos = self.env.get_repository() 
     421        repos = getattr(repos, 'repos', repos) 
     422        # repos is always a SubversionRepository now 
     423        target_path = context.resource.id 
     424        target_rev = context.resource.version 
     425        if has_eligible: 
     426            ancestor_path_first_revs = {} 
     427            node = repos.get_node(target_path, target_rev) 
     428            while node: 
     429                node = node.get_copy_origin() 
     430                if node and node.path != target_path: 
     431                    ancestor_path_first_revs[node.path] = node.rev + 1 
     432        for line in props[name].splitlines(): 
     433            node = None 
     434            try: 
     435                path, revs = line.split(':', 1) 
     436                spath = path.strip('/') 
     437                revs = revs.strip() 
     438                if 'LOG_VIEW' in context.perm('source', spath): 
     439                    row = [tag.a(path, title=_('View dir'), 
     440                                 href=context.href.browser(spath, 
     441                                                rev=context.resource.version))] 
     442                    if revs: 
     443                        label = (_('merged'), _('blocked'))[ 
     444                                name.endswith('blocked')] 
     445                        row.append(self._get_revs_link(label, context, spath,  
     446                                                       revs)) 
     447                    else: 
     448                        row.append(no_revisions) 
     449                    try: 
     450                        import time 
     451                        now = time.time() 
     452                        node = repos.get_node(path, target_rev) 
     453                    except NoSuchNode: 
     454                        node = None 
     455                    if node and has_eligible: 
     456                        first_rev = ancestor_path_first_revs.get(path) 
     457                        if not first_rev: 
     458                            first_rev = node.get_copy_origin_rev() or 1 
     459                        if target_rev - first_rev < 1000: 
     460                            eligible = set(r for p, r in  
     461                                           repos._history(path, first_rev,  
     462                                                          target_rev,  
     463                                                          node.pool)) 
     464                        else: 
     465                            eligible = set(range(first_rev, target_rev)) 
     466                             
     467                        print spath, 't1', time.time() - now 
     468                        eligible -= set(Ranges(revs)) 
     469                        blocked = self._get_blocked_revs(props, name, spath) 
     470                        eligible -= set(Ranges(blocked)) 
     471                        if eligible: 
     472                            eligible = to_ranges(eligible) 
     473                            print spath, 't2', time.time() - now 
     474                            row.append(tag.a(_('eligible'), 
     475                                       title=eligible.replace(',', ', '), 
     476                                       href=context.href.log(spath, 
     477                                                             revs=eligible))) 
     478                        else: 
     479                            row.append(no_eligible) 
     480                    if node: 
     481                        visible_rows.append(tag.td(each) for each in row) 
     482                    else: 
     483                        hidden_rows.append(tag.td(e) for e in line.split(':')) 
     484                else: 
     485                    revs = revs.replace(',', u',\u200b') 
     486                    visible_rows.append(tag.td(path), 
     487                                        tag.td(revs, 
     488                                        colspan=has_eligible and 2 or None)) 
     489            except Exception, e: 
     490                self.log.warning('Rendering of property %s failed: %s', name, 
     491                                 exception_to_unicode(e)) 
     492                rows = [visible_rows, hidden_rows][bool(node)] 
     493                rows.append(tag.td(line, colspan=has_eligible and 3 or 2)) 
     494        vtable = tag.table(tag.tbody(tag.tr(each) for each in visible_rows)) 
     495        ttable = htable = None 
     496        if hidden_rows: 
     497            # FIXME: the ids below should be unique... 
     498            ttable = tag.table(tag.tbody(tag.tr(tag.td( 
     499                tag.a(_('Toggle deleted branches'),  
     500                      href="#toggleoldmerged", id="toggleoldmerged"))))), 
     501            htable = tag.table(tag.tbody(tag.tr(each) for each in hidden_rows),  
     502                               style='display: none', id="oldmerged") 
     503        return tag.div(vtable, ttable, htable) 
     504     
    398505    def _render_needslock(self, context): 
    399506        return tag.img(src=context.href.chrome('common/lock-locked.png'), 
    400507                       alt="needs lock", title="needs lock") 
    401508 
     509    def _get_revs_link(self, label, context, spath, revs): 
     510        if ',' in revs or '-' in revs: 
     511            revs_href = context.href.log(spath, revs=revs) 
     512        else: 
     513            revs_href = context.href.changeset(revs, spath) 
     514        return tag.a(label, title=revs.replace(',', ', '), href=revs_href) 
    402515 
     516     
     517    # IPropertyDiffRenderer methods 
     518 
     519    def match_property_diff(self, name): 
     520        return (name == 'svn:mergeinfo' or name.startswith('svnmerge-')) and \ 
     521                4 or 0 
     522 
     523    def render_property_diff(self, name, old_context, old_props, 
     524                             new_context, new_props, options): 
     525        # build 3 columns table showing modifications on merge sources 
     526        # || source (added|modified|removed) || added revs || removed revs || 
     527        def parse_sources(props): 
     528            sources = {} 
     529            for line in props[name].splitlines(): 
     530                path, revs = line.split(':', 1) 
     531                spath = path.strip('/') 
     532                revs = revs.strip() 
     533                sources[spath] = set(Ranges(revs)) 
     534            return sources 
     535        old_sources = parse_sources(old_props) 
     536        new_sources = parse_sources(new_props) 
     537        # go through new sources, detect modified ones or added ones 
     538        modified_and_added_sources = [] 
     539        for spath, new_revs in new_sources.iteritems(): 
     540            if spath in old_sources: 
     541                old_revs = old_sources.pop(spath) 
     542                status = '' 
     543            else: 
     544                old_revs = set() 
     545                status = _(' (added)') 
     546            def revs_link(revs, context): 
     547                if revs: 
     548                    revs = to_ranges(revs) 
     549                    return self._get_revs_link(revs.replace(',', u',\u200b'), 
     550                            context, spath, revs) 
     551            source_href = new_context.href.browser(spath, 
     552                                              rev=new_context.resource.version) 
     553            added = revs_link(new_revs - old_revs, new_context) 
     554            removed = revs_link(old_revs - new_revs, old_context) 
     555            if added or removed: 
     556                modified_and_added_sources.append([ 
     557                    tag.a(spath, status, title=_('View dir'), href=source_href), 
     558                    added and tag_("%(revs)s merged", revs=tag.tt(added)), 
     559                    removed and tag_("%(revs)s no longer merged",  
     560                                     revs=tag.tt(removed)), 
     561                ])  
     562        # go through remaining old sources, those were deleted 
     563        removed_sources = [] 
     564        for spath, old_revs in old_sources.iteritems(): 
     565            removed_sources.append( 
     566                    tag.a(_("source %(source)s removed", source=spath), 
     567                           title=_('View dir'), 
     568                           href=old_context.href.browser(spath, 
     569                                            rev=old_context.resource.version))) 
     570        return tag.li(tag_("Property %(prop)s changed", prop=tag.strong(name)), 
     571                tag.table(tag.tbody( 
     572                    [tag.tr(tag.td(dir), tag.td(added), tag.td(removed)) 
     573                     for dir, added, removed in modified_and_added_sources], 
     574                    [tag.tr(tag.td(dir, colspan=3))  
     575                     for dir in removed_sources]), class_='props')) 
     576 
     577 
    403578class SubversionRepository(Repository): 
    404579    """Repository implementation based on the svn.fs API.""" 
    405580 
     
    734909 
    735910class SubversionNode(Node): 
    736911 
    737     def __init__(self, path, rev, repos, pool=None, parent=None): 
     912    def __init__(self, path, rev, repos, pool=None, parent_root=None): 
    738913        self.repos = repos 
    739914        self.fs_ptr = repos.fs_ptr 
    740915        self.authz = repos.authz 
     
    744919        self._requested_rev = rev 
    745920        pool = self.pool() 
    746921 
    747         if parent and parent._requested_rev == self._requested_rev: 
    748             self.root = parent.root 
     922        if parent_root: 
     923            self.root = parent_root 
    749924        else: 
    750925            self.root = fs.revision_root(self.fs_ptr, rev, self.pool()) 
    751926        node_type = fs.check_path(self.root, self._scoped_path_utf8, pool) 
     
    791966                                                            path.strip('/'))): 
    792967                continue 
    793968            yield SubversionNode(path, self._requested_rev, self.repos, 
    794                                  self.pool, self) 
     969                                 self.pool, self.root) 
    795970 
    796971    def get_history(self, limit=None): 
    797972        newer = None # 'newer' is the previously seen history tuple 
     
    8711046        return fs.node_prop(self.root, self._scoped_path_utf8, name, 
    8721047                            self.pool()) 
    8731048 
     1049    def get_copy_origin_rev(self): 
     1050        root_and_path = fs.closest_copy(self.root, self._scoped_path_utf8, 
     1051                                        self.pool()) 
     1052        if root_and_path: 
     1053            return fs.revision_root_revision(root_and_path[0]) 
    8741054 
     1055    def get_copy_origin(self): 
     1056        root_and_path = fs.closest_copy(self.root, self._scoped_path_utf8, 
     1057                                        self.pool()) 
     1058        if root_and_path: 
     1059            root, path = root_and_path 
     1060            rev = fs.revision_root_revision(root) 
     1061            if (path, rev) == (self.path, self.rev): 
     1062                rev, path = fs.copied_from(root, path, self.pool()) 
     1063            return SubversionNode(path, rev, self.repos, self.pool, root) 
     1064 
     1065 
    8751066class SubversionChangeset(Changeset): 
    8761067 
    8771068    def __init__(self, rev, authz, scope, fs_ptr, pool=None): 
  • trac/versioncontrol/templates/browser.html

     
    1414    <script type="text/javascript" src="${chrome.htdocs_location}js/folding.js"></script> 
    1515    <script type="text/javascript"> 
    1616      jQuery(document).ready(function($) { 
     17        $("#toggleoldmerged").click(function() { 
     18                  $("#oldmerged").slideToggle("fast"); 
     19                  return false; 
     20        }); 
    1721        $("#jumploc input").hide(); 
    1822        $("#jumploc select").change(function () { 
    1923          this.parentNode.parentNode.submit(); 
  • trac/util/__init__.py

     
    542542        else: 
    543543            return 0 
    544544 
     545def to_ranges(revs): 
     546    """Converts a list of revisions to a minimal set of ranges. 
     547     
     548    >>> to_ranges([2, 12, 3, 6, 9, 1, 5, 11]) 
     549    '1-3,5-6,9,11-12' 
     550    >>> to_ranges([]) 
     551    '' 
     552    """ 
     553    ranges = [] 
     554    begin = end = None 
     555    def store(): 
     556        if end == begin: 
     557            ranges.append(str(begin)) 
     558        else: 
     559            ranges.append('%d-%d' % (begin, end)) 
     560    for rev in sorted(revs): 
     561        if begin is None: 
     562            begin = end = rev 
     563        elif rev == end + 1: 
     564            end = rev 
     565        else: 
     566            store() 
     567            begin = end = rev 
     568    if begin is not None: 
     569        store() 
     570    return ','.join(ranges) 
     571 
    545572def content_disposition(type, filename=None): 
    546573    """Generate a properly escaped Content-Disposition header""" 
    547574    if filename is not None: