Edgewall Software

Ticket #199: diff_module_alpha3.diff

File diff_module_alpha3.diff, 34.6 KB (added by cboos, 7 years ago)

Third iteration, added the UI for arbitrary diff (attachment is there now)

  • htdocs/css/browser.css

     
    4545#dirlist td.name a, #dirlist td.rev a { border-bottom: none; display: block } 
    4646#dirlist td.change * { font-size: 9px } 
    4747 
     48/* Log */ 
     49tr.diff input {  
     50 padding: 0 1em 0 1em; 
     51 margin: 0;  
     52} 
     53 
     54#anydiff { 
     55 background: #f7f7f0; 
     56 border: 1px outset #998; 
     57 margin: 0 1em 1em; 
     58 padding: .8em; 
     59 float: right; 
     60} 
     61#anydiff em { 
     62 background: #fbfbfb; 
     63} 
     64#anydiff form, #anydiff div { 
     65 vertical-align: top; 
     66 display: inline; 
     67 margin-right: 0; 
     68 margin-left: 0; 
     69} 
     70#anydiff input {  
     71 vertical-align: baseline; 
     72} 
     73 
    4874/* Styles for the revision log table 
    4975   (extends the styles for "table.listing") */ 
    5076#chglist { margin-top: 0 } 
  • trac/db_default.py

     
    458458 
    459459default_components = ('trac.About', 'trac.attachment', 'trac.Browser', 
    460460                      'trac.Changeset', 'trac.Search', 'trac.Settings', 
     461                      'trac.Diff', 
    461462                      'trac.ticket.query', 'trac.ticket.report', 
    462463                      'trac.Roadmap', 
    463464                      'trac.ticket.web_ui', 'trac.Timeline', 'trac.wiki.web_ui', 
  • trac/versioncontrol/svn_fs.py

     
    2727import os.path 
    2828import time 
    2929import weakref 
     30import posixpath 
    3031 
    3132from svn import fs, repos, core, delta 
    3233 
     
    175176    def __del__(self): 
    176177        self.close() 
    177178 
     179    def has_node(self, path, rev): 
     180        rev_root = fs.revision_root(self.fs_ptr, rev, self.pool) 
     181        node_type = fs.check_path(rev_root, path, self.pool) 
     182        return node_type in _kindmap 
     183 
    178184    def normalize_path(self, path): 
    179         return path == '/' and path or path.strip('/') 
     185        return path == '/' and path or path and path.strip('/') or '' 
    180186 
    181187    def normalize_rev(self, rev): 
    182188        try: 
     
    267273        rev = self.normalize_rev(rev) 
    268274        expect_deletion = False 
    269275        while rev: 
    270             rev_root = fs.revision_root(self.fs_ptr, rev, self.pool) 
    271             node_type = fs.check_path(rev_root, path, self.pool) 
    272             if node_type in _kindmap: # then path exists at that rev 
     276            if self.has_node(path, rev): 
    273277                if expect_deletion: 
    274278                    # it was missing, now it's there again: rev+1 must be a delete 
    275279                    yield path, rev+1, Changeset.DELETE 
     
    294298                expect_deletion = True 
    295299                rev = self.previous_rev(rev) 
    296300 
     301    def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 
     302        old_node = new_node = None 
     303        old_rev = self.normalize_rev(old_rev) 
     304        new_rev = self.normalize_rev(new_rev) 
     305        if self.has_node(old_path, old_rev): 
     306            old_node = self.get_node(old_path, old_rev) 
     307            old_path = old_node.created_path 
     308            old_rev = old_node.created_rev 
     309        if self.has_node(new_path, new_rev): 
     310            new_node = self.get_node(new_path, new_rev) 
     311            new_path = new_node.created_path 
     312            new_rev = new_node.created_rev 
     313        if not old_node and not new_node: 
     314            raise TracError, ('None of the diff arguments are valid: ' 
     315                              'neither %s in revision %s nor %s in revision %s exist ' 
     316                              'in the repository' % (old_path, old_rev, 
     317                                                     new_path, new_rev)) 
     318        elif old_node and new_node: 
     319            if new_node.kind != old_node.kind: 
     320                raise TracError, ('Diff mismatch: Trying to diff a %s (%s at %s) ' 
     321                                  'with a %s (%s at %s).' \ 
     322                                  % (old_node.kind, old_path, old_rev, 
     323                                     new_node.kind, new_path, new_rev)) 
     324        if new_node: 
     325            isdir = new_node.isdir 
     326        else: 
     327            isdir = old_node.isdir 
     328        if isdir: 
     329            old_base = old_path 
     330            new_base = new_path 
     331        else: 
     332            old_base = posixpath.split(old_path)[0] 
     333            new_base = posixpath.split(new_path)[0] 
    297334 
     335        editor = DiffChangeEditor() 
     336        e_ptr, e_baton = delta.make_editor(editor, self.pool) 
     337        old_root = fs.revision_root(self.fs_ptr, old_rev, self.pool) 
     338        new_root = fs.revision_root(self.fs_ptr, new_rev, self.pool) 
     339        if isdir: 
     340            old_dir, old_entry = old_path, '' 
     341        else: 
     342            old_dir, old_entry = posixpath.split(old_path) 
     343        def authz_cb(root, path, pool): return 1 
     344        text_deltas = 0 # as this is currently re-done in Diff.py... 
     345        entry_props = 0 # ("... typically used only for working copy updates") 
     346        repos.svn_repos_dir_delta(old_root, 
     347                                  old_dir, old_entry, 
     348                                  new_root, new_path, 
     349                                  e_ptr, e_baton, authz_cb, 
     350                                  text_deltas, 
     351                                  isdir and 1 or 0, 
     352                                  entry_props, 
     353                                  ignore_ancestry, 
     354                                  self.pool) 
     355        for d in editor.deltas: 
     356            yield (posixpath.join(old_base,d[0]), posixpath.join(new_base,d[0]), 
     357                   d[1], d[2]) 
     358 
     359 
    298360class SubversionNode(Node): 
    299361 
    300362    pool = property(fget=lambda self: self._pool(), 
     
    450512 
    451513    def _get_prop(self, name): 
    452514        return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool) 
     515 
     516 
     517# 
     518# Delta editor for diffs between arbitrary nodes (recycling my old code for #295 :) ) 
     519# 
     520# Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used 
     521#         because 'repos.svn_repos_dir_delta' *doesn't* provide it. 
     522# 
     523# Note 2: the 'dir_baton' is the path of the parent directory 
     524# 
     525 
     526class DiffChangeEditor(delta.Editor):  
     527 
     528    def __init__(self): 
     529        self.deltas = [] 
     530        self.skip_dir_prop_change = 0 
     531 
     532    def _norm(self, path): 
     533        """Path are normalized to __not__ have a leading slash""" 
     534        return path == '/' and path or path and path.strip('/') or '' 
     535     
     536    # -- svn.delta.Editor callbacks 
     537 
     538    def open_root(self, base_revision, dir_pool): 
     539        return '/' 
     540 
     541    def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, dir_pool): 
     542        self.deltas.append((path, Node.DIRECTORY, Changeset.ADD)) 
     543        # don't create an additional 'Changeset.EDIT' entry for this directory 
     544        # in case there's also a dir property change: 
     545        self.skip_dir_prop_change = 1  
     546        return path 
     547 
     548    def open_directory(self, path, dir_baton, base_revision, dir_pool): 
     549        self.deltas.append((path, Node.DIRECTORY, Changeset.EDIT)) 
     550        self.skip_dir_prop_change = 0 
     551        return path 
     552 
     553    def change_dir_prop(self, dir_baton, name, value, pool): 
     554        if self.skip_dir_prop_change: 
     555            return 
     556        self.deltas.append((dir_baton, Node.DIRECTORY, Changeset.EDIT)) 
     557        self.skip_dir_prop_change = 1 
     558 
     559    def close_directory(self, dir_baton): 
     560        self.skip_dir_prop_change = 0 
     561 
     562    def delete_entry(self, path, revision, dir_baton, pool): 
     563        self.deltas.append((path, Node.FILE, Changeset.DELETE)) # should be Node.UNKNOWN 
     564 
     565    def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, dir_pool): 
     566        self.deltas.append((self._norm(path), Node.FILE, Changeset.ADD)) 
     567 
     568    def open_file(self, path, dir_baton, dummy_rev, file_pool): 
     569        self.deltas.append((self._norm(path), Node.FILE, Changeset.EDIT)) 
     570 
  • trac/versioncontrol/cache.py

     
    8888    def get_node(self, path, rev=None): 
    8989        return self.repos.get_node(path, rev) 
    9090 
     91    def has_node(self, path, rev): 
     92        return self.repos.has_node(path, rev) 
     93 
    9194    def get_oldest_rev(self): 
    9295        return self.repos.oldest_rev 
    9396 
     
    112115    def normalize_rev(self, rev): 
    113116        return self.repos.normalize_rev(rev) 
    114117 
     118    def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 
     119        return self.repos.get_diffs(old_path, old_rev, new_path, new_rev, ignore_ancestry) 
    115120 
     121 
    116122class CachedChangeset(Changeset): 
    117123 
    118124    def __init__(self, rev, db, authz): 
  • trac/versioncontrol/main.py

     
    4242        """ 
    4343        raise NotImplementedError 
    4444 
     45    def has_node(self, path, rev): 
     46        """ 
     47        Tell if there's a node at the specified (path,rev) combination. 
     48        """ 
     49        raise NotImplementedError 
     50     
    4551    def get_node(self, path, rev=None): 
    4652        """ 
    4753        Retrieve a Node (directory or file) from the repository at the 
     
    114120        'None' is a valid revision value and represents the youngest revision. 
    115121        """ 
    116122        return NotImplementedError 
    117          
    118123 
     124    def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 
     125        """ 
     126        Generator that yields (old_path, new_path, kind, change) tuples 
     127        for each node change between the two arbitrary (path,rev) pairs. 
     128        """ 
     129        raise NotImplementedError 
     130 
     131 
    119132class Node(object): 
    120133    """ 
    121134    Represents a directory or file in the repository. 
  • trac/Diff.py

     
    2424from __future__ import generators 
    2525import time 
    2626import re 
     27import posixpath 
    2728 
    2829from trac import mimeview, perm, util 
    2930from trac.core import * 
    30 from trac.Timeline import ITimelineEventProvider 
    3131from trac.versioncontrol import Changeset, Node 
    3232from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff 
    3333from trac.web.chrome import add_link, add_stylesheet 
     
    3535from trac.wiki import wiki_to_html, wiki_to_oneliner 
    3636 
    3737 
    38 class ChangesetModule(Component): 
     38class Diff(dict): 
     39    def __getattr__(self,str): 
     40        return self[str] 
     41     
    3942 
    40     implements(IRequestHandler, ITimelineEventProvider) 
     43class DiffModule(Component): 
    4144 
     45    implements(IRequestHandler) 
     46 
    4247    # IRequestHandler methods 
    4348 
    4449    def match_request(self, req): 
    45         match = re.match(r'/changeset/([0-9]+)$', req.path_info) 
     50        match = re.match(r'/diff(?:(/.*)|$)', req.path_info) 
    4651        if match: 
    47             req.args['rev'] = match.group(1) 
     52            req.args['path'] = match.group(1) 
    4853            return 1 
    4954 
    5055    def process_request(self, req): 
    5156        req.perm.assert_permission(perm.CHANGESET_VIEW) 
    5257 
    53         rev = req.args.get('rev') 
     58        path = req.args.get('path') 
    5459        repos = self.env.get_repository(req.authname) 
     60        path = repos.normalize_path(path) 
     61        rev = req.args.get('rev', repos.youngest_rev) # 'path history' mode 
     62        old = req.args.get('old')                     # 'arbitrary diff' mode 
     63        new = req.args.get('new') 
     64        old_path = req.args.get('old_path', path) 
     65        if old_path == path and old == new: # force 'path history' mode 
     66            rev = old 
     67            old_path = old = new = None 
    5568 
    5669        diff_options = get_diff_options(req) 
    5770        if req.args.has_key('update'): 
    58             req.redirect(self.env.href.changeset(rev)) 
     71            if old or new: 
     72                req.redirect(self.env.href.diff(path, new=new, old_path=old_path, old=old)) 
     73            else: 
     74                req.redirect(self.env.href.diff(path, rev=rev)) 
    5975 
    60         chgset = repos.get_changeset(rev) 
    61         req.check_modified(chgset.date, 
    62                            diff_options[0] + ''.join(diff_options[1])) 
     76        if old or new: 
     77            chgset = None 
     78            if not new: 
     79                new = repos.next_rev(old) # FIXME: must lookup the next entry in node history 
     80            elif not old: 
     81                old = repos.previous_rev(new) 
     82            if not old_path: 
     83                old_path = path 
     84            diff = Diff(old_path=old_path, old_rev=old, 
     85                        new_path=path, new_rev=new) 
     86        else: 
     87            chgset = repos.get_changeset(rev) 
     88            diff = Diff(old_path=path, old_rev=repos.previous_rev(rev), 
     89                        new_path=path, new_rev=rev) 
     90         
     91        # TODO: 
     92#         req.check_modified(chgset.date, 
     93#                            diff_options[0] + ''.join(diff_options[1])) 
    6394 
    6495        format = req.args.get('format') 
    6596        if format == 'diff': 
    66             self._render_diff(req, repos, chgset, diff_options) 
     97            self._render_diff(req, repos, diff, chgset, diff_options) 
    6798            return 
    6899        elif format == 'zip': 
    69             self._render_zip(req, repos, chgset) 
     100            self._render_zip(req, repos, diff, chgset) 
    70101            return 
    71102 
    72         self._render_html(req, repos, chgset, diff_options) 
     103        self._render_html(req, repos, diff, chgset, diff_options) 
    73104        add_link(req, 'alternate', '?format=diff', 'Unified Diff', 
    74105                 'text/plain', 'diff') 
    75106        add_link(req, 'alternate', '?format=zip', 'Zip Archive', 
    76107                 'application/zip', 'zip') 
    77108        add_stylesheet(req, 'changeset.css') 
    78109        add_stylesheet(req, 'diff.css') 
    79         return 'changeset.cs', None 
     110        return 'diff.cs', None 
    80111 
    81     # ITimelineEventProvider methods 
    82112 
    83     def get_timeline_filters(self, req): 
    84         if req.perm.has_permission(perm.CHANGESET_VIEW): 
    85             yield ('changeset', 'Repository checkins') 
    86  
    87     def get_timeline_events(self, req, start, stop, filters): 
    88         if 'changeset' in filters: 
    89             absurls = req.args.get('format') == 'rss' # Kludge 
    90             show_files = int(self.config.get('timeline', 
    91                                              'changeset_show_files')) 
    92             db = self.env.get_db_cnx() 
    93             repos = self.env.get_repository() 
    94             rev = repos.youngest_rev 
    95             while rev: 
    96                 chgset = repos.get_changeset(rev) 
    97                 if chgset.date < start: 
    98                     return 
    99                 if chgset.date < stop: 
    100                     if absurls: 
    101                         href = self.env.abs_href.changeset(chgset.rev) 
    102                     else: 
    103                         href = self.env.href.changeset(chgset.rev) 
    104                     title = 'Changeset <em>[%s]</em> by %s' % ( 
    105                             util.escape(chgset.rev), util.escape(chgset.author)) 
    106                     message = wiki_to_oneliner(util.shorten_line(chgset.message or '--'), 
    107                                                self.env, db, absurls=absurls) 
    108                     if show_files: 
    109                         files = [] 
    110                         for chg in chgset.get_changes(): 
    111                             if show_files > 0 and len(files) >= show_files: 
    112                                 files.append('...') 
    113                                 break 
    114                             files.append('<span class="%s">%s</span>' 
    115                                          % (chg[2], util.escape(chg[0]))) 
    116                         message = '<span class="changes">' + ', '.join(files) +\ 
    117                                   '</span>: ' + message 
    118                     yield 'changeset', href, title, chgset.date, chgset.author,\ 
    119                           message 
    120                 rev = repos.previous_rev(rev) 
    121  
    122113    # Internal methods 
    123114 
    124     def _render_html(self, req, repos, chgset, diff_options): 
     115    def _render_html(self, req, repos, diff, chgset, diff_options): 
    125116        """HTML version""" 
    126         req.hdf['title'] = '[%s]' % chgset.rev 
    127         req.hdf['changeset'] = { 
    128             'revision': chgset.rev, 
    129             'time': time.strftime('%c', time.localtime(chgset.date)), 
    130             'author': util.escape(chgset.author or 'anonymous'), 
    131             'message': wiki_to_html(chgset.message or '--', self.env, req, 
    132                                     escape_newlines=True) 
    133         } 
     117        req.hdf['diff'] = diff 
     118        req.hdf['diff.href'] = { 
     119            'new_rev': self.env.href.changeset(diff.new_rev), 
     120            'old_rev': self.env.href.changeset(diff.old_rev), 
     121            'new_path': self.env.href.browser(diff.new_path, rev=diff.new_rev), 
     122            'old_path': self.env.href.changeset(diff.old_path, rev=diff.old_rev) 
     123            } 
     124        if chgset: # 'path history' mode 
     125            req.hdf['title'] = 'Changes for %s at Revision %s' % (diff.new_path, chgset.rev) 
     126            req.hdf['changeset'] = { 
     127                'revision': chgset.rev, 
     128                'time': time.strftime('%c', time.localtime(chgset.date)), 
     129                'author': util.escape(chgset.author or 'anonymous'), 
     130                'message': wiki_to_html(chgset.message or '--', self.env, req, 
     131                                        escape_newlines=True) 
     132                } 
    134133 
    135         oldest_rev = repos.oldest_rev 
    136         if chgset.rev != oldest_rev: 
    137             add_link(req, 'first', self.env.href.changeset(oldest_rev), 
    138                      'Changeset %s' % oldest_rev) 
    139             previous_rev = repos.previous_rev(chgset.rev) 
    140             add_link(req, 'prev', self.env.href.changeset(previous_rev), 
    141                      'Changeset %s' % previous_rev) 
    142         youngest_rev = repos.youngest_rev 
    143         if str(chgset.rev) != str(youngest_rev): 
    144             next_rev = repos.next_rev(chgset.rev) 
    145             add_link(req, 'next', self.env.href.changeset(next_rev), 
    146                      'Changeset %s' % next_rev) 
    147             add_link(req, 'last', self.env.href.changeset(youngest_rev), 
    148                      'Changeset %s' % youngest_rev) 
     134            oldest_rev = repos.oldest_rev 
     135            if chgset.rev != oldest_rev: 
     136                add_link(req, 'first', self.env.href.diff(diff.old_path, rev=oldest_rev), 
     137                         'Changeset %s' % oldest_rev) # FIXME (use the history) 
     138                previous_rev = repos.previous_rev(chgset.rev) 
     139                add_link(req, 'prev', self.env.href.diff(diff.old_path, rev=previous_rev), 
     140                         'Changeset %s' % previous_rev) 
     141            youngest_rev = repos.youngest_rev 
     142            if str(chgset.rev) != str(youngest_rev): 
     143                next_rev = repos.next_rev(chgset.rev) 
     144                add_link(req, 'next', self.env.href.diff(diff.new_path, rev=next_rev), 
     145                         'Changeset %s' % next_rev) 
     146                add_link(req, 'last', self.env.href.diff(diff.new_path, rev=youngest_rev), 
     147                         'Changeset %s' % youngest_rev) 
     148        elif diff.new_path == diff.old_path: # 'diff between 2 revisions' mode 
     149            req.hdf['title'] = 'Diff for %s between Revisions %s and %s' \ 
     150                               % (diff.new_path, diff.old_rev, diff.new_rev) 
     151        else:                           # 'arbitrary diff' mode 
     152            req.hdf['title'] = 'Diff between %s at Revision %s and %s at Revision %s' \ 
     153                               % (diff.old_path, diff.old_rev, 
     154                                  diff.new_path, diff.new_rev) 
    149155 
    150156        edits = [] 
    151157        idx = 0 
    152         for path, kind, change, base_path, base_rev in chgset.get_changes(): 
     158        old_rev = diff.old_rev 
     159        new_rev = diff.new_rev 
     160        for old_path, new_path, kind, change in repos.get_diffs(**diff): 
     161            print 'delta %d: %s %s delta from %s@%s to %s@%s' \ 
     162                  % (idx, change, kind, old_path, old_rev, new_path, new_rev) 
    153163            info = {'change': change} 
    154             if base_path: 
    155                 info['path.old'] = base_path 
    156                 info['rev.old'] = base_rev 
    157                 info['browser_href.old'] = self.env.href.browser(base_path, 
    158                                                                  rev=base_rev) 
    159             if path: 
    160                 info['path.new'] = path 
    161                 info['rev.new'] = chgset.rev 
    162                 info['browser_href.new'] = self.env.href.browser(path, 
    163                                                                  rev=chgset.rev) 
     164            if old_path: 
     165                info['path.old'] = old_path 
     166                info['rev.old'] = old_rev 
     167                info['browser_href.old'] = self.env.href.browser(old_path, 
     168                                                                 rev=old_rev) 
     169            if new_path: 
     170                info['path.new'] = new_path 
     171                info['rev.new'] = new_rev 
     172                info['browser_href.new'] = self.env.href.browser(new_path, 
     173                                                                 rev=new_rev) 
    164174            if change in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE): 
    165                 edits.append((idx, path, kind, base_path, base_rev)) 
    166             req.hdf['changeset.changes.%d' % idx] = info 
     175                edits.append((idx, old_path, new_path, kind)) 
     176            req.hdf['diff.changes.%d' % idx] = info 
    167177            idx += 1 
    168  
    169         for idx, path, kind, base_path, base_rev in edits: 
    170             old_node = repos.get_node(base_path or path, base_rev) 
    171             new_node = repos.get_node(path, chgset.rev) 
    172  
     178         
     179        for idx, old_path, new_path, kind in edits: 
     180            old_node = repos.get_node(old_path, old_rev) 
     181            new_node = repos.get_node(new_path, new_rev) 
     182             
    173183            # Property changes 
    174184            old_props = old_node.get_properties() 
    175185            new_props = new_node.get_properties() 
     
    183193                for k,v in new_props.items(): 
    184194                    if not k in old_props: 
    185195                        changed_props[k] = {'new': v} 
    186                 req.hdf['changeset.changes.%d.props' % idx] = changed_props 
     196                req.hdf['diff.changes.%d.props' % idx] = changed_props 
    187197 
    188198            if kind == Node.DIRECTORY: 
    189199                continue 
    190200 
    191201            # Content changes 
    192202            default_charset = self.config.get('trac', 'default_charset') 
    193             old_content = old_node.get_content().read() 
     203            old_content = old_node.get_content().read()             
    194204            if mimeview.is_binary(old_content): 
    195205                continue 
    196206            charset = mimeview.get_charset(old_node.content_type) or \ 
     
    217227                                   ignore_blank_lines='-B' in diff_options[1], 
    218228                                   ignore_case='-i' in diff_options[1], 
    219229                                   ignore_space_changes='-b' in diff_options[1]) 
    220                 req.hdf['changeset.changes.%d.diff' % idx] = changes 
     230                req.hdf['diff.changes.%d.diff' % idx] = changes 
    221231 
    222     def _render_diff(self, req, repos, chgset, diff_options): 
     232 
     233    def _render_diff(self, req, repos, chgset, diff_options): # TODO 
    223234        """Raw Unified Diff version""" 
    224235        req.send_response(200) 
    225236        req.send_header('Content-Type', 'text/plain;charset=utf-8') 
     
    284295                                         ignore_space_changes='-b' in diff_options[1]): 
    285296                    req.write(line + util.CRLF) 
    286297 
    287     def _render_zip(self, req, repos, chgset): 
     298    def _render_zip(self, req, repos, chgset): # TODO 
    288299        """ZIP archive with all the added and/or modified files.""" 
    289300        req.send_response(200) 
    290301        req.send_header('Content-Type', 'application/zip') 
  • trac/Browser.py

     
    113113 
    114114        req.hdf['title'] = path 
    115115        req.hdf['browser'] = { 
    116             'path': path, 
    117             'revision': rev or repos.youngest_rev, 
     116            'path': node.path, 
     117            'revision': node.rev, 
    118118            'props': dict([(util.escape(name), util.escape(value)) 
    119119                           for name, value in node.get_properties().items()]), 
    120             'href': self.env.href.browser(path,rev=rev or repos.youngest_rev), 
    121             'log_href': self.env.href.log(path) 
     120            'href': self.env.href.browser(node.path,rev=node.rev), 
     121            'diff_href': self.env.href.diff(node.path,rev=node.rev), 
     122            'log_href': self.env.href.log(node.path) 
    122123        } 
    123124 
    124125        path_links = _get_path_links(self.env.href, path, rev) 
     
    268269        stop_rev = req.args.get('stop_rev') 
    269270        verbose = req.args.get('verbose') 
    270271        limit = int(req.args.get('limit') or 100) 
     272        old = req.args.get('old') 
     273        new = req.args.get('new') 
     274        select_for_diff = req.args.get('diff') 
    271275 
     276        repos = self.env.get_repository(req.authname) 
     277        normpath = repos.normalize_path(path) 
     278        rev = str(repos.normalize_rev(rev)) 
     279        old = old or str(repos.previous_rev(rev)) 
     280        new = new or rev 
     281 
    272282        req.hdf['title'] = path + ' (log)' 
    273283        req.hdf['log'] = { 
    274284            'path': path, 
     
    276286            'verbose': verbose, 
    277287            'stop_rev': stop_rev, 
    278288            'browser_href': self.env.href.browser(path, rev=rev), 
    279             'log_href': self.env.href.log(path, rev=rev) 
     289            'log_href': self.env.href.log(path, rev=rev), 
     290            'diff_href': self.env.href.diff(path, old=old, new=new), 
     291            'old': old, 
     292            'new': new 
    280293        } 
    281294 
     295        if select_for_diff == "1": 
     296            req.session['diff_base_path'] = path 
     297            req.session['diff_base_rev'] = rev 
     298 
     299        if req.session.has_key('diff_base_path'): 
     300            if select_for_diff == "0": 
     301                del req.session['diff_base_path'] 
     302                del req.session['diff_base_rev'] 
     303            anydiff_href = self.env.href.diff(path,new=rev, 
     304                                              old_path=req.session['diff_base_path'], 
     305                                              old=req.session['diff_base_rev']) 
     306            req.hdf['log.anydiff_href'] = anydiff_href 
     307            req.hdf['session'] = { 
     308                'diff_base_path': req.session['diff_base_path'], 
     309                'diff_base_rev': req.session['diff_base_rev'] 
     310                } 
     311 
    282312        path_links = _get_path_links(self.env.href, path, rev) 
    283313        req.hdf['log.path'] = path_links 
    284314        if path_links: 
    285315            add_link(req, 'up', path_links[-1]['href'], 'Parent directory') 
    286316 
    287         repos = self.env.get_repository(req.authname) 
    288         normpath = repos.normalize_path(path) 
    289         rev = str(repos.normalize_rev(rev)) 
    290  
    291317        # 'node' or 'path' history: use get_node()/get_history() or get_path_history() 
    292318        if mode != 'path_history': 
    293319            try: 
  • templates/log.cs

     
    33 
    44<div id="ctxtnav" class="nav"> 
    55 <ul> 
    6   <li class="last"><a href="<?cs 
    7     var:log.browser_href ?>">View Latest Revision</a></li><?cs 
     6  <li class="last"> 
     7   <a href="<?cs var:log.browser_href ?>">View Latest Revision</a> 
     8  </li><?cs 
    89  if:len(chrome.links.prev) ?> 
    910   <li class="first<?cs if:!len(chrome.links.next) ?> last<?cs /if ?>"> 
    1011    &larr; <a href="<?cs var:chrome.links.prev.0.href ?>" title="<?cs 
     
    7475   </dl> 
    7576  </div> 
    7677 </div> 
     78 
     79 <div id="anydiff"><?cs 
     80  if:session.diff_base_path == log.path && session.diff_base_rev == log.rev ?> 
     81   <em>This Path/Revision is the Base for Diff</em><?cs 
     82  else ?><?cs 
     83   if:session.diff_base_path ?> 
     84     <form action="<?cs var:log.diff_href ?>" method="get"> 
     85      <input type="hidden" name="old_path" value="<?cs var:session.diff_base_path ?>" /> 
     86      <input type="hidden" name="old" value="<?cs var:session.diff_base_rev ?>" /> 
     87      <input type="hidden" name="new" value="<?cs var:log.rev ?>" /> 
     88      <div class="buttons"> 
     89       <input type="submit" value="Diff"  
     90              title="Diff the current Path/Revision against the selected Base" /> 
     91      </div> 
     92     </form> 
     93    against: <em><?cs var:session.diff_base_path ?></em>  
     94     in revision <em><?cs var:session.diff_base_rev ?></em> 
     95    <form action="<?cs var:log.log_href ?>" method="get"> 
     96     <input type="hidden" name="diff" value="0" /> 
     97     <div class="buttons"> 
     98      <input type="submit" value="Clear"  
     99       title="Clear the Base for Diff" /> 
     100     </div> 
     101    </form><?cs 
     102   /if ?> 
     103   <form action="<?cs var:log.log_href ?>" method="get"> 
     104    <input type="hidden" name="diff" value="1" /> 
     105    <div class="buttons"> 
     106     <input type="submit"  
     107       value="Set<?cs if:!session.diff_base_path ?>  Base for Diff<?cs /if ?>" 
     108      title="Select the current Path/Revision as the new Base for Diff" /> 
     109    </div> 
     110   </form><?cs 
     111  /if ?> 
     112 </div> 
     113 
    77114 <table id="chglist" class="listing"> 
     115  <form action="<?cs var:log.diff_href ?>" method="get"> 
    78116  <thead> 
     117   <tr class="diff"> 
     118    <th colspan="2"> 
     119     <div class="buttons"><input type="submit" value="Diff"  
     120       title="Diff from Old Revision to New Revision (select them below)" /></div> 
     121    </th> 
     122   </tr> 
    79123   <tr> 
     124    <th>Old</th> 
     125    <th>New</th> 
    80126    <th class="change"></th> 
    81127    <th class="data">Date</th> 
    82128    <th class="rev">Rev</th> 
     
    87133  </thead> 
    88134  <tbody><?cs 
    89135   set:indent = #1 ?><?cs 
     136   set:idx = #0 ?><?cs 
    90137   each:item = log.items ?><?cs 
    91138    if:item.copyfrom_path ?> 
    92139     <tr class="<?cs if:name(item) % #2 ?>even<?cs else ?>odd<?cs /if ?>"> 
     
    99146      set:indent = #1 ?><?cs 
    100147    /if ?> 
    101148    <tr class="<?cs if:name(item) % #2 ?>even<?cs else ?>odd<?cs /if ?>"> 
     149     <td><input type="radio" name="old" value="<?cs var:item.rev ?>" <?cs 
     150          if:idx == #1 ?> checked="checked" <?cs /if ?> /></td> 
     151     <td><input type="radio" name="new" value="<?cs var:item.rev ?>" <?cs 
     152          if:idx == #0 ?> checked="checked" <?cs /if ?> /></td> 
    102153     <td class="change" style="padding-left:<?cs var:indent ?>em"> 
    103154      <a title="View log starting at this revision" href="<?cs var:item.log_href ?>"> 
    104155       <div class="<?cs var:item.change ?>"></div> 
     
    115166     <td class="author"><?cs var:log.changes[item.rev].author ?></td> 
    116167     <td class="summary"><?cs var:log.changes[item.rev].message ?></td> 
    117168    </tr><?cs 
     169    set:idx = idx + 1 ?><?cs 
    118170   /each ?> 
    119171  </tbody> 
     172  </form> 
    120173 </table><?cs 
    121174 if:len(links.prev) || len(links.next) ?><div id="paging" class="nav"><ul><?cs 
    122175  if:len(links.prev) ?><li class="first<?cs 
  • templates/browser.cs

     
    33 
    44<div id="ctxtnav" class="nav"> 
    55 <ul> 
     6  <li class="first"><a href="<?cs var:browser.diff_href ?>">Diff to previous</a></li> 
    67  <li class="last"><a href="<?cs var:browser.log_href ?>">Revision Log</a></li> 
    78 </ul> 
    89</div> 
  • templates/diff.cs

     
    22<?cs include "macros.cs"?> 
    33 
    44<div id="ctxtnav" class="nav"> 
    5  <h2>Changeset Navigation</h2><?cs 
     5 <h2>Diff Navigation</h2><?cs 
    66 with:links = chrome.links ?> 
    77  <ul><?cs 
    88   if:len(links.prev) ?> 
    99    <li class="first<?cs if:!len(links.next) ?> last<?cs /if ?>"> 
    1010     <a class="prev" href="<?cs var:links.prev.0.href ?>" title="<?cs 
    11        var:links.prev.0.title ?>">Previous Changeset</a> 
     11       var:links.prev.0.title ?>">Previous Diff</a> 
    1212    </li><?cs 
    1313   /if ?><?cs 
    1414   if:len(links.next) ?> 
    1515    <li class="<?cs if:len(links.prev) ?>first <?cs /if ?>last"> 
    1616     <a class="next" href="<?cs var:links.next.0.href ?>" title="<?cs 
    17        var:links.next.0.title ?>">Next Changeset</a> 
     17       var:links.next.0.title ?>">Next Diff</a> 
    1818    </li><?cs 
    1919   /if ?> 
    2020  </ul><?cs 
     
    2222</div> 
    2323 
    2424<div id="content" class="changeset"> 
    25 <h1>Changeset <?cs var:changeset.revision ?></h1> 
     25<h1><?cs 
     26 if:len(changeset) > #0 ?> 
     27  Changes for <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>"> 
     28   <?cs var:diff.new_path ?></a>  
     29  in Revision <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>"> 
     30   <?cs var:diff.new_rev ?></a><?cs 
     31 elif:diff.new_path == diff.old_path ?> 
     32  Differences for <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>"> 
     33   <?cs var:diff.new_path ?></a>  
     34  between Revisions <a title="Show full changeset" href="<?cs var:diff.href.old_rev ?>"> 
     35   <?cs var:diff.old_rev ?></a> 
     36  and <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>"> 
     37   <?cs var:diff.new_rev ?></a><?cs 
     38 else ?> 
     39  Differences between <a title="Show entry in browser" href="<?cs var:diff.href.old_path ?>"> 
     40   <?cs var:diff.old_path ?></a>  
     41  at Revision <a title="Show full changeset" href="<?cs var:diff.href.old_rev ?>"> 
     42   <?cs var:diff.old_rev ?></a> 
     43  and <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>"> 
     44   <?cs var:diff.new_path ?></a>  
     45  at Revision <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>"> 
     46   <?cs var:diff.new_rev ?></a><?cs 
     47 /if ?> 
     48</h1> 
    2649 
    27 <?cs each:change = changeset.changes ?><?cs 
     50<?cs each:change = diff.changes ?><?cs 
    2851 if:len(change.diff) ?><?cs 
    2952  set:has_diffs = 1 ?><?cs 
    3053 /if ?><?cs 
     
    3255  || diff.options.ignorecase || diff.options.ignorewhitespace ?> 
    3356<form method="post" id="prefs" action=""> 
    3457 <div> 
     58  <input type="hidden" name="old_path" value="<?cs var:diff.old_path ?>" /> 
     59  <input type="hidden" name="old" value="<?cs var:diff.old_rev ?>" /> 
     60  <input type="hidden" name="new" value="<?cs var:diff.new_rev ?>" /> 
    3561  <label for="style">View differences</label> 
    3662  <select id="style" name="style"> 
    3763   <option value="inline"<?cs 
     
    100126  /if ?> 
    101127<?cs /def ?> 
    102128 
    103 <dl id="overview"> 
     129<dl id="overview"><?cs 
     130 if:len(changeset) > #0 ?> 
    104131 <dt class="time">Timestamp:</dt> 
    105132 <dd class="time"><?cs var:changeset.time ?></dd> 
    106133 <dt class="author">Author:</dt> 
    107134 <dd class="author"><?cs var:changeset.author ?></dd> 
    108135 <dt class="message">Message:</dt> 
    109  <dd class="message" id="searchable"><?cs var:changeset.message ?></dd> 
     136 <dd class="message" id="searchable"><?cs var:changeset.message ?></dd><?cs 
     137 /if ?> 
    110138 <dt class="files">Files:</dt> 
    111139 <dd class="files"> 
    112   <ul><?cs each:item = changeset.changes ?> 
     140  <ul><?cs each:item = diff.changes ?> 
    113141   <li><?cs 
    114142    if:item.change == 'add' ?><?cs 
    115143     call:node_change(item, 'add', 'added') ?><?cs 
     
    140168  </dl> 
    141169 </div> 
    142170 <ul class="entries"><?cs 
    143  each:item = changeset.changes ?><?cs 
     171 each:item = diff.changes ?><?cs 
    144172  if:len(item.diff) || len(item.props) ?><li class="entry" id="file<?cs 
    145173   var:name(item) ?>"><h2><a href="<?cs 
    146174   var:item.browser_href.new ?>" title="Show new revision <?cs