Changeset 1694

Show
Ignore:
Timestamp:
12/14/06 18:51:34 (2 years ago)
Author:
eblot
Message:

RevtreePlugin:

  • Make the enhancers pluggable using the Trac component framework
    • a 'enhancer' is a regular Trac plugin, any number of enhancer can be added to enhance the revision tree graph
    • default enhancer creates operations for branch tagging (only), log message-based enhancer to come
    • the so-called optimizer (which should be renamed a.s.a.p.) can be replaced
  • More clean up
  • Remove a stupid red bar (css)

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • revtreeplugin/0.10/revtree/enhancer.py

    r1652 r1694  
    1313# 
    1414 
     15from revtree import IRevtreeEnhancer 
    1516from revtree.svgview import SvgOperation, SvgGroup 
     17from trac.core import * 
    1618 
    1719 
    18 class Enhancer(object): 
    19     """Enhance the appearance of the RevTree with site-specific properties 
    20     This file is a very basic skeleton that needs to customized, to provide 
    21     SvgOperation, SvgGroup and other widgets in the RevTree graphic""" 
     20class SimpleContainer(object): 
     21    """Simple container for enhancer parameters""" 
    2222     
    23     def __init__(self, repos, svgrevtree): 
    24         self._repos = repos 
    25         self._creations = [] 
    26         self._deliveries = [] 
    27         self._groups = [] 
    28         self._svgrevtree = svgrevtree 
     23    def __init__(self): 
     24        pass 
     25         
     26 
     27class SimpleEnhancer(Component): 
     28    """Enhance the appearance of the RevTree with site-specific properties. 
     29     
     30    Create branch clone operation (on branch/tag operations) 
     31     
     32    This class is a very basic skeleton that needs to customized, to provide 
     33    SvgOperation, SvgGroup and other widgets in the RevTree graphic 
     34    """ 
     35     
     36    implements(IRevtreeEnhancer)     
     37     
     38    def create(self, env, req, repos, svgrevtree): 
     39        """Creates the internal data from the repository""" 
     40        enhancer = SimpleContainer() 
     41        enhancer.repos = repos 
     42        enhancer.creations = [] 
     43        enhancer.deliveries = [] 
     44        enhancer.groups = [] 
     45        enhancer.svgrevtree = svgrevtree 
    2946        # z-depth indexed widgets: back=1, fore=2 
    30         self._widgets = ([], [], []) 
    31  
    32     def create(self): 
    33         """Creates the internal data from the repository""" 
    34         self._sort() 
    35         for branch in self._repos.branches().values(): 
    36             svgbranch = self._svgrevtree.svgbranch(branch=branch) 
     47        enhancer.widgets = ([], [], []) 
     48         
     49        #self._sort() 
     50        for branch in enhancer.repos.branches().values(): 
     51            svgbranch = enhancer.svgrevtree.svgbranch(branch=branch) 
    3752            if not svgbranch: 
    3853                # branch has probably been filtered out 
     
    4560                svgbranch.svgchangeset(firstchgset).invert_color() 
    4661                (rev, path) = branch.source() 
    47                 srcchg = self._repos.changeset(rev) 
     62                srcchg = enhancer.repos.changeset(rev) 
    4863                if srcchg is None: 
    4964                    continue 
    5065                # .. and create an operation between both changesets 
    51                 self._creations.append((srcchg, firstchgset)) 
     66                enhancer.creations.append((srcchg, firstchgset)) 
    5267            lastchgset = branch.youngest() 
    5368            if lastchgset: 
     
    5671                    # tweak the color of this changeset 
    5772                    svgbranch.svgchangeset(lastchgset).kill() 
     73        return enhancer 
    5874 
    59     def build(self): 
     75    def build(self, enhancer): 
    6076        """Build the enhanced widgets""" 
    61         for (srcchg, dstchg) in self._creations: 
    62             svgsrcbr = self._svgrevtree.svgbranch(branchname=srcchg.branchname) 
     77        for (srcchg, dstchg) in enhancer.creations: 
     78            svgsrcbr = enhancer.svgrevtree.svgbranch(branchname=srcchg.branchname) 
    6379            if svgsrcbr is None: 
    6480                continue 
    6581            svgsrcchg = svgsrcbr.svgchangeset(srcchg) 
    66             svgdstbr = self._svgrevtree.svgbranch(branchname=dstchg.branchname) 
     82            svgdstbr = enhancer.svgrevtree.svgbranch(branchname=dstchg.branchname) 
    6783            if svgdstbr is None: 
    6884                continue 
    6985            svgdstchg = svgdstbr.svgchangeset(dstchg) 
    70             op = SvgOperation(self._svgrevtree, svgsrcchg, svgdstchg, '#5faf5f') 
    71             self._widgets[2].append(op) 
     86            op = SvgOperation(enhancer.svgrevtree, svgsrcchg, svgdstchg, \ 
     87                              '#5faf5f') 
     88            enhancer.widgets[2].append(op) 
    7289                     
    73         for wl in self._widgets: 
     90        for wl in enhancer.widgets: 
    7491            map(lambda w: w.build(), wl) 
    7592         
    76     def render(self, level): 
    77         """Renders widgets, from background plane to foreground plane""" 
    78         if level < len(self._widgets): 
    79             map(lambda w: w.render(), self._widgets[level]) 
     93    def render(self, enhancer, level): 
     94        """Renders the widgets, from background plane to foreground plane""" 
     95        if level < len(enhancer.widgets): 
     96            map(lambda w: w.render(), enhancer.widgets[level]) 
     97             
     98 
     99 
    80100         
    81     def optimize(self, branches): 
    82         """Provides a list of branches, sorted from left-most to rigtht-most 
    83         position. 
    84         Optimal placement is required to reduce the number of operation links 
    85         that cross each other on the rendered graphic""" 
    86         return [b for b in self._obranches if b in branches] 
    87          
    88     def _sort(self): 
    89         """Computes the optimal placement of branches. 
    90         This example is FAR from providing optimal placements ;-)""" 
    91         graph = {} 
    92         for v in self._repos.branches().values(): 
    93             k = v.name 
    94             src = v.source() 
    95             if src: 
    96                 (rev, path) = src 
    97                 if graph.has_key(path): 
    98                     graph[path].append(k) 
    99                 else: 
    100                     graph[path] = [k] 
    101         density = [] 
    102         for (p, v) in graph.items(): 
    103             density.append((p,len(v))) 
    104         density.sort(lambda a,b: cmp(a[1],b[1])) 
    105         density.reverse() 
    106         order = [] 
    107         cur = 0 
    108         for (branch, weight) in density: 
    109             order.insert(cur, branch) 
    110             if cur: 
    111                 cur = 0 
    112             else: 
    113                 cur = len(order) 
    114         branches = [] 
    115         for br in graph.values(): 
    116             branches.extend(br) 
    117         branches.extend([br.name for br in self._repos.branches().values() \ 
    118                          if br.name not in branches]) 
    119         for branch in branches: 
    120             if branch in order: 
    121                 continue 
    122             order.insert(cur, branch) 
    123             if cur: 
    124                 cur = 0 
    125             else: 
    126                 cur = len(order) 
    127         self._obranches = [self._repos.branch(name) for name in order] 
    128   
  • revtreeplugin/0.10/revtree/htdocs/css/revtree.css

    r1675 r1694  
    3131   /* FIXME */ 
    3232   width: 100%; 
    33    border-top: 4px solid red; 
    34    background-color: red; 
    3533   margin-top: 0; 
    3634   margin-left: 0; 
  • revtreeplugin/0.10/revtree/__init__.py

    r1633 r1694  
    1212# history and logs, available at http://projects.edgewall.com/trac/. 
    1313 
    14 from revtree.model import Repository, ChangesetEmptyRange 
    15 from revtree.svgview import SvgRevtree 
     14from revtree.api import EmptyRangeError, IRevtreeEnhancer, \ 
     15                        IRevtreeOptimizer, RevtreeSystem 
  • revtreeplugin/0.10/revtree/model.py

    r1675 r1694  
    1515import re 
    1616import time 
     17 
     18from revtree import IRevtreeOptimizer 
    1719from trac.versioncontrol import Node, Changeset 
    18 from trac.core import TracError 
    19  
     20from trac.core import * 
    2021# only for get_revision_properties 
    2122from svn import fs 
    2223 
    23 class ChangesetEmptyRange(TracError): 
    24     """Defines a RevTree error (no changeset in the selected range)""" 
    25     def __init__(self, msg=None): 
    26         TracError.__init__(self, "%sNo changeset" \ 
    27                            % (msg and '%s: ' % msg or '')) 
     24 
     25class DefaultRevtreeOptimizer(Component): 
     26    """Default optmizer""" 
     27     
     28    implements(IRevtreeOptimizer)     
     29         
     30    def optimize(self, repos, branches): 
     31        """Computes the optimal placement of branches. 
     32         
     33        Optimal placement is recommended to reduce the number of operation  
     34        links that cross each other on the rendered graphic. 
     35        This rudimentary example is FAR from providing optimal placements... 
     36        """ 
     37        # FIXME: really stupid algorithm 
     38        graph = {} 
     39        for v in repos.branches().values(): 
     40            k = v.name 
     41            src = v.source() 
     42            if src: 
     43                (rev, path) = src 
     44                if graph.has_key(path): 
     45                    graph[path].append(k) 
     46                else: 
     47                    graph[path] = [k] 
     48        density = [] 
     49        for (p, v) in graph.items(): 
     50            density.append((p,len(v))) 
     51        density.sort(lambda a,b: cmp(a[1],b[1])) 
     52        density.reverse() 
     53        order = [] 
     54        cur = 0 
     55        for (branch, weight) in density: 
     56            order.insert(cur, branch) 
     57            if cur: 
     58                cur = 0 
     59            else: 
     60                cur = len(order) 
     61        nbranches = [] 
     62        for br in graph.values(): 
     63            nbranches.extend(br) 
     64        nbranches.extend([br.name for br in repos.branches().values() \ 
     65                          if br.name not in nbranches]) 
     66        for branch in nbranches: 
     67            if branch in order: 
     68                continue 
     69            order.insert(cur, branch) 
     70            if cur: 
     71                cur = 0 
     72            else: 
     73                cur = len(order) 
     74        obranches = [repos.branch(name) for name in order] 
     75        # FIXME: use filter() 
     76        return [b for b in obranches if b in branches] 
     77 
    2878 
    2979class BranchChangeset(object): 
     
    284334        """Builds an internal representation of the repository, which  
    285335           is used to generate a graphical view of it""" 
    286         self._crepos.sync() 
    287336        start = 0 
    288337        stop = int(time.time()) 
     
    305354            vcsort = [(c.rev, c) for c in vcchangesets] 
    306355        if len(vcsort) < 1: 
    307             raise ChangesetEmptyRange 
     356            raise EmptyRangeError 
    308357        vcsort.sort() 
    309358        self._revrange = (vcsort[0][1].rev,vcsort[-1][1].rev) 
     
    322371              (br, len(self._branches[br])) 
    323372        return msg 
     373 
  • revtreeplugin/0.10/revtree/svgview.py

    r1675 r1694  
    1616import os 
    1717 
    18 from revtree import ChangesetEmptyRange 
     18from colorsys import rgb_to_hsv, hsv_to_rgb 
    1919from math import sqrt 
    2020from random import randrange, seed 
    21 from colorsys import rgb_to_hsv, hsv_to_rgb 
     21from revtree import EmptyRangeError, IRevtreeEnhancer, IRevtreeOptimizer 
     22from trac.core import * 
    2223 
    2324UNIT = 25 
     
    4849    return "javascript:window.parent.location.href='%s'" % url 
    4950 
    50              
     51 
    5152class SvgColor(object): 
    5253    """Helpers for color management (conversion, generation, ...)""" 
     
    224225                              self._strokecolor, 
    225226                              self._parent.strokewidth()) 
    226                 g = SVG.group('rev%d' % self._revision, 
     227                g = SVG.group('grp%d' % self._revision, 
    227228                              elements=[self._widget, lt, lb]) 
    228229                self._widget = g 
     
    752753    """Main object that represents the revision tree as a SVG graph""" 
    753754 
    754     def __init__(self, env, repos, urlbase): 
     755    def __init__(self, env, repos, urlbase, enhancers, optimizer): 
    755756        """Construct a new SVG revision tree""" 
    756757        # Environment 
     
    763764        self.revrange = None 
    764765        # Optional enhancers 
    765         self._enhancers = [] 
     766        self.enhancers = enhancers 
     767        # Optimizer 
     768        self.optimizer = optimizer 
    766769        # Trunk branches 
    767770        self.trunks = self.env.config.get('revtree', 'trunks',  
     
    777780        # Operation points 
    778781        self._oppoints = {} 
     782        # Add-on elements (from enhancers) 
     783        self._addons = {} 
    779784        # Init color generator with a predefined value 
    780785        seed(0) 
    781          
    782     def add_enhancer(self, enhancer): 
    783         self._enhancers.append(enhancer) 
    784          
     786                 
    785787    def position(self): 
    786788        """Return the position of the revision tree widget""" 
     
    816818        return 'url(#%s)' % self._arrows.create(color, head) 
    817819         
    818     def create(self, revisions=None, branches=None, authors=None,  
     820    def create(self, req, revisions=None, branches=None, authors=None,  
    819821                     hidetermbranch=False, style='compact'): 
    820822        if revisions is not None: 
     
    845847            self._vtimes[r] = vtime 
    846848            vtime += 1 
    847         map(lambda e: e.create(), self._enhancers) 
     849        for enhancer in self.enhancers: 
     850            self._addons[enhancer] = \ 
     851                enhancer.create(self.env, req, self.repos, self) 
    848852                      
    849853    def build(self): 
    850854        """Build the graph""" 
    851         branches = [svgbr.branch() for svgbr in self._svgbranches.values()] 
    852         for enhancer in self._enhancers:   
    853             f_branches = enhancer.optimize(branches) 
    854             if f_branches: 
    855                 # stop on the first enhancer 
    856                 branches = f_branches 
    857                 break 
     855        branches = self.optimizer.optimize(self.repos, \ 
     856            [svgbr.branch() for svgbr in self._svgbranches.values()]) 
    858857        branch_xpos = UNIT 
    859858        svgbranches = [self.svgbranch(branch=b) for b in branches] 
    860859        for svgbranch in svgbranches: 
    861860            svgbranch.build((branch_xpos, UNIT/6)) 
    862             branch_xpos += svgbranch.header().extent()[0] + UNIT             
    863         for enhancer in self._enhancers: 
    864             enhancer.build() 
     861            branch_xpos += svgbranch.header().extent()[0] + UNIT 
     862        map(lambda e: e.build(self._addons[e]), self.enhancers) 
     863        # FIXME: why not using svgbranches ? 
    865864        svgbranches = self._svgbranches.values() 
    866865        svgbranches.sort() 
     
    871870                maxheight = h 
    872871        if not svgbranches: 
    873             raise ChangesetEmptyRange 
     872            raise EmptyRangeError 
    874873        w = svgbranches[-1].position()[0] - svgbranches[0].position()[0] + \ 
    875874            svgbranches[-1].extent()[0] + 2*UNIT 
     
    913912 
    914913    def __str__(self): 
    915         """Render the revision tree as a SVG string""" 
     914        """Dump the revision tree as a SVG string""" 
    916915        import cStringIO 
    917916        xml=cStringIO.StringIO() 
     
    926925         
    927926    def render(self, scale=1, width=None, height=None, linkparent=False): 
     927        """Render the revision tree""" 
    928928        self._svg = SVG.svg((0,0,self._extent[0],self._extent[1]), 
    929929                            scale*self._extent[0], scale*self._extent[1]) 
    930930        self._arrows.render() 
    931         map(lambda e: e.render(1), self._enhancers) 
     931        # FIXME: only two levels for enhancers (background, foreground) 
     932        map(lambda e: e.render(self._addons[e], 1), self.enhancers) 
    932933        map(lambda b: b.render(), self._svgbranches.values()) 
    933         map(lambda e: e.render(2), self._enhancers) 
     934        map(lambda e: e.render(self._addons[e], 2), self.enhancers) 
    934935        #dbgDump(self._svg) 
  • revtreeplugin/0.10/revtree/web_ui.py

    r1675 r1694  
    1717import time 
    1818 
     19from revtree.api import EmptyRangeError, RevtreeSystem 
     20from revtree.model import Repository 
    1921from trac.core import * 
    2022from trac.perm import IPermissionRequestor 
     
    2729from trac.wiki import wiki_to_html, WikiSystem 
    2830 
    29 from revtree import Repository, SvgRevtree, ChangesetEmptyRange 
    30 from revtree.enhancer import Enhancer 
    3131 
    3232class RevtreeStore(object): 
     
    134134         
    135135    def __getitem__(self, name): 
    136         """getter (dictionary)""" 
     136        """Getter (dictionary)""" 
    137137        return self.values[name] 
    138138 
    139139    def __setitem__(self, name, value): 
    140         """setter (dictionnary)""" 
     140        """Setter (dictionnary)""" 
    141141        self.values[name] = value 
    142142 
     
    147147    implements(IPermissionRequestor, INavigationContributor, \ 
    148148               IRequestHandler, ITemplateProvider) 
    149      
     149                    
    150150    PERIODS = { 1 : 'day', 2 : '2 days', 3 : '3 days', 7: 'week', 
    151151                14 : 'fortnight', 31 : 'month', 61 : '2 months',  
     
    184184        else: 
    185185            return self._process_revtree(req) 
    186              
     186 
     187    # ITemplateProvider 
     188     
     189    def get_htdocs_dirs(self): 
     190        """Return the absolute path of a directory containing additional 
     191        static resources (such as images, style sheets, etc). 
     192        """ 
     193        from pkg_resources import resource_filename 
     194        return [('revtree', resource_filename(__name__, 'htdocs'))] 
     195     
     196    def get_templates_dirs(self): 
     197        """Return the absolute path of the directory containing the provided 
     198        ClearSilver templates. 
     199        """ 
     200        from pkg_resources import resource_filename 
     201        return [resource_filename(__name__, 'templates')] 
     202     
     203    # end of interface implementation 
     204             
     205    def __init__(self): 
     206        """Reads the configuration and run sanity checks""" 
     207        if self.config.get('trac', 'repository_type') != 'svn': 
     208            raise TracError, "Revtree only supports Subversion repositories" 
     209        bre = self.config.get('revtree', 'branch_re', 
     210                  r'^(?P<branch>branches/[^/]+|trunk|data)' 
     211                  r'(?:/(?P<path>.*))?$') 
     212        self.bcre = re.compile(bre) 
     213        self.trunks = self.env.config.get('revtree', 'trunks',  
     214                                          'trunk').split(' ') 
     215        self.scale = float(self.env.config.get('revtree', 'scale', '1')) 
     216        repos = self.env.get_repository() 
     217        self.oldest = int(self.env.config.get('revtree', 'revbase',  
     218                                              repos.get_oldest_rev())) 
     219        self.youngest = repos.get_youngest_rev() 
     220        if self.config.getbool('revtree', 'reltime', True): 
     221            self.timebase = repos.get_changeset(self.youngest).date 
     222        else: 
     223            self.timebase = None 
     224        self.style = self.config.get('revtree', 'style', 'compact') 
     225        if self.style not in [ 'compact', 'timeline']: 
     226            raise TracError, "Unsupported style: %s" % self.style 
     227 
    187228    def _process_log(self, req): 
    188229        """Handle AJAX log requests""" 
     
    230271        try: 
    231272            if not revstore.can_be_rendered(): 
    232                 raise ChangesetEmptyRange 
     273                raise EmptyRangeError 
    233274                 
    234275            repos = Repository(self.env, req.authname) 
     
    239280            filename = self._get_filename() 
    240281                                         
    241             svgrevtree = SvgRevtree(self.env, repos, self.urlbase) 
    242             enhancer = Enhancer(repos, svgrevtree) 
    243             svgrevtree.add_enhancer(enhancer) 
    244             svgrevtree.create(revstore.revrange, revstore.get_branches(),  
     282            svgrevtree = RevtreeSystem(self.env).get_revtree(repos) 
     283             
     284            #enhancer = Enhancer(repos, svgrevtree) 
     285            #svgrevtree.add_enhancer(enhancer) 
     286             
     287            svgrevtree.create(req, revstore.revrange, revstore.get_branches(),  
    245288                              revstore.get_authors(),  
    246289                              revstore.get_hidetermbranch(),  
     
    266309            revstore.save(req.session) 
    267310             
    268         except ChangesetEmptyRange
     311        except EmptyRangeError
    269312            req.hdf['revtree.errormsg'] = "Selected filters cannot render" \ 
    270313                                          " a revision tree" 
     
    290333        add_stylesheet(req, 'revtree/css/revtree.css') 
    291334        return 'revtree.cs', 'application/xhtml+xml' 
    292  
    293     # ITemplateProvider 
    294  
    295     def get_htdocs_dirs(self): 
    296         """Return the absolute path of a directory containing additional 
    297         static resources (such as images, style sheets, etc). 
    298         """ 
    299         from pkg_resources import resource_filename 
    300         return [('revtree', resource_filename(__name__, 'htdocs'))] 
    301  
    302     def get_templates_dirs(self): 
    303         """Return the absolute path of the directory containing the provided 
    304         ClearSilver templates. 
    305         """ 
    306         from pkg_resources import resource_filename 
    307         return [resource_filename(__name__, 'templates')] 
    308  
    309     # end of interface implementation 
    310  
    311     def __init__(self): 
    312         if self.config.get('trac', 'repository_type') != 'svn': 
    313             raise TracError, "Revtree only supports Subversion repositories" 
    314         bre = self.config.get('revtree', 'branch_re', 
    315                   r'^(?P<branch>branches/[^/]+|trunk|data)' 
    316                   r'(?:/(?P<path>.*))?$') 
    317         self.bcre = re.compile(bre) 
    318         self.urlbase = self.config.get('trac', 'base_url') 
    319         self.trunks = self.env.config.get('revtree', 'trunks',  
    320                                           'trunk').split(' ') 
    321         self.scale = float(self.env.config.get('revtree', 'scale', '1')) 
    322         if not self.urlbase: 
    323             raise TracError, "Base URL not defined" 
    324         repos = self.env.get_repository() 
    325         self.oldest = int(self.env.config.get('revtree', 'revbase',  
    326                                               repos.get_oldest_rev())) 
    327         self.youngest = repos.get_youngest_rev() 
    328         if self.config.getbool('revtree', 'reltime', True): 
    329             self.timebase = repos.get_changeset(self.youngest).date 
    330         else: 
    331             self.timebase = None 
    332         self.style = self.config.get('revtree', 'style', 'compact') 
    333         if self.style not in [ 'compact', 'timeline']: 
    334             raise TracError, "Unsupported style: %s" % self.style 
    335335 
    336336    def _get_periods(self): 
  • revtreeplugin/0.10/setup.py

    r1669 r1694  
    1616 
    1717PACKAGE = 'RevtreePlugin' 
    18 VERSION = '0.4.2
     18VERSION = '0.4.3
    1919 
    2020setup ( 
     
    3939        'trac.plugins': [ 
    4040            'revtree.web_ui = revtree.web_ui', 
     41            'revtree.enhancer = revtree.enhancer' 
    4142        ] 
    4243    }