Edgewall Software

Ticket #5821: wiki-blame-r5892.diff

File wiki-blame-r5892.diff, 12.8 KB (added by thatch, 17 months ago)
  • trac/htdocs/css/wiki.css

     
    4242.wiki-toc ul ul, .wiki-toc ol ol { padding-left: 1.2em } 
    4343.wiki-toc li { margin: 0; padding: 0 } 
    4444.wiki-toc .active { background: #ff9; position: relative; } 
     45 
     46.blame_wiki { width: 5em; } 
  • trac/templates/macros.html

     
    5656  - 
    5757  -     `label` the label to use after the Previous/Next words 
    5858  -     `uplabel` the label to use for the Up link 
     59  -     `others` a list of any other links you need to appear 
    5960  - 
    6061  -     Assume the 'chrome' datastructure to be available from the context. 
    6162  --> 
    62   <ul py:def="prevnext_nav(label, uplabel=None)" py:with="links = chrome.links" 
     63  <ul py:def="prevnext_nav(label, uplabel=None, others=None)" py:with="links = chrome.links" 
    6364      py:if="'up' in chrome.links or 
    6465             'prev' in chrome.links or 
    6566             'next' in chrome.links"> 
     
    7374      <a py:with="link = links.up[0]" href="${link.href}" 
    7475         title="${link.title}">$uplabel</a> 
    7576    </li> 
     77    <li py:for="a in others"> 
     78      ${a} 
     79    </li> 
    7680    <li class="last" py:choose=""> 
    7781      <a py:when="'next' in links"  py:with="link = links.next[0]" 
    7882         class="next" href="${link.href}" 
  • trac/wiki/web_ui.py

     
    2727from trac.attachment import AttachmentModule 
    2828from trac.context import Context, ResourceSystem, ResourceNotFound 
    2929from trac.core import * 
    30 from trac.mimeview.api import Mimeview, IContentConverter 
     30from trac.mimeview.api import Mimeview, IContentConverter, \ 
     31    is_binary, IHTMLPreviewAnnotator 
    3132from trac.perm import IPermissionRequestor 
    3233from trac.search import ISearchSource, search_to_sql, shorten_result 
    3334from trac.timeline.api import ITimelineEventProvider, TimelineEvent 
     
    5152 
    5253    implements(IContentConverter, INavigationContributor, IPermissionRequestor, 
    5354               IRequestHandler, ITimelineEventProvider, ISearchSource, 
    54                ITemplateProvider) 
     55               ITemplateProvider, IHTMLPreviewAnnotator) 
    5556 
    5657    page_manipulators = ExtensionPoint(IWikiPageManipulator) 
    5758 
     
    531532            'attachments': AttachmentModule(self.env).attachment_list(context), 
    532533            'default_template': self.DEFAULT_PAGE_TEMPLATE, 
    533534            'templates': templates, 
    534             'version': version 
     535            'version': version, 
     536            'annotate_links': [tag.a(href=req.href.wiki(page.name, 
     537                version=page.version, annotate='1'))("Annotate")], 
    535538        }) 
     539 
     540        if 'annotate' in req.args: 
     541            annotations = ['lineno', 'blame_wiki'] 
     542            context.latest_page = page # XXX hack 
     543            data['preview'] = Mimeview(self.env).preview_data(context, 
     544                page.text, len(page.text), 
     545                'text/x-trac-wiki', page.name, '??', 
     546                annotations=annotations, force_source=True) 
     547            data['annotate_links'] = [tag.a(href=req.href.wiki(page.name, 
     548                version=page.version))("Normal")] 
     549        data['tag'] = tag 
    536550        return 'wiki_view.html', data, None 
    537551 
    538552    # ITimelineEventProvider methods 
     
    598612                yield (req.href.wiki(name), '%s: %s' % (name, shorten_line(text)), 
    599613                       datetime.fromtimestamp(ts, utc), author, 
    600614                       shorten_result(text, terms)) 
     615    def get_annotation_type(self): 
     616        return 'blame_wiki', 'Version', 'Version in which the line changed' 
     617 
     618    def get_annotation_data(self, context): 
     619        annotations = context.latest_page.get_annotations() 
     620        #FIXME: use set 
     621        ages = {} 
     622        #FIXME: use ages. 
     623        return (ages, annotations) 
     624 
     625    def annotate_row(self, context, row, lineno, line, data): 
     626        ages, linedata = data 
     627        row.append(tag.th(tag.a('@', linedata[lineno-1], 
     628href=context.href.wiki(context.latest_page.name, version=linedata[lineno-1])), class_='blame')) 
  • trac/wiki/model.py

     
    2222from trac.core import * 
    2323from trac.util.datefmt import utc, to_timestamp 
    2424from trac.util.translation import _ 
     25from trac.util.blame import LineBlame 
    2526from trac.wiki.api import WikiSystem 
    2627 
    27  
    2828class WikiPage(object): 
    2929    """Represents a wiki page (new or existing).""" 
    3030 
     
    163163        for version,ts,author,comment,ipnr in cursor: 
    164164            time = datetime.fromtimestamp(ts, utc) 
    165165            yield version,time,author,comment,ipnr 
     166 
     167    # Future IBlame? 
     168    def get_annotations(self, db=None): 
     169        # we don't want to allow options, just want opcodes. 
     170        from difflib import SequenceMatcher 
     171 
     172        if not db: 
     173            db = self.env.get_db_cnx() 
     174        cursor = db.cursor() 
     175        cursor.execute("SELECT version, time, author, comment, text FROM wiki " 
     176                       "WHERE name=%s AND version <=%s " 
     177                       "ORDER BY version", (self.name, self.version)) 
     178        b = LineBlame() 
     179        for version, ts, author, comment, text in cursor: 
     180            b.add(version, text.splitlines()) 
     181        return b.revs #FIXME: also return some info about authors, time, etc. 
     182 
  • trac/wiki/templates/wiki_view.html

     
    2121      <h2>Wiki Navigation</h2> 
    2222      <py:choose> 
    2323        <py:when test="version"> 
    24           ${prevnext_nav('Version', 'View Latest Version')} 
     24          ${prevnext_nav('Version', 'View Latest Version', 
     25[tag.a(href=href.wiki(page.name, version=context.req.args.version, 
     26annotate='1'))("Annotate")])} 
    2527        </py:when> 
    2628        <ul py:otherwise=""> 
    2729          <li><a href="${href.wiki('WikiStart')}">Start Page</a></li> 
    2830          <li><a href="${href.wiki('TitleIndex')}">Index</a></li> 
    2931          <li><a href="${href.wiki(page.name, action='history')}">History</a></li> 
     32          <li py:if="'annotate' not in context.req.args"><a href="${href.wiki(page.name, 
     33              annotate='1')}">Annotate</a></li> 
     34          <li py:if="'annotate' in context.req.args"><a 
     35href="${href.wiki(page.name)}">Normal</a></li> 
    3036          <li class="last"> 
    3137            <a href="${req.href.wiki(page.name, action='diff', version=page.version)}">Last Change</a> 
    3238          </li> 
     
    5258 
    5359      <div class="wikipage searchable" py:choose="" xml:space="preserve"> 
    5460        <py:when test="page.exists" xml:space="preserve"> 
    55           ${wiki_to_html(context, page.text)} 
     61          <py:if test="not preview"> 
     62            ${wiki_to_html(context, page.text)} 
     63          </py:if> 
     64          <div py:if="preview" class="searchable"> 
     65            ${preview_file(preview)} 
     66          </div> 
    5667        </py:when> 
    5768        <py:otherwise> 
    5869          Describe ${page.name} here. 
  • trac/util/blame.py

     
     1""" 
     2Provide annotations for source code.  Also known as a blame or praise report. 
     3 
     4Extracted from http://timhatch.com/projects/pyannotate/ 
     5""" 
     6 
     7from difflib import SequenceMatcher 
     8 
     9import sys 
     10 
     11__author__ = "Tim Hatch <tim@timhatch.com>" 
     12__copyright__ = "Copyright (c) 2006 Tim Hatch" 
     13__license__ = "BSD" 
     14__all__ = ["Blame", "TokenBlame", "LineBlame", "BackwardsTokenBlame", "BackwardsLineBlame"] 
     15 
     16class Blame(object): 
     17    def show_info(self, data=None): 
     18        if data is None: data = self.data 
     19        for rev, line in zip(self.revs, data): 
     20            print "%8s %s" % (rev, line) 
     21        if len(self.revs) != len(data): 
     22            print >>sys.stderr, "Warning: not equal length in show_info", len(self.revs), len(data) 
     23 
     24class TokenBlame(Blame): 
     25    """ 
     26    A basic blame, starting from the beginning and working forward. 
     27    Must process all revisions in order to arrive at an answer. 
     28     
     29    Feed it diff tokens! 
     30    """ 
     31    def __init__(self): 
     32        self.oldest_rev = None 
     33        self.newest_rev = None 
     34        self.revs = [] 
     35    def set_size(self, rev, l): 
     36        """ 
     37        Used for setting the initial revision of a file for future comparison. 
     38 
     39        Eventually this will not be required, as the max(i2) of the first diff 
     40        passed in tells us how long the file is (as long as it knows the 'prev' 
     41        rev. 
     42        """ 
     43        self.oldest_rev = rev 
     44        self.newest_rev = rev 
     45        self.revs = [rev] * l 
     46    def add(self, rev, tokens): 
     47        if self.oldest_rev is None: 
     48            self.oldest_rev = rev 
     49 
     50        # Process data. 
     51        r = self.revs 
     52        for tag, i1, i2, j1, j2 in tokens: 
     53            if tag == "insert": 
     54                r[j1:j1] = [rev] * (j2-j1) 
     55            elif tag == "replace": 
     56                r[j1:j1+(i2-i1)] = [rev] * (j2-j1) 
     57            elif tag == "delete": 
     58                del r[j1:j1+(i2-i1)] 
     59            #FIXME: if we had to set oldest_rev, add the equal blocks too, 
     60            # but we don't know what rev they belong to. 
     61 
     62        self.newest_rev = rev 
     63 
     64class BackwardsTokenBlame(Blame): 
     65    """ 
     66    A basic blame, starting from the end and working backward. 
     67    Only needs to process lines as long as there are changes left to be found. 
     68 
     69    Feed it diff tokens! 
     70 
     71    I was toying with 'rev' being the left one. 
     72    """ 
     73    def __init__(self): 
     74        Blame.__init__(self) 
     75        self.oldest_rev = None 
     76        self.newest_rev = None 
     77        self.revs = [] 
     78         
     79    def set_size(self, rev, l): 
     80        """ 
     81        Used for setting the initial revision of a file for future comparison. 
     82 
     83        Eventually this will not be required, as the max(i2) of the first diff 
     84        passed in tells us how long the file is (as long as it knows the 'prev' 
     85        rev. 
     86        """ 
     87        self.oldest_rev = rev 
     88        self.newest_rev = rev 
     89        self.revs = [-1] * l 
     90        self.offsets = dict(zip(range(l), [0] * l)) 
     91        self.b_rev = rev 
     92 
     93    def add(self, rev, tokens): 
     94        """ 
     95        This is perhaps different because 'rev' is the one which causes the 
     96        change.  For a->b working backward, feed tokens normally but pass 
     97        a's rev.  For a<-b working forward, pass b's rev. 
     98        """ 
     99        if self.newest_rev is None: 
     100            self.newest_rev = rev 
     101 
     102        newoffsets = {} 
     103        o = self.offsets 
     104        for tag, i1, i2, j1, j2 in tokens: 
     105            if tag == "delete": 
     106                # We don't need to go back further, these lines have no 
     107                # further lineage. 
     108                pass 
     109            elif tag == "insert" or tag == "replace": 
     110                # These lines are touched in this rev, so don't go back 
     111                # further. 
     112                x = j1-i1 
     113                # Mark them as sourced by this rev. 
     114                for j in range(j1, j2): 
     115                    if j in o: # and o[j] != -1: 
     116                        self.revs[j - o[j]] = self.b_rev 
     117            elif tag == "equal": 
     118                # These _do_ go back further, so update their offsets. 
     119                x = j1-i1 
     120                for i in range(i1, i2): 
     121                    if (i+x) in o: # and self.offsets[i+x] != -1: 
     122                        newoffsets[i] = o[i+x] - x 
     123 
     124        self.offsets = newoffsets 
     125        self.oldest_rev = rev 
     126        self.b_rev = rev 
     127 
     128        if not len(self.offsets): raise StopIteration() 
     129 
     130class LineBlame(TokenBlame): 
     131    """ 
     132    Feed me lines! 
     133    """ 
     134    def __init__(self): 
     135        TokenBlame.__init__(self) 
     136        self.data = None 
     137    def add(self, rev, lines): 
     138        if self.oldest_rev is None: 
     139            self.set_size(rev, len(lines)) 
     140        else: 
     141            s = SequenceMatcher(None, self.data, lines) 
     142            TokenBlame.add(self, rev, s.get_opcodes()) 
     143        self.data = lines 
     144 
     145class BackwardsLineBlame(BackwardsTokenBlame): 
     146    """ 
     147    Feed me lines!  Backwards! 
     148    """ 
     149    def __init__(self): 
     150        BackwardsTokenBlame.__init__(self) 
     151        self.data = None 
     152    def add(self, rev, lines): 
     153        if self.oldest_rev is None: 
     154            self.set_size(rev, len(lines)) 
     155            self.data = lines 
     156            self.prev_data = lines 
     157        else: 
     158            s = SequenceMatcher(None, lines, self.prev_data) 
     159            #codes = list(s.get_opcodes()) 
     160            #print "\n", rev, self.prev_data, lines 
     161            #print codes 
     162            BackwardsTokenBlame.add(self, rev, s.get_opcodes()) 
     163        self.prev_data = lines 
     164