Index: htdocs/css/browser.css
===================================================================
--- htdocs/css/browser.css	(revision 1756)
+++ htdocs/css/browser.css	(working copy)
@@ -45,6 +45,32 @@
 #dirlist td.name a, #dirlist td.rev a { border-bottom: none; display: block }
 #dirlist td.change * { font-size: 9px }
 
+/* Log */
+tr.diff input { 
+ padding: 0 1em 0 1em;
+ margin: 0; 
+}
+
+#anydiff {
+ background: #f7f7f0;
+ border: 1px outset #998;
+ margin: 0 1em 1em;
+ padding: .8em;
+ float: left;
+}
+#anydiff em {
+ background: #fbfbfb;
+}
+#anydiff form, #anydiff div {
+ vertical-align: top;
+ display: inline;
+ margin-right: 0;
+ margin-left: 0;
+}
+#anydiff input { 
+ vertical-align: baseline;
+}
+
 /* Styles for the revision log table
    (extends the styles for "table.listing") */
 #chglist { margin-top: 0 }
Index: trac/db_default.py
===================================================================
--- trac/db_default.py	(revision 1756)
+++ trac/db_default.py	(working copy)
@@ -458,6 +458,7 @@
 
 default_components = ('trac.About', 'trac.attachment', 'trac.Browser',
                       'trac.Changeset', 'trac.Search', 'trac.Settings',
+                      'trac.Diff',
                       'trac.ticket.query', 'trac.ticket.report',
                       'trac.Roadmap',
                       'trac.ticket.web_ui', 'trac.Timeline', 'trac.wiki.web_ui',
Index: trac/versioncontrol/svn_fs.py
===================================================================
--- trac/versioncontrol/svn_fs.py	(revision 1756)
+++ trac/versioncontrol/svn_fs.py	(working copy)
@@ -27,6 +27,7 @@
 import os.path
 import time
 import weakref
+import posixpath
 
 from svn import fs, repos, core, delta
 
@@ -175,8 +176,13 @@
     def __del__(self):
         self.close()
 
+    def has_node(self, path, rev):
+        rev_root = fs.revision_root(self.fs_ptr, rev, self.pool)
+        node_type = fs.check_path(rev_root, path, self.pool)
+        return node_type in _kindmap
+
     def normalize_path(self, path):
-        return path == '/' and path or path.strip('/')
+        return path == '/' and path or path and path.strip('/') or ''
 
     def normalize_rev(self, rev):
         try:
@@ -267,9 +273,7 @@
         rev = self.normalize_rev(rev)
         expect_deletion = False
         while rev:
-            rev_root = fs.revision_root(self.fs_ptr, rev, self.pool)
-            node_type = fs.check_path(rev_root, path, self.pool)
-            if node_type in _kindmap: # then path exists at that rev
+            if self.has_node(path, rev):
                 if expect_deletion:
                     # it was missing, now it's there again: rev+1 must be a delete
                     yield path, rev+1, Changeset.DELETE
@@ -294,7 +298,66 @@
                 expect_deletion = True
                 rev = self.previous_rev(rev)
 
+    def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
+        old_node = new_node = None
+        old_rev = self.normalize_rev(old_rev)
+        new_rev = self.normalize_rev(new_rev)
+        if self.has_node(old_path, old_rev):
+            old_node = self.get_node(old_path, old_rev)
+            old_path = old_node.created_path
+            old_rev = old_node.created_rev
+        if self.has_node(new_path, new_rev):
+            new_node = self.get_node(new_path, new_rev)
+            new_path = new_node.created_path
+            new_rev = new_node.created_rev
+        if not old_node and not new_node:
+            raise TracError, ('None of the diff arguments are valid: '
+                              'neither %s in revision %s nor %s in revision %s exist '
+                              'in the repository' % (old_path, old_rev,
+                                                     new_path, new_rev))
+        elif old_node and new_node:
+            if new_node.kind != old_node.kind:
+                raise TracError, ('Diff mismatch: Trying to diff '
+                                  'a %s (%s in revision %s) '
+                                  'with a %s (%s in revision %s).' \
+                                  % (old_node.kind, old_path, old_rev,
+                                     new_node.kind, new_path, new_rev))
+        if new_node:
+            isdir = new_node.isdir
+        else:
+            isdir = old_node.isdir
+        if isdir:
+            editor = DiffChangeEditor()
+            e_ptr, e_baton = delta.make_editor(editor, self.pool)
+            old_root = fs.revision_root(self.fs_ptr, old_rev, self.pool)
+            new_root = fs.revision_root(self.fs_ptr, new_rev, self.pool)
+            if isdir:
+                old_dir, old_entry = old_path, ''
+            def authz_cb(root, path, pool): return 1
+            text_deltas = 0 # as this is currently re-done in Diff.py...
+            entry_props = 0 # ("... typically used only for working copy updates")
+            print 'svn_repos_dir_delta: ', old_dir, old_entry, ' -vs.- ', new_path
+            repos.svn_repos_dir_delta(old_root, old_path, '',
+                                      new_root, new_path,
+                                      e_ptr, e_baton, authz_cb,
+                                      text_deltas,
+                                      isdir and 1 or 0,
+                                      entry_props,
+                                      ignore_ancestry,
+                                      self.pool)
+            for d in editor.deltas:
+                yield (posixpath.join(old_path,d[0]), posixpath.join(new_path,d[0]),
+                       d[1], d[2])
+        else:
+            if new_node and old_node:
+                change = Changeset.EDIT
+            elif new_node:
+                change = Changeset.ADD
+            elif old_node:
+                change = Changeset.DELETE
+            yield (old_path, new_path, Node.FILE, change)
 
+
 class SubversionNode(Node):
 
     pool = property(fget=lambda self: self._pool(),
@@ -450,3 +513,59 @@
 
     def _get_prop(self, name):
         return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool)
+
+
+#
+# Delta editor for diffs between arbitrary nodes (recycling my old code for #295 :) )
+#
+# Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used
+#         because 'repos.svn_repos_dir_delta' *doesn't* provide it.
+#
+# Note 2: the 'dir_baton' is the path of the parent directory
+#
+
+class DiffChangeEditor(delta.Editor): 
+
+    def __init__(self):
+        self.deltas = []
+        self.skip_dir_prop_change = 0
+
+    def _norm(self, path):
+        """Path are normalized to __not__ have a leading slash"""
+        return path == '/' and path or path and path.strip('/') or ''
+    
+    # -- svn.delta.Editor callbacks
+
+    def open_root(self, base_revision, dir_pool):
+        return '/'
+
+    def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, dir_pool):
+        self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
+        # don't create an additional 'Changeset.EDIT' entry for this directory
+        # in case there's also a dir property change:
+        self.skip_dir_prop_change = 1 
+        return path
+
+    def open_directory(self, path, dir_baton, base_revision, dir_pool):
+        self.deltas.append((path, Node.DIRECTORY, Changeset.EDIT))
+        self.skip_dir_prop_change = 0
+        return path
+
+    def change_dir_prop(self, dir_baton, name, value, pool):
+        if self.skip_dir_prop_change:
+            return
+        self.deltas.append((dir_baton, Node.DIRECTORY, Changeset.EDIT))
+        self.skip_dir_prop_change = 1
+
+    def close_directory(self, dir_baton):
+        self.skip_dir_prop_change = 0
+
+    def delete_entry(self, path, revision, dir_baton, pool):
+        self.deltas.append((path, Node.FILE, Changeset.DELETE)) # should be Node.UNKNOWN
+
+    def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, dir_pool):
+        self.deltas.append((self._norm(path), Node.FILE, Changeset.ADD))
+
+    def open_file(self, path, dir_baton, dummy_rev, file_pool):
+        self.deltas.append((self._norm(path), Node.FILE, Changeset.EDIT))
+
Index: trac/versioncontrol/cache.py
===================================================================
--- trac/versioncontrol/cache.py	(revision 1756)
+++ trac/versioncontrol/cache.py	(working copy)
@@ -88,6 +88,9 @@
     def get_node(self, path, rev=None):
         return self.repos.get_node(path, rev)
 
+    def has_node(self, path, rev):
+        return self.repos.has_node(path, rev)
+
     def get_oldest_rev(self):
         return self.repos.oldest_rev
 
@@ -112,7 +115,10 @@
     def normalize_rev(self, rev):
         return self.repos.normalize_rev(rev)
 
+    def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
+        return self.repos.get_diffs(old_path, old_rev, new_path, new_rev, ignore_ancestry)
 
+
 class CachedChangeset(Changeset):
 
     def __init__(self, rev, db, authz):
Index: trac/versioncontrol/main.py
===================================================================
--- trac/versioncontrol/main.py	(revision 1756)
+++ trac/versioncontrol/main.py	(working copy)
@@ -42,6 +42,12 @@
         """
         raise NotImplementedError
 
+    def has_node(self, path, rev):
+        """
+        Tell if there's a node at the specified (path,rev) combination.
+        """
+        raise NotImplementedError
+    
     def get_node(self, path, rev=None):
         """
         Retrieve a Node (directory or file) from the repository at the
@@ -114,8 +120,15 @@
         'None' is a valid revision value and represents the youngest revision.
         """
         return NotImplementedError
-        
 
+    def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
+        """
+        Generator that yields (old_path, new_path, kind, change) tuples
+        for each node change between the two arbitrary (path,rev) pairs.
+        """
+        raise NotImplementedError
+
+
 class Node(object):
     """
     Represents a directory or file in the repository.
Index: trac/Diff.py
===================================================================
--- trac/Diff.py	(revision 1756)
+++ trac/Diff.py	(working copy)
@@ -24,10 +24,10 @@
 from __future__ import generators
 import time
 import re
+import posixpath
 
 from trac import mimeview, perm, util
 from trac.core import *
-from trac.Timeline import ITimelineEventProvider
 from trac.versioncontrol import Changeset, Node
 from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff
 from trac.web.chrome import add_link, add_stylesheet
@@ -35,141 +35,157 @@
 from trac.wiki import wiki_to_html, wiki_to_oneliner
 
 
-class ChangesetModule(Component):
+class Diff(dict):
+    def __getattr__(self,str):
+        return self[str]
+    
 
-    implements(IRequestHandler, ITimelineEventProvider)
+class DiffModule(Component):
 
+    implements(IRequestHandler)
+
     # IRequestHandler methods
 
     def match_request(self, req):
-        match = re.match(r'/changeset/([0-9]+)$', req.path_info)
+        match = re.match(r'/diff(?:(/.*)|$)', req.path_info)
         if match:
-            req.args['rev'] = match.group(1)
+            req.args['path'] = match.group(1)
             return 1
 
     def process_request(self, req):
         req.perm.assert_permission(perm.CHANGESET_VIEW)
 
-        rev = req.args.get('rev')
+        path = req.args.get('path')
         repos = self.env.get_repository(req.authname)
+        path = repos.normalize_path(path)
+        rev = req.args.get('rev', repos.youngest_rev) # 'path history' mode
+        old = req.args.get('old')                     # 'arbitrary diff' mode
+        new = req.args.get('new')
+        old_path = req.args.get('old_path', path)
+        if old_path == path and old == new: # force 'path history' mode
+            print "force 'path history' mode"
+            rev = old
+            old_path = old = new = None
 
         diff_options = get_diff_options(req)
         if req.args.has_key('update'):
-            req.redirect(self.env.href.changeset(rev))
+            if old or new:
+                req.redirect(self.env.href.diff(path, new=new, old_path=old_path, old=old))
+            else:
+                req.redirect(self.env.href.diff(path, rev=rev))
 
-        chgset = repos.get_changeset(rev)
-        req.check_modified(chgset.date,
-                           diff_options[0] + ''.join(diff_options[1]))
+        if old or new:
+            chgset = None
+            if not new:
+                new = repos.next_rev(old) # FIXME: must lookup the next entry in node history
+            elif not old:
+                old = repos.previous_rev(new)
+            if not old_path:
+                old_path = path
+            diff = Diff(old_path=old_path, old_rev=old,
+                        new_path=path, new_rev=new)
+            diffargs = 'new=%s&old_path=%s&old=%s' \
+                       % (new, old_path, old)
+        else:
+            chgset = repos.get_changeset(rev)
+            diff = Diff(old_path=path, old_rev=repos.previous_rev(rev),
+                        new_path=path, new_rev=rev)
+            diffargs = 'rev=%s' % rev
 
+        print diff
+        
+        # TODO:
+#         req.check_modified(chgset.date,
+#                            diff_options[0] + ''.join(diff_options[1]))
+
         format = req.args.get('format')
         if format == 'diff':
-            self._render_diff(req, repos, chgset, diff_options)
+            self._render_diff(req, repos, diff, chgset, diff_options)
             return
         elif format == 'zip':
-            self._render_zip(req, repos, chgset)
+            self._render_zip(req, repos, diff, chgset)
             return
 
-        self._render_html(req, repos, chgset, diff_options)
-        add_link(req, 'alternate', '?format=diff', 'Unified Diff',
+        self._render_html(req, repos, diff, chgset, diff_options)
+        add_link(req, 'alternate', '?format=diff&'+diffargs, 'Unified Diff',
                  'text/plain', 'diff')
-        add_link(req, 'alternate', '?format=zip', 'Zip Archive',
+        add_link(req, 'alternate', '?format=zip&'+diffargs, 'Zip Archive',
                  'application/zip', 'zip')
         add_stylesheet(req, 'changeset.css')
         add_stylesheet(req, 'diff.css')
-        return 'changeset.cs', None
+        return 'diff.cs', None
 
-    # ITimelineEventProvider methods
 
-    def get_timeline_filters(self, req):
-        if req.perm.has_permission(perm.CHANGESET_VIEW):
-            yield ('changeset', 'Repository checkins')
-
-    def get_timeline_events(self, req, start, stop, filters):
-        if 'changeset' in filters:
-            absurls = req.args.get('format') == 'rss' # Kludge
-            show_files = int(self.config.get('timeline',
-                                             'changeset_show_files'))
-            db = self.env.get_db_cnx()
-            repos = self.env.get_repository()
-            rev = repos.youngest_rev
-            while rev:
-                chgset = repos.get_changeset(rev)
-                if chgset.date < start:
-                    return
-                if chgset.date < stop:
-                    if absurls:
-                        href = self.env.abs_href.changeset(chgset.rev)
-                    else:
-                        href = self.env.href.changeset(chgset.rev)
-                    title = 'Changeset <em>[%s]</em> by %s' % (
-                            util.escape(chgset.rev), util.escape(chgset.author))
-                    message = wiki_to_oneliner(util.shorten_line(chgset.message or '--'),
-                                               self.env, db, absurls=absurls)
-                    if show_files:
-                        files = []
-                        for chg in chgset.get_changes():
-                            if show_files > 0 and len(files) >= show_files:
-                                files.append('...')
-                                break
-                            files.append('<span class="%s">%s</span>'
-                                         % (chg[2], util.escape(chg[0])))
-                        message = '<span class="changes">' + ', '.join(files) +\
-                                  '</span>: ' + message
-                    yield 'changeset', href, title, chgset.date, chgset.author,\
-                          message
-                rev = repos.previous_rev(rev)
-
     # Internal methods
 
-    def _render_html(self, req, repos, chgset, diff_options):
+    def _render_html(self, req, repos, diff, chgset, diff_options):
         """HTML version"""
-        req.hdf['title'] = '[%s]' % chgset.rev
-        req.hdf['changeset'] = {
-            'revision': chgset.rev,
-            'time': time.strftime('%c', time.localtime(chgset.date)),
-            'author': util.escape(chgset.author or 'anonymous'),
-            'message': wiki_to_html(chgset.message or '--', self.env, req,
-                                    escape_newlines=True)
-        }
+        req.hdf['diff'] = diff
+        req.hdf['diff.href'] = {
+            'new_rev': self.env.href.changeset(diff.new_rev),
+            'old_rev': self.env.href.changeset(diff.old_rev),
+            'new_path': self.env.href.browser(diff.new_path, rev=diff.new_rev),
+            'old_path': self.env.href.browser(diff.old_path, rev=diff.old_rev)
+            }
+        if chgset: # 'path history' mode
+            req.hdf['title'] = 'Changes for %s at Revision %s' % (diff.new_path, chgset.rev)
+            req.hdf['changeset'] = {
+                'revision': chgset.rev,
+                'time': time.strftime('%c', time.localtime(chgset.date)),
+                'author': util.escape(chgset.author or 'anonymous'),
+                'message': wiki_to_html(chgset.message or '--', self.env, req,
+                                        escape_newlines=True)
+                }
 
-        oldest_rev = repos.oldest_rev
-        if chgset.rev != oldest_rev:
-            add_link(req, 'first', self.env.href.changeset(oldest_rev),
-                     'Changeset %s' % oldest_rev)
-            previous_rev = repos.previous_rev(chgset.rev)
-            add_link(req, 'prev', self.env.href.changeset(previous_rev),
-                     'Changeset %s' % previous_rev)
-        youngest_rev = repos.youngest_rev
-        if str(chgset.rev) != str(youngest_rev):
-            next_rev = repos.next_rev(chgset.rev)
-            add_link(req, 'next', self.env.href.changeset(next_rev),
-                     'Changeset %s' % next_rev)
-            add_link(req, 'last', self.env.href.changeset(youngest_rev),
-                     'Changeset %s' % youngest_rev)
+            oldest_rev = repos.oldest_rev
+            if chgset.rev != oldest_rev:
+                add_link(req, 'first', self.env.href.diff(diff.old_path, rev=oldest_rev),
+                         'Changeset %s' % oldest_rev) # FIXME (use the history)
+                previous_rev = repos.previous_rev(chgset.rev)
+                add_link(req, 'prev', self.env.href.diff(diff.old_path, rev=previous_rev),
+                         'Changeset %s' % previous_rev)
+            youngest_rev = repos.youngest_rev
+            if str(chgset.rev) != str(youngest_rev):
+                next_rev = repos.next_rev(chgset.rev)
+                add_link(req, 'next', self.env.href.diff(diff.new_path, rev=next_rev),
+                         'Changeset %s' % next_rev)
+                add_link(req, 'last', self.env.href.diff(diff.new_path, rev=youngest_rev),
+                         'Changeset %s' % youngest_rev)
+        elif diff.new_path == diff.old_path: # 'diff between 2 revisions' mode
+            req.hdf['title'] = 'Diff for %s between Revisions %s and %s' \
+                               % (diff.new_path, diff.old_rev, diff.new_rev)
+        else:                           # 'arbitrary diff' mode
+            req.hdf['title'] = 'Diff between %s at Revision %s and %s at Revision %s' \
+                               % (diff.old_path, diff.old_rev,
+                                  diff.new_path, diff.new_rev)
 
         edits = []
         idx = 0
-        for path, kind, change, base_path, base_rev in chgset.get_changes():
+        old_rev = diff.old_rev
+        new_rev = diff.new_rev
+        for old_path, new_path, kind, change in repos.get_diffs(**diff):
+            print 'delta %d: %s %s delta from %s@%s to %s@%s' \
+                  % (idx, change, kind, old_path, old_rev, new_path, new_rev)
             info = {'change': change}
-            if base_path:
-                info['path.old'] = base_path
-                info['rev.old'] = base_rev
-                info['browser_href.old'] = self.env.href.browser(base_path,
-                                                                 rev=base_rev)
-            if path:
-                info['path.new'] = path
-                info['rev.new'] = chgset.rev
-                info['browser_href.new'] = self.env.href.browser(path,
-                                                                 rev=chgset.rev)
+            if old_path:
+                info['path.old'] = old_path
+                info['rev.old'] = old_rev
+                info['browser_href.old'] = self.env.href.browser(old_path,
+                                                                 rev=old_rev)
+            if new_path:
+                info['path.new'] = new_path
+                info['rev.new'] = new_rev
+                info['browser_href.new'] = self.env.href.browser(new_path,
+                                                                 rev=new_rev)
             if change in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE):
-                edits.append((idx, path, kind, base_path, base_rev))
-            req.hdf['changeset.changes.%d' % idx] = info
+                edits.append((idx, old_path, new_path, kind))
+            req.hdf['diff.changes.%d' % idx] = info
             idx += 1
-
-        for idx, path, kind, base_path, base_rev in edits:
-            old_node = repos.get_node(base_path or path, base_rev)
-            new_node = repos.get_node(path, chgset.rev)
-
+        
+        for idx, old_path, new_path, kind in edits:
+            old_node = repos.get_node(old_path, old_rev)
+            new_node = repos.get_node(new_path, new_rev)
+            
             # Property changes
             old_props = old_node.get_properties()
             new_props = new_node.get_properties()
@@ -183,14 +199,14 @@
                 for k,v in new_props.items():
                     if not k in old_props:
                         changed_props[k] = {'new': v}
-                req.hdf['changeset.changes.%d.props' % idx] = changed_props
+                req.hdf['diff.changes.%d.props' % idx] = changed_props
 
             if kind == Node.DIRECTORY:
                 continue
 
             # Content changes
             default_charset = self.config.get('trac', 'default_charset')
-            old_content = old_node.get_content().read()
+            old_content = old_node.get_content().read()            
             if mimeview.is_binary(old_content):
                 continue
             charset = mimeview.get_charset(old_node.content_type) or \
@@ -217,9 +233,10 @@
                                    ignore_blank_lines='-B' in diff_options[1],
                                    ignore_case='-i' in diff_options[1],
                                    ignore_space_changes='-b' in diff_options[1])
-                req.hdf['changeset.changes.%d.diff' % idx] = changes
+                req.hdf['diff.changes.%d.diff' % idx] = changes
 
-    def _render_diff(self, req, repos, chgset, diff_options):
+
+    def _render_diff(self, req, repos, diff, chgset, diff_options):
         """Raw Unified Diff version"""
         req.send_response(200)
         req.send_header('Content-Type', 'text/plain;charset=utf-8')
@@ -227,15 +244,20 @@
                         'filename=Changeset%s.diff' % req.args.get('rev'))
         req.end_headers()
 
-        for path, kind, change, base_path, base_rev in chgset.get_changes():
+        old_rev = diff.old_rev
+        new_rev = diff.new_rev
+        print diff
+        for old_path, new_path, kind, change in repos.get_diffs(**diff):
+            print '.diff delta : %s %s delta from %s@%s to %s@%s' \
+                  % ( change, kind, old_path, old_rev, new_path, new_rev)
             if change == Changeset.ADD:
                 old_node = None
             else:
-                old_node = repos.get_node(base_path or path, base_rev)
+                old_node = repos.get_node(old_path, old_rev)
             if change == Changeset.DELETE:
                 new_node = None
             else:
-                new_node = repos.get_node(path, chgset.rev)
+                new_node = repos.get_node(new_path, new_rev)
 
             # TODO: Property changes
 
@@ -271,7 +293,7 @@
                     if option[:2] == '-U':
                         context = int(option[2:])
                         break
-                req.write('Index: ' + path + util.CRLF)
+                req.write('Index: ' + new_path + util.CRLF)
                 req.write('=' * 67 + util.CRLF)
                 req.write('--- %s (revision %s)' % old_node_info +
                           util.CRLF)
@@ -284,12 +306,13 @@
                                          ignore_space_changes='-b' in diff_options[1]):
                     req.write(line + util.CRLF)
 
-    def _render_zip(self, req, repos, chgset):
+    def _render_zip(self, req, repos, diff, chgset):
         """ZIP archive with all the added and/or modified files."""
+        new_rev = diff.new_rev
         req.send_response(200)
         req.send_header('Content-Type', 'application/zip')
         req.send_header('Content-Disposition',
-                        'filename=Changeset%s.zip' % chgset.rev)
+                        'filename=Changeset%s.zip' % new_rev)
         req.end_headers()
 
         try:
@@ -300,9 +323,9 @@
 
         buf = StringIO()
         zipfile = ZipFile(buf, 'w', ZIP_DEFLATED)
-        for path, kind, change, base_path, base_rev in chgset.get_changes():
+        for old_path, new_path, kind, change in repos.get_diffs(**diff):
             if kind == Node.FILE and change != Changeset.DELETE:
-                node = repos.get_node(path, chgset.rev)
+                node = repos.get_node(new_path, new_rev)
                 zipinfo = ZipInfo()
                 zipinfo.filename = node.path
                 zipinfo.date_time = time.gmtime(node.last_modified)[:6]
Index: trac/Browser.py
===================================================================
--- trac/Browser.py	(revision 1756)
+++ trac/Browser.py	(working copy)
@@ -75,7 +75,24 @@
         })
     return links
 
+def _anydiff_support(env, req, node):
+    path, rev = node.created_path, node.created_rev # Kludge: needs to be in Node interface
+    select_for_diff = req.args.get('diff')
+    req.hdf['diff.anydiff_href'] = env.href.diff(path)
+    if select_for_diff == "1":
+        req.session['diff_base_path'] = path
+        req.session['diff_base_rev'] = rev
 
+    if req.session.has_key('diff_base_path'):
+        if select_for_diff == "0":
+            del req.session['diff_base_path']
+            del req.session['diff_base_rev']
+        else:
+            req.hdf['session'] = {
+                'diff_base_path': req.session['diff_base_path'],
+                'diff_base_rev': req.session['diff_base_rev']
+                }
+
 class BrowserModule(Component):
 
     implements(INavigationContributor, IRequestHandler)
@@ -113,14 +130,17 @@
 
         req.hdf['title'] = path
         req.hdf['browser'] = {
-            'path': path,
-            'revision': rev or repos.youngest_rev,
+            'path': node.path,
+            'revision': node.rev,
             'props': dict([(util.escape(name), util.escape(value))
                            for name, value in node.get_properties().items()]),
-            'href': self.env.href.browser(path,rev=rev or repos.youngest_rev),
-            'log_href': self.env.href.log(path)
+            'href': self.env.href.browser(node.path,rev=node.rev),
+            'diff_href': self.env.href.diff(node.path,rev=node.rev),
+            'log_href': self.env.href.log(node.path)
         }
 
+        _anydiff_support(self.env, req, node)
+
         path_links = _get_path_links(self.env.href, path, rev)
         if len(path_links) > 1:
             add_link(req, 'up', path_links[-2]['href'], 'Parent directory')
@@ -268,7 +288,15 @@
         stop_rev = req.args.get('stop_rev')
         verbose = req.args.get('verbose')
         limit = int(req.args.get('limit') or 100)
+        old = req.args.get('old')
+        new = req.args.get('new')
 
+        repos = self.env.get_repository(req.authname)
+        normpath = repos.normalize_path(path)
+        rev = str(repos.normalize_rev(rev))
+        old = old or str(repos.previous_rev(rev))
+        new = new or rev
+
         req.hdf['title'] = path + ' (log)'
         req.hdf['log'] = {
             'path': path,
@@ -276,7 +304,10 @@
             'verbose': verbose,
             'stop_rev': stop_rev,
             'browser_href': self.env.href.browser(path, rev=rev),
-            'log_href': self.env.href.log(path, rev=rev)
+            'log_href': self.env.href.log(path, rev=rev),
+            'diff_href': self.env.href.diff(path, old=old, new=new),
+            'old': old,
+            'new': new
         }
 
         path_links = _get_path_links(self.env.href, path, rev)
@@ -284,14 +315,11 @@
         if path_links:
             add_link(req, 'up', path_links[-1]['href'], 'Parent directory')
 
-        repos = self.env.get_repository(req.authname)
-        normpath = repos.normalize_path(path)
-        rev = str(repos.normalize_rev(rev))
-
         # 'node' or 'path' history: use get_node()/get_history() or get_path_history()
         if mode != 'path_history':
             try:
                 node = repos.get_node(path, rev)
+                _anydiff_support(self.env, req, node)
             except TracError:
                 node = None
             if not node:
Index: templates/anydiff.cs
===================================================================
--- templates/anydiff.cs	(revision 0)
+++ templates/anydiff.cs	(revision 0)
@@ -0,0 +1,37 @@
+<?cs 
+def:anydiff(new_path,new_rev,module_href) ?>
+ <div id="anydiff"><?cs
+  if:session.diff_base_path == new_path && session.diff_base_rev == new_rev ?>
+   <em>This Path/Revision is the Base for Diff</em><?cs
+  else ?><?cs
+   if:session.diff_base_path ?>
+     <form action="<?cs var:diff.anydiff_href ?>" method="get">
+      <input type="hidden" name="old_path" value="<?cs var:session.diff_base_path ?>" />
+      <input type="hidden" name="old" value="<?cs var:session.diff_base_rev ?>" />
+      <input type="hidden" name="new" value="<?cs var:new_rev ?>" />
+      <div class="buttons">
+       <input type="submit" value="Diff" 
+              title="Diff the current Path/Revision against the selected Base" />
+      </div>
+     </form>
+    against: <em><?cs var:session.diff_base_path ?></em> 
+     in revision <em><?cs var:session.diff_base_rev ?></em>
+    <form action="<?cs var:module_href ?>" method="get">
+     <input type="hidden" name="diff" value="0" />
+     <div class="buttons">
+      <input type="submit" value="Clear" 
+       title="Clear the Base for Diff" />
+     </div>
+    </form><?cs
+   /if ?>
+   <form action="<?cs var:module_href ?>" method="get">
+    <input type="hidden" name="diff" value="1" />
+    <div class="buttons">
+     <input type="submit" 
+       value="Set<?cs if:!session.diff_base_path ?>  Base for Diff<?cs /if ?>"
+      title="Select the current Path/Revision as the new Base for Diff" />
+    </div>
+   </form><?cs
+  /if ?>
+ </div><?cs
+/def ?>
Index: templates/log.cs
===================================================================
--- templates/log.cs	(revision 1756)
+++ templates/log.cs	(working copy)
@@ -1,10 +1,12 @@
 <?cs include "header.cs"?>
 <?cs include "macros.cs"?>
+<?cs include "anydiff.cs"?>
 
 <div id="ctxtnav" class="nav">
  <ul>
-  <li class="last"><a href="<?cs
-    var:log.browser_href ?>">View Latest Revision</a></li><?cs
+  <li class="last">
+   <a href="<?cs var:log.browser_href ?>">View Latest Revision</a>
+  </li><?cs
   if:len(chrome.links.prev) ?>
    <li class="first<?cs if:!len(chrome.links.next) ?> last<?cs /if ?>">
     &larr; <a href="<?cs var:chrome.links.prev.0.href ?>" title="<?cs
@@ -61,6 +63,9 @@
           title="Warning: by updating, you will clear the page history" />
   </div>
  </form>
+
+<?cs call:anydiff(log.path, log.rev, log.log_href) ?>
+
  <div class="diff">
   <div id="legend">
    <h3>Legend:</h3>
@@ -74,9 +79,19 @@
    </dl>
   </div>
  </div>
+
  <table id="chglist" class="listing">
+  <form action="<?cs var:log.diff_href ?>" method="get">
   <thead>
+   <tr class="diff">
+    <th colspan="2">
+     <div class="buttons"><input type="submit" value="Diff" 
+       title="Diff from Old Revision to New Revision (select them below)" /></div>
+    </th>
+   </tr>
    <tr>
+    <th>Old</th>
+    <th>New</th>
     <th class="change"></th>
     <th class="data">Date</th>
     <th class="rev">Rev</th>
@@ -87,6 +102,7 @@
   </thead>
   <tbody><?cs
    set:indent = #1 ?><?cs
+   set:idx = #0 ?><?cs
    each:item = log.items ?><?cs
     if:item.copyfrom_path ?>
      <tr class="<?cs if:name(item) % #2 ?>even<?cs else ?>odd<?cs /if ?>">
@@ -99,6 +115,10 @@
       set:indent = #1 ?><?cs
     /if ?>
     <tr class="<?cs if:name(item) % #2 ?>even<?cs else ?>odd<?cs /if ?>">
+     <td><input type="radio" name="old" value="<?cs var:item.rev ?>" <?cs
+          if:idx == #1 ?> checked="checked" <?cs /if ?> /></td>
+     <td><input type="radio" name="new" value="<?cs var:item.rev ?>" <?cs
+          if:idx == #0 ?> checked="checked" <?cs /if ?> /></td>
      <td class="change" style="padding-left:<?cs var:indent ?>em">
       <a title="View log starting at this revision" href="<?cs var:item.log_href ?>">
        <div class="<?cs var:item.change ?>"></div>
@@ -115,8 +135,10 @@
      <td class="author"><?cs var:log.changes[item.rev].author ?></td>
      <td class="summary"><?cs var:log.changes[item.rev].message ?></td>
     </tr><?cs
+    set:idx = idx + 1 ?><?cs
    /each ?>
   </tbody>
+  </form>
  </table><?cs
  if:len(links.prev) || len(links.next) ?><div id="paging" class="nav"><ul><?cs
   if:len(links.prev) ?><li class="first<?cs
Index: templates/browser.cs
===================================================================
--- templates/browser.cs	(revision 1756)
+++ templates/browser.cs	(working copy)
@@ -1,8 +1,10 @@
 <?cs include "header.cs"?>
 <?cs include "macros.cs"?>
+<?cs include "anydiff.cs"?>
 
 <div id="ctxtnav" class="nav">
  <ul>
+  <li class="first"><a href="<?cs var:browser.diff_href ?>">Diff to previous</a></li>
   <li class="last"><a href="<?cs var:browser.log_href ?>">Revision Log</a></li>
  </ul>
 </div>
@@ -114,5 +116,7 @@
   ?>/TracBrowser">TracBrowser</a> for help on using the browser.
  </div>
 
+ <?cs call:anydiff(browser.path, browser.revision, browser.href) ?>
+
 </div>
 <?cs include:"footer.cs"?>
Index: templates/diff.cs
===================================================================
--- templates/diff.cs	(revision 1756)
+++ templates/diff.cs	(working copy)
@@ -2,19 +2,19 @@
 <?cs include "macros.cs"?>
 
 <div id="ctxtnav" class="nav">
- <h2>Changeset Navigation</h2><?cs
+ <h2>Diff Navigation</h2><?cs
  with:links = chrome.links ?>
   <ul><?cs
    if:len(links.prev) ?>
     <li class="first<?cs if:!len(links.next) ?> last<?cs /if ?>">
      <a class="prev" href="<?cs var:links.prev.0.href ?>" title="<?cs
-       var:links.prev.0.title ?>">Previous Changeset</a>
+       var:links.prev.0.title ?>">Previous Diff</a>
     </li><?cs
    /if ?><?cs
    if:len(links.next) ?>
     <li class="<?cs if:len(links.prev) ?>first <?cs /if ?>last">
      <a class="next" href="<?cs var:links.next.0.href ?>" title="<?cs
-       var:links.next.0.title ?>">Next Changeset</a>
+       var:links.next.0.title ?>">Next Diff</a>
     </li><?cs
    /if ?>
   </ul><?cs
@@ -22,9 +22,32 @@
 </div>
 
 <div id="content" class="changeset">
-<h1>Changeset <?cs var:changeset.revision ?></h1>
+<h1><?cs
+ if:len(changeset) > #0 ?>
+  Changes for <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>">
+   <?cs var:diff.new_path ?></a> 
+  in Revision <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>">
+   <?cs var:diff.new_rev ?></a><?cs
+ elif:diff.new_path == diff.old_path ?>
+  Differences for <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>">
+   <?cs var:diff.new_path ?></a> 
+  between Revisions <a title="Show full changeset" href="<?cs var:diff.href.old_rev ?>">
+   <?cs var:diff.old_rev ?></a>
+  and <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>">
+   <?cs var:diff.new_rev ?></a><?cs
+ else ?>
+  Differences between <a title="Show entry in browser" href="<?cs var:diff.href.old_path ?>">
+   <?cs var:diff.old_path ?></a> 
+  at Revision <a title="Show full changeset" href="<?cs var:diff.href.old_rev ?>">
+   <?cs var:diff.old_rev ?></a>
+  and <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>">
+   <?cs var:diff.new_path ?></a> 
+  at Revision <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>">
+   <?cs var:diff.new_rev ?></a><?cs
+ /if ?>
+</h1>
 
-<?cs each:change = changeset.changes ?><?cs
+<?cs each:change = diff.changes ?><?cs
  if:len(change.diff) ?><?cs
   set:has_diffs = 1 ?><?cs
  /if ?><?cs
@@ -32,6 +55,9 @@
   || diff.options.ignorecase || diff.options.ignorewhitespace ?>
 <form method="post" id="prefs" action="">
  <div>
+  <input type="hidden" name="old_path" value="<?cs var:diff.old_path ?>" />
+  <input type="hidden" name="old" value="<?cs var:diff.old_rev ?>" />
+  <input type="hidden" name="new" value="<?cs var:diff.new_rev ?>" />
   <label for="style">View differences</label>
   <select id="style" name="style">
    <option value="inline"<?cs
@@ -100,16 +126,18 @@
   /if ?>
 <?cs /def ?>
 
-<dl id="overview">
+<dl id="overview"><?cs
+ if:len(changeset) > #0 ?>
  <dt class="time">Timestamp:</dt>
  <dd class="time"><?cs var:changeset.time ?></dd>
  <dt class="author">Author:</dt>
  <dd class="author"><?cs var:changeset.author ?></dd>
  <dt class="message">Message:</dt>
- <dd class="message" id="searchable"><?cs var:changeset.message ?></dd>
+ <dd class="message" id="searchable"><?cs var:changeset.message ?></dd><?cs
+ /if ?>
  <dt class="files">Files:</dt>
  <dd class="files">
-  <ul><?cs each:item = changeset.changes ?>
+  <ul><?cs each:item = diff.changes ?>
    <li><?cs
     if:item.change == 'add' ?><?cs
      call:node_change(item, 'add', 'added') ?><?cs
@@ -140,7 +168,7 @@
   </dl>
  </div>
  <ul class="entries"><?cs
- each:item = changeset.changes ?><?cs
+ each:item = diff.changes ?><?cs
   if:len(item.diff) || len(item.props) ?><li class="entry" id="file<?cs
    var:name(item) ?>"><h2><a href="<?cs
    var:item.browser_href.new ?>" title="Show new revision <?cs

