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
42 42 .wiki-toc ul ul, .wiki-toc ol ol { padding-left: 1.2em } 43 43 .wiki-toc li { margin: 0; padding: 0 } 44 44 .wiki-toc .active { background: #ff9; position: relative; } 45 46 .blame_wiki { width: 5em; } -
trac/templates/macros.html
56 56 - 57 57 - `label` the label to use after the Previous/Next words 58 58 - `uplabel` the label to use for the Up link 59 - `others` a list of any other links you need to appear 59 60 - 60 61 - Assume the 'chrome' datastructure to be available from the context. 61 62 --> 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" 63 64 py:if="'up' in chrome.links or 64 65 'prev' in chrome.links or 65 66 'next' in chrome.links"> … … 73 74 <a py:with="link = links.up[0]" href="${link.href}" 74 75 title="${link.title}">$uplabel</a> 75 76 </li> 77 <li py:for="a in others"> 78 ${a} 79 </li> 76 80 <li class="last" py:choose=""> 77 81 <a py:when="'next' in links" py:with="link = links.next[0]" 78 82 class="next" href="${link.href}" -
trac/wiki/web_ui.py
27 27 from trac.attachment import AttachmentModule 28 28 from trac.context import Context, ResourceSystem, ResourceNotFound 29 29 from trac.core import * 30 from trac.mimeview.api import Mimeview, IContentConverter 30 from trac.mimeview.api import Mimeview, IContentConverter, \ 31 is_binary, IHTMLPreviewAnnotator 31 32 from trac.perm import IPermissionRequestor 32 33 from trac.search import ISearchSource, search_to_sql, shorten_result 33 34 from trac.timeline.api import ITimelineEventProvider, TimelineEvent … … 51 52 52 53 implements(IContentConverter, INavigationContributor, IPermissionRequestor, 53 54 IRequestHandler, ITimelineEventProvider, ISearchSource, 54 ITemplateProvider )55 ITemplateProvider, IHTMLPreviewAnnotator) 55 56 56 57 page_manipulators = ExtensionPoint(IWikiPageManipulator) 57 58 … … 531 532 'attachments': AttachmentModule(self.env).attachment_list(context), 532 533 'default_template': self.DEFAULT_PAGE_TEMPLATE, 533 534 '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")], 535 538 }) 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 536 550 return 'wiki_view.html', data, None 537 551 538 552 # ITimelineEventProvider methods … … 598 612 yield (req.href.wiki(name), '%s: %s' % (name, shorten_line(text)), 599 613 datetime.fromtimestamp(ts, utc), author, 600 614 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], 628 href=context.href.wiki(context.latest_page.name, version=linedata[lineno-1])), class_='blame')) -
trac/wiki/model.py
22 22 from trac.core import * 23 23 from trac.util.datefmt import utc, to_timestamp 24 24 from trac.util.translation import _ 25 from trac.util.blame import LineBlame 25 26 from trac.wiki.api import WikiSystem 26 27 27 28 28 class WikiPage(object): 29 29 """Represents a wiki page (new or existing).""" 30 30 … … 163 163 for version,ts,author,comment,ipnr in cursor: 164 164 time = datetime.fromtimestamp(ts, utc) 165 165 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
21 21 <h2>Wiki Navigation</h2> 22 22 <py:choose> 23 23 <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, 26 annotate='1'))("Annotate")])} 25 27 </py:when> 26 28 <ul py:otherwise=""> 27 29 <li><a href="${href.wiki('WikiStart')}">Start Page</a></li> 28 30 <li><a href="${href.wiki('TitleIndex')}">Index</a></li> 29 31 <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 35 href="${href.wiki(page.name)}">Normal</a></li> 30 36 <li class="last"> 31 37 <a href="${req.href.wiki(page.name, action='diff', version=page.version)}">Last Change</a> 32 38 </li> … … 52 58 53 59 <div class="wikipage searchable" py:choose="" xml:space="preserve"> 54 60 <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> 56 67 </py:when> 57 68 <py:otherwise> 58 69 Describe ${page.name} here. -
trac/util/blame.py
1 """ 2 Provide annotations for source code. Also known as a blame or praise report. 3 4 Extracted from http://timhatch.com/projects/pyannotate/ 5 """ 6 7 from difflib import SequenceMatcher 8 9 import 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 16 class 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 24 class 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 64 class 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 130 class 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 145 class 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
