diff -ruN -x .svn trac-0.9b2/htdocs/css/browser.css anydiff-branch/htdocs/css/browser.css
--- trac-0.9b2/htdocs/css/browser.css	2005-09-25 16:47:54.000000000 +0200
+++ anydiff-branch/htdocs/css/browser.css	2005-09-07 17:25:21.000000000 +0200
@@ -45,6 +45,29 @@
 #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; 
+}
+
+div.buttons {
+ clear: left;
+}
+
+#anydiff {
+ margin: 0 0 1em;
+ float: left;
+}
+#anydiff form, #anydiff div, #anydiff h2 {
+ display: inline;
+}
+#anydiff input { 
+ vertical-align: baseline;
+ margin: 0 -0.5em 0 1em;
+}
+
+
 /* Styles for the revision log table
    (extends the styles for "table.listing") */
 #chglist { margin-top: 0 }
diff -ruN -x .svn trac-0.9b2/htdocs/css/changeset.css anydiff-branch/htdocs/css/changeset.css
--- trac-0.9b2/htdocs/css/changeset.css	2005-09-25 16:47:54.000000000 +0200
+++ anydiff-branch/htdocs/css/changeset.css	2005-06-07 17:33:23.000000000 +0200
@@ -26,3 +26,19 @@
 
 .diff ul.props { font-size: 90%; list-style: disc; margin: .5em 0 0; padding: 0 .5em 1em 2em }
 .diff ul.props li { margin: 0; padding: 0 }
+
+
+#title dl {
+ display: inline;
+ font-size: 110%
+}
+#title dt { 
+  font-size: 110%;
+  font-weight: bold;
+  display: inline; 
+  margin-left: 3em;
+}
+#title dd { 
+  display: inline;
+  margin-left: 0.4em;
+}
diff -ruN -x .svn trac-0.9b2/templates/anydiff.cs anydiff-branch/templates/anydiff.cs
--- trac-0.9b2/templates/anydiff.cs	1970-01-01 01:00:00.000000000 +0100
+++ anydiff-branch/templates/anydiff.cs	2005-08-30 10:46:05.000000000 +0200
@@ -0,0 +1,47 @@
+<?cs include "header.cs"?>
+
+<div id="ctxtnav" class="nav">
+ <h2>Navigation</h2><?cs
+ with:links = chrome.links ?>
+  <ul>
+  </ul><?cs
+ /with ?>
+</div>
+
+<div id="content" class="changeset">
+ <div id="title">
+    <h1>Select Base and Target for Diff:</h1>
+ </div>
+
+ <div id="anydiff">
+  <form action="<?cs var:anydiff.diff_href ?>" method="post">
+   <table>
+    <tr>
+     <th><label for="old_path">From:</label></th>
+     <td>
+      <input type="text" id="old_path" name="old_path" value="<?cs
+         var:anydiff.old_path ?>" size="44" />
+      <label for="old_rev">at Revision:</label>
+      <input type="text" id="old_rev" name="old" value="<?cs
+         var:anydiff.old_rev ?>" size="4" />
+     </td>
+    </tr>
+    <tr>
+     <th><label for="new_path">To:</label></th>
+     <td>
+      <input type="text" id="new_path" name="path" value="<?cs
+         var:anydiff.new_path ?>" size="44" />
+      <label for="new_rev">at Revision:</label>
+      <input type="text" id="new_rev" name="new" value="<?cs
+         var:anydiff.new_rev ?>" size="4" />
+     </td>
+    </tr>
+   </table>
+   <div class="buttons">
+      <input type="submit" value="View changes" />
+   </div>
+  </form>
+ </div>
+</div>
+
+<?cs include "footer.cs"?>
diff -ruN -x .svn trac-0.9b2/templates/browser.cs anydiff-branch/templates/browser.cs
--- trac-0.9b2/templates/browser.cs	2005-09-25 16:48:01.000000000 +0200
+++ anydiff-branch/templates/browser.cs	2005-09-19 15:12:12.000000000 +0200
@@ -3,19 +3,25 @@
 
 <div id="ctxtnav" class="nav">
  <ul>
-  <li class="last"><a href="<?cs var:browser.log_href ?>">Revision Log</a></li>
+  <li class="first"><a href="<?cs var:browser.restr_changeset_href ?>">
+   Last Change</a></li>
+  <li class="last"><a href="<?cs var:browser.log_href ?>">
+   Revision Log</a></li>
  </ul>
 </div>
 
+
 <div id="content" class="browser">
  <h1><?cs call:browser_path_links(browser.path, browser) ?></h1>
 
  <div id="jumprev">
-  <form action="" method="get"><div>
-   <label for="rev">View revision:</label>
-   <input type="text" id="rev" name="rev" value="<?cs
-     var:browser.revision ?>" size="4" />
-  </div></form>
+  <form action="" method="get">
+   <div>
+    <label for="rev">View revision:</label>
+    <input type="text" id="rev" name="rev" value="<?cs
+       var:browser.revision ?>" size="4" />
+   </div>
+  </form>
  </div>
 
  <?cs if:browser.is_dir ?>
@@ -114,5 +120,19 @@
   ?>/TracBrowser">TracBrowser</a> for help on using the browser.
  </div>
 
+  <div id="anydiff"><?cs
+   if len(browser.path) > #1 ?>
+    <form action="<?cs var:browser.anydiff_href ?>" method="get">
+     <input type="hidden" name="new_path" value="<?cs var:browser.path ?>" />
+     <input type="hidden" name="old_path" value="<?cs var:browser.path ?>" />
+     <input type="hidden" name="new_rev" value="<?cs var:browser.revision ?>" />
+     <input type="hidden" name="old_rev" value="<?cs var:browser.revision ?>" />
+     <div class="buttons">
+      <input type="submit" value="View changes..." title="Prepare an Arbitrary Diff" />
+     </div>
+    </form><?cs
+   /if ?>
+  </div>
+
 </div>
 <?cs include:"footer.cs"?>
diff -ruN -x .svn trac-0.9b2/templates/changeset.cs anydiff-branch/templates/changeset.cs
--- trac-0.9b2/templates/changeset.cs	2005-09-25 16:48:01.000000000 +0200
+++ anydiff-branch/templates/changeset.cs	1970-01-01 01:00:00.000000000 +0100
@@ -1,206 +0,0 @@
-<?cs include "header.cs"?>
-<?cs include "macros.cs"?>
-
-<div id="ctxtnav" class="nav">
- <h2>Changeset 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>
-    </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>
-    </li><?cs
-   /if ?>
-  </ul><?cs
- /with ?>
-</div>
-
-<div id="content" class="changeset">
-<h1>Changeset <?cs var:changeset.revision ?></h1>
-
-<?cs each:change = changeset.changes ?><?cs
- if:len(change.diff) ?><?cs
-  set:has_diffs = 1 ?><?cs
- /if ?><?cs
-/each ?><?cs if:has_diffs || diff.options.ignoreblanklines 
-  || diff.options.ignorecase || diff.options.ignorewhitespace ?>
-<form method="post" id="prefs" action="">
- <div>
-  <label for="style">View differences</label>
-  <select id="style" name="style">
-   <option value="inline"<?cs
-     if:diff.style == 'inline' ?> selected="selected"<?cs
-     /if ?>>inline</option>
-   <option value="sidebyside"<?cs
-     if:diff.style == 'sidebyside' ?> selected="selected"<?cs
-     /if ?>>side by side</option>
-  </select>
-  <div class="field">
-   Show <input type="text" name="contextlines" id="contextlines" size="2"
-     maxlength="2" value="<?cs var:diff.options.contextlines ?>" />
-   <label for="contextlines">lines around each change</label>
-  </div>
-  <fieldset id="ignore">
-   <legend>Ignore:</legend>
-   <div class="field">
-    <input type="checkbox" id="blanklines" name="ignoreblanklines"<?cs
-      if:diff.options.ignoreblanklines ?> checked="checked"<?cs /if ?> />
-    <label for="blanklines">Blank lines</label>
-   </div>
-   <div class="field">
-    <input type="checkbox" id="case" name="ignorecase"<?cs
-      if:diff.options.ignorecase ?> checked="checked"<?cs /if ?> />
-    <label for="case">Case changes</label>
-   </div>
-   <div class="field">
-    <input type="checkbox" id="whitespace" name="ignorewhitespace"<?cs
-      if:diff.options.ignorewhitespace ?> checked="checked"<?cs /if ?> />
-    <label for="whitespace">White space changes</label>
-   </div>
-  </fieldset>
-  <div class="buttons">
-   <input type="submit" name="update" value="Update" />
-  </div>
- </div>
-</form><?cs /if ?>
-
-<?cs def:node_change(item,cl,kind) ?><?cs 
-  set:ndiffs = len(item.diff) ?><?cs
-  set:nprops = len(item.props) ?>
-  <div class="<?cs var:cl ?>"></div><?cs 
-  if:cl == "rem" ?>
-   <a title="Show what was removed (rev. <?cs var:item.rev.old ?>)" href="<?cs
-     var:item.browser_href.old ?>"><?cs var:item.path.old ?></a><?cs
-  else ?>
-   <a title="Show entry in browser" href="<?cs
-     var:item.browser_href.new ?>"><?cs var:item.path.new ?></a><?cs
-  /if ?>
-  <span class="comment">(<?cs var:kind ?>)</span><?cs
-  if:item.path.old && item.change == 'copy' || item.change == 'move' ?>
-   <small><em>(<?cs var:kind ?> from <a href="<?cs
-    var:item.browser_href.old ?>" title="Show original file (rev. <?cs
-    var:item.rev.old ?>)"><?cs var:item.path.old ?></a>)</em></small><?cs
-  /if ?><?cs
-  if:$ndiffs + $nprops > #0 ?>
-    (<a href="#file<?cs var:name(item) ?>" title="Show differences"><?cs
-      if:$ndiffs > #0 ?><?cs var:ndiffs ?>&nbsp;diff<?cs if:$ndiffs > #1 ?>s<?cs /if ?><?cs 
-      /if ?><?cs
-      if:$ndiffs && $nprops ?>, <?cs /if ?><?cs 
-      if:$nprops > #0 ?><?cs var:nprops ?>&nbsp;prop<?cs if:$nprops > #1 ?>s<?cs /if ?><?cs
-      /if ?></a>)<?cs
-  elif:cl == "mod" ?>
-    (<a href="<?cs var:item.browser_href.old ?>"
-        title="Show previous version in browser">previous</a>)<?cs
-  /if ?>
-<?cs /def ?>
-
-<dl id="overview">
- <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>
- <dt class="files">Files:</dt>
- <dd class="files">
-  <ul><?cs each:item = changeset.changes ?>
-   <li><?cs
-    if:item.change == 'add' ?><?cs
-     call:node_change(item, 'add', 'added') ?><?cs
-    elif:item.change == 'delete' ?><?cs
-     call:node_change(item, 'rem', 'deleted') ?><?cs
-    elif:item.change == 'copy' ?><?cs
-     call:node_change(item, 'cp', 'copied') ?><?cs
-    elif:item.change == 'move' ?><?cs
-     call:node_change(item, 'mv', 'moved') ?><?cs
-    elif:item.change == 'edit' ?><?cs
-     call:node_change(item, 'mod', 'modified') ?><?cs
-    /if ?>
-   </li>
-  <?cs /each ?></ul>
- </dd>
-</dl>
-
-<div class="diff">
- <div id="legend">
-  <h3>Legend:</h3>
-  <dl>
-   <dt class="unmod"></dt><dd>Unmodified</dd>
-   <dt class="add"></dt><dd>Added</dd>
-   <dt class="rem"></dt><dd>Removed</dd>
-   <dt class="mod"></dt><dd>Modified</dd>
-   <dt class="cp"></dt><dd>Copied</dd>
-   <dt class="mv"></dt><dd>Moved</dd>
-  </dl>
- </div>
- <ul class="entries"><?cs
- each:item = changeset.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
-   var:item.rev.new ?> of this file in browser"><?cs
-   var:item.path.new ?></a></h2><?cs
-   if:len(item.props) ?><ul class="props"><?cs
-    each:prop = item.props ?><li>Property <strong><?cs
-     var:name(prop) ?></strong> <?cs
-     if:prop.old && prop.new ?>changed from <?cs
-     elif:!prop.old ?>set<?cs
-     else ?>deleted<?cs
-     /if ?><?cs
-     if:prop.old && prop.new ?><em><tt><?cs var:prop.old ?></tt></em><?cs /if ?><?cs
-     if:prop.new ?> to <em><tt><?cs var:prop.new ?></tt></em><?cs /if ?></li><?cs
-    /each ?></ul><?cs
-   /if ?><?cs
-   if:len(item.diff) ?><table class="<?cs
-    var:diff.style ?>" summary="Differences" cellspacing="0"><?cs
-    if:diff.style == 'sidebyside' ?>
-     <colgroup class="l"><col class="lineno" /><col class="content" /></colgroup>
-     <colgroup class="r"><col class="lineno" /><col class="content" /></colgroup>
-     <thead><tr>
-      <th colspan="2"><a href="<?cs
-       var:item.browser_href.old ?>" title="Show old rev. <?cs
-       var:item.rev.old ?> of <?cs var:item.path.old ?>">Revision <?cs
-       var:item.rev.old ?></a></th>
-      <th colspan="2"><a href="<?cs
-       var:item.browser_href.new ?>" title="Show new rev. <?cs
-       var:item.rev.new ?> of <?cs var:item.path.new ?>">Revision <?cs
-       var:item.rev.new ?></a></th>
-      </tr>
-     </thead><?cs
-     each:change = item.diff ?><tbody><?cs
-      call:diff_display(change, diff.style) ?></tbody><?cs
-      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr>
-       <th>&hellip;</th><td>&nbsp;</td><th>&hellip;</th><td>&nbsp;</td>
-      </tr></tbody><?cs /if ?><?cs
-     /each ?><?cs
-    else ?>
-     <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup>
-     <thead><tr>
-      <th title="Revision <?cs var:item.rev.old ?>"><a href="<?cs
-       var:item.browser_href.old ?>" title="Show old version of <?cs
-       var:item.path.old ?>">r<?cs var:item.rev.old ?></a></th>
-      <th title="Revision <?cs var:item.rev.new ?>"><a href="<?cs
-       var:item.browser_href.new ?>" title="Show new version of <?cs
-       var:item.path.new ?>">r<?cs var:item.rev.new ?></a></th>
-      <th>&nbsp;</th></tr>
-     </thead><?cs
-     each:change = item.diff ?><?cs
-      call:diff_display(change, diff.style) ?><?cs
-      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr>
-       <th>&hellip;</th><th>&hellip;</th><td>&nbsp;</td>
-      </tr></tbody><?cs /if ?><?cs
-     /each ?><?cs
-    /if ?></table><?cs
-   /if ?></li><?cs
-  /if ?><?cs
- /each ?></ul>
-</div>
-
-</div>
-<?cs include "footer.cs"?>
diff -ruN -x .svn trac-0.9b2/templates/diff.cs anydiff-branch/templates/diff.cs
--- trac-0.9b2/templates/diff.cs	1970-01-01 01:00:00.000000000 +0100
+++ anydiff-branch/templates/diff.cs	2005-09-28 10:33:07.000000000 +0200
@@ -0,0 +1,258 @@
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav">
+ <h2>Navigation</h2><?cs
+ with:links = chrome.links ?>
+  <ul><?cs
+   if:diff.chgset ?><?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 <?cs 
+         if:diff.restricted ?>Change<?cs else ?>Changeset<?cs /if ?></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 <?cs 
+         if:diff.restricted ?>Change<?cs else ?>Changeset<?cs /if ?></a>
+     </li><?cs
+    /if ?><?cs
+   else ?>
+    <li class="first"><a href="<?cs var:diff.reverse_href ?>">Reverse Diff</a></li><?cs
+   /if ?>
+  </ul><?cs
+ /with ?>
+</div>
+
+<div id="content" class="changeset">
+ <div id="title"><?cs
+  if:diff.chgset ?><?cs
+   if:diff.restricted ?>
+    <h1>Changeset <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>">
+      <?cs var:diff.new_rev ?></a> 
+     for <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>">
+      <?cs var:diff.new_path ?></a> 
+    </h1><?cs
+   else ?>
+    <h1>Changeset <?cs var:diff.new_rev ?></h1><?cs
+   /if ?><?cs
+  else ?><?cs
+    if:diff.restricted ?>
+    <h1>Changes in <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>">
+      <?cs var:diff.new_path ?></a>
+     from revision <a title="Show full changeset" href="<?cs var:diff.href.old_rev ?>">
+      <?cs var:diff.old_rev ?></a>
+     to <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>">
+      <?cs var:diff.new_rev ?></a>
+    </h1><?cs
+   else ?>
+    <h1>Changes from <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>
+     to <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>
+    </h1><?cs
+   /if ?><?cs
+  /if ?>
+ </div>
+
+<?cs each:change = diff.changes ?><?cs
+ if:len(change.diff) ?><?cs
+  set:has_diffs = 1 ?><?cs
+ /if ?><?cs
+/each ?><?cs if:has_diffs || diff.options.ignoreblanklines 
+  || diff.options.ignorecase || diff.options.ignorewhitespace ?>
+<form method="post" id="prefs" action="">
+ <div><?cs 
+  if:!diff.chgset ?>
+   <input type="hidden" name="old_path" value="<?cs var:diff.old_path ?>" />
+   <input type="hidden" name="path" value="<?cs var:diff.new_path ?>" />
+   <input type="hidden" name="old" value="<?cs var:diff.old_rev ?>" />
+   <input type="hidden" name="new" value="<?cs var:diff.new_rev ?>" /><?cs
+  /if ?>
+  <label for="style">View differences</label>
+  <select id="style" name="style">
+   <option value="inline"<?cs
+     if:diff.style == 'inline' ?> selected="selected"<?cs
+     /if ?>>inline</option>
+   <option value="sidebyside"<?cs
+     if:diff.style == 'sidebyside' ?> selected="selected"<?cs
+     /if ?>>side by side</option>
+  </select>
+  <div class="field">
+   Show <input type="text" name="contextlines" id="contextlines" size="2"
+     maxlength="2" value="<?cs var:diff.options.contextlines ?>" />
+   <label for="contextlines">lines around each change</label>
+  </div>
+  <fieldset id="ignore">
+   <legend>Ignore:</legend>
+   <div class="field">
+    <input type="checkbox" id="blanklines" name="ignoreblanklines"<?cs
+      if:diff.options.ignoreblanklines ?> checked="checked"<?cs /if ?> />
+    <label for="blanklines">Blank lines</label>
+   </div>
+   <div class="field">
+    <input type="checkbox" id="case" name="ignorecase"<?cs
+      if:diff.options.ignorecase ?> checked="checked"<?cs /if ?> />
+    <label for="case">Case changes</label>
+   </div>
+   <div class="field">
+    <input type="checkbox" id="whitespace" name="ignorewhitespace"<?cs
+      if:diff.options.ignorewhitespace ?> checked="checked"<?cs /if ?> />
+    <label for="whitespace">White space changes</label>
+   </div>
+  </fieldset>
+  <div class="buttons">
+   <input type="submit" name="update" value="Update" />
+  </div>
+ </div>
+</form><?cs /if ?>
+
+<?cs def:node_change(item,cl,kind) ?><?cs 
+  set:ndiffs = len(item.diff) ?><?cs
+  set:nprops = len(item.props) ?>
+  <div class="<?cs var:cl ?>"></div><?cs 
+  if:cl == "rem" ?>
+   <a title="Show what was removed (rev. <?cs var:item.rev.old ?>)" href="<?cs
+     var:item.browser_href.old ?>"><?cs var:item.path.old ?></a><?cs
+  else ?>
+   <a title="Show entry in browser" href="<?cs
+     var:item.browser_href.new ?>"><?cs var:item.path.new ?></a><?cs
+  /if ?>
+  <span class="comment">(<?cs var:kind ?>)</span><?cs
+  if:item.path.old && item.change == 'copy' || item.change == 'move' ?>
+   <small><em>(<?cs var:kind ?> from <a href="<?cs
+    var:item.browser_href.old ?>" title="Show original file (rev. <?cs
+    var:item.rev.old ?>)"><?cs var:item.path.old ?></a>)</em></small><?cs
+  /if ?><?cs
+  if:$ndiffs + $nprops > #0 ?>
+    (<a href="#file<?cs var:name(item) ?>" title="Show differences"><?cs
+      if:$ndiffs > #0 ?><?cs var:ndiffs ?>&nbsp;diff<?cs if:$ndiffs > #1 ?>s<?cs /if ?><?cs 
+      /if ?><?cs
+      if:$ndiffs && $nprops ?>, <?cs /if ?><?cs 
+      if:$nprops > #0 ?><?cs var:nprops ?>&nbsp;prop<?cs if:$nprops > #1 ?>s<?cs /if ?><?cs
+      /if ?></a>)<?cs
+  elif:cl == "mod" ?>
+    (<a href="<?cs var:item.browser_href.old ?>"
+        title="Show previous version in browser">previous</a>)<?cs
+  /if ?>
+<?cs /def ?>
+
+<dl id="overview"><?cs
+ if:diff.chgset ?>
+ <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><?cs
+ /if ?>
+ <dt class="files"><?cs 
+  if:len(diff.changes) > #0 ?>
+   Files:<?cs
+  else ?>
+   (None)<?cs
+  /if ?>
+ </dt>
+ <dd class="files">
+  <ul><?cs each:item = diff.changes ?>
+   <li><?cs
+    if:item.change == 'add' ?><?cs
+     call:node_change(item, 'add', 'added') ?><?cs
+    elif:item.change == 'delete' ?><?cs
+     call:node_change(item, 'rem', 'deleted') ?><?cs
+    elif:item.change == 'copy' ?><?cs
+     call:node_change(item, 'cp', 'copied') ?><?cs
+    elif:item.change == 'move' ?><?cs
+     call:node_change(item, 'mv', 'moved') ?><?cs
+    elif:item.change == 'edit' ?><?cs
+     call:node_change(item, 'mod', 'modified') ?><?cs
+    /if ?>
+   </li>
+  <?cs /each ?></ul>
+ </dd>
+</dl>
+
+<div class="diff">
+ <div id="legend">
+  <h3>Legend:</h3>
+  <dl>
+   <dt class="unmod"></dt><dd>Unmodified</dd>
+   <dt class="add"></dt><dd>Added</dd>
+   <dt class="rem"></dt><dd>Removed</dd>
+   <dt class="mod"></dt><dd>Modified</dd>
+   <dt class="cp"></dt><dd>Copied</dd>
+   <dt class="mv"></dt><dd>Moved</dd>
+  </dl>
+ </div>
+ <ul class="entries"><?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
+   var:item.rev.new ?> of this file in browser"><?cs
+   var:item.path.new ?></a></h2><?cs
+   if:len(item.props) ?><ul class="props"><?cs
+    each:prop = item.props ?><li>Property <strong><?cs
+     var:name(prop) ?></strong> <?cs
+     if:prop.old && prop.new ?>changed from <?cs
+     elif:!prop.old ?>set<?cs
+     else ?>deleted<?cs
+     /if ?><?cs
+     if:prop.old && prop.new ?><em><tt><?cs var:prop.old ?></tt></em><?cs /if ?><?cs
+     if:prop.new ?> to <em><tt><?cs var:prop.new ?></tt></em><?cs /if ?></li><?cs
+    /each ?></ul><?cs
+   /if ?><?cs
+   if:len(item.diff) ?><table class="<?cs
+    var:diff.style ?>" summary="Differences" cellspacing="0"><?cs
+    if:diff.style == 'sidebyside' ?>
+     <colgroup class="l"><col class="lineno" /><col class="content" /></colgroup>
+     <colgroup class="r"><col class="lineno" /><col class="content" /></colgroup>
+     <thead><tr>
+      <th colspan="2"><a href="<?cs
+       var:item.browser_href.old ?>" title="Show old rev. <?cs
+       var:item.rev.old ?> of <?cs var:item.path.old ?>">Revision <?cs
+       var:item.rev.old ?></a></th>
+      <th colspan="2"><a href="<?cs
+       var:item.browser_href.new ?>" title="Show new rev. <?cs
+       var:item.rev.new ?> of <?cs var:item.path.new ?>">Revision <?cs
+       var:item.rev.new ?></a></th>
+      </tr>
+     </thead><?cs
+     each:change = item.diff ?><tbody><?cs
+      call:diff_display(change, diff.style) ?></tbody><?cs
+      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr>
+       <th>&hellip;</th><td>&nbsp;</td><th>&hellip;</th><td>&nbsp;</td>
+      </tr></tbody><?cs /if ?><?cs
+     /each ?><?cs
+    else ?>
+     <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup>
+     <thead><tr>
+      <th title="Revision <?cs var:item.rev.old ?>"><a href="<?cs
+       var:item.browser_href.old ?>" title="Show old version of <?cs
+       var:item.path.old ?>">r<?cs var:item.rev.old ?></a></th>
+      <th title="Revision <?cs var:item.rev.new ?>"><a href="<?cs
+       var:item.browser_href.new ?>" title="Show new version of <?cs
+       var:item.path.new ?>">r<?cs var:item.rev.new ?></a></th>
+      <th>&nbsp;</th></tr>
+     </thead><?cs
+     each:change = item.diff ?><?cs
+      call:diff_display(change, diff.style) ?><?cs
+      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr>
+       <th>&hellip;</th><th>&hellip;</th><td>&nbsp;</td>
+      </tr></tbody><?cs /if ?><?cs
+     /each ?><?cs
+    /if ?></table><?cs
+   /if ?></li><?cs
+  /if ?><?cs
+ /each ?></ul>
+</div>
+
+</div>
+<?cs include "footer.cs"?>
diff -ruN -x .svn trac-0.9b2/templates/log.cs anydiff-branch/templates/log.cs
--- trac-0.9b2/templates/log.cs	2005-09-25 16:48:01.000000000 +0200
+++ anydiff-branch/templates/log.cs	2005-09-28 10:33:07.000000000 +0200
@@ -3,8 +3,9 @@
 
 <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 +62,7 @@
           title="Warning: by updating, you will clear the page history" />
   </div>
  </form>
+
  <div class="diff">
   <div id="legend">
    <h3>Legend:</h3>
@@ -74,9 +76,16 @@
    </dl>
   </div>
  </div>
+
+ <form action="<?cs var:log.href ?>" method="post">
+  <div class="buttons"><input type="submit" value="View changes" 
+       title="Diff from Old Revision to New Revision (select them below)" />
+ </div>
  <table id="chglist" class="listing">
   <thead>
    <tr>
+    <th>Old</th>
+    <th>New</th>
     <th class="change"></th>
     <th class="data">Date</th>
     <th class="rev">Rev</th>
@@ -87,10 +96,11 @@
   </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 ?>">
-      <td class="copyfrom_path" colspan="6" style="padding-left: <?cs var:indent ?>em">
+      <td class="copyfrom_path" colspan="8" style="padding-left: <?cs var:indent ?>em">
        copied from <a href="<?cs var:item.browser_href ?>"?><?cs var:item.copyfrom_path ?></a>:
       </td>
      </tr><?cs
@@ -99,6 +109,12 @@
       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.path ?>#<?cs var:item.rev ?>" <?cs
+          if:idx == #1 ?> checked="checked" <?cs /if ?> /></td>
+     <td><input type="radio" name="new" 
+                value="<?cs var:item.path ?>#<?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 ?>">
        <span class="<?cs var:item.change ?>"></span>
@@ -117,9 +133,14 @@
      <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>
- </table><?cs
+ </table>
+ <div class="buttons"><input type="submit" value="View changes" 
+      title="Diff from Old Revision to New Revision (select them above)" />
+ </div>
+ </form><?cs
  if:len(links.prev) || len(links.next) ?><div id="paging" class="nav"><ul><?cs
   if:len(links.prev) ?><li class="first<?cs
    if:!len(links.next) ?> last<?cs /if ?>">&larr; <a href="<?cs
diff -ruN -x .svn trac-0.9b2/templates/wiki.cs anydiff-branch/templates/wiki.cs
--- trac-0.9b2/templates/wiki.cs	2005-09-25 16:48:01.000000000 +0200
+++ anydiff-branch/templates/wiki.cs	2005-09-28 10:33:07.000000000 +0200
@@ -154,6 +154,9 @@
     var:wiki.page_name ?></a></h1>
   <?cs if:len(wiki.history) ?><form method="get" action="">
    <input type="hidden" name="action" value="diff" />
+   <div class="buttons">
+    <input type="submit" value="View changes" />
+   </div>
    <table id="wikihist" class="listing" summary="Change history">
     <thead><tr>
      <th class="diff"></th>
diff -ruN -x .svn trac-0.9b2/trac/__init__.py anydiff-branch/trac/__init__.py
--- trac-0.9b2/trac/__init__.py	2005-09-25 16:48:00.000000000 +0200
+++ anydiff-branch/trac/__init__.py	2005-09-28 10:33:06.000000000 +0200
@@ -10,7 +10,7 @@
 """
 __docformat__ = 'epytext en'
 
-__version__ = '0.9b2'
+__version__ = '0.9b2-anydiff'
 __url__ = 'http://trac.edgewall.com/'
 __copyright__ = '(C) 2003-2005 Edgewall Software'
 __license__ = 'BSD'
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/api.py anydiff-branch/trac/versioncontrol/api.py
--- trac-0.9b2/trac/versioncontrol/api.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/api.py	2005-09-28 10:33:04.000000000 +0200
@@ -39,6 +39,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
@@ -111,7 +117,17 @@
         'None' is a valid revision value and represents the youngest revision.
         """
         return NotImplementedError
-        
+
+    def get_deltas(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
+        """
+        Generator that yields change tuples (old_node, new_node, kind, change)
+        for each node change between the two arbitrary (path,rev) pairs.
+
+        The old_node is assumed to be None when the change is an ADD,
+        the new_node is assumed to be None when the change is a DELETE.
+        """
+        raise NotImplementedError
+
 
 class Node(object):
     """
@@ -149,9 +165,22 @@
         node (if the underlying version control system supports that), which
         will be indicated by the first element of the tuple (i.e. the path)
         changing.
+        Starts with an entry for the current revision.
         """
         raise NotImplementedError
 
+    def get_previous(self):
+        """
+        Return the (path, rev, chg) tuple corresponding to the previous
+        revision for that node.
+        """
+        skip = True
+        for p in self.get_history(2):
+            if skip:
+                skip = False
+            else:
+                return p
+
     def get_properties(self):
         """
         Returns a dictionary containing the properties (meta-data) of the node.
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/cache.py anydiff-branch/trac/versioncontrol/cache.py
--- trac-0.9b2/trac/versioncontrol/cache.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/cache.py	2005-09-07 17:25:25.000000000 +0200
@@ -84,6 +84,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
 
@@ -108,6 +111,9 @@
     def normalize_rev(self, rev):
         return self.repos.normalize_rev(rev)
 
+    def get_deltas(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
+        return self.repos.get_deltas(old_path, old_rev, new_path, new_rev, ignore_ancestry)
+
 
 class CachedChangeset(Changeset):
 
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/diff.py anydiff-branch/trac/versioncontrol/diff.py
--- trac-0.9b2/trac/versioncontrol/diff.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/diff.py	2005-09-28 10:33:04.000000000 +0200
@@ -218,6 +218,8 @@
                            ignore_space_changes)
     for group in _group_opcodes(opcodes, context):
         i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
+        if i1 == 0 and i2 == 0:
+            i1, i2 = -1, -1 # support for 'A'dd changes
         yield '@@ -%d,%d +%d,%d @@' % (i1 + 1, i2 - i1, j1 + 1, j2 - j1)
         for tag, i1, i2, j1, j2 in group:
             if tag == 'equal':
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/svn_fs.py anydiff-branch/trac/versioncontrol/svn_fs.py
--- trac-0.9b2/trac/versioncontrol/svn_fs.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/svn_fs.py	2005-09-28 10:33:04.000000000 +0200
@@ -207,6 +207,11 @@
     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 (not path or path == '/') and '/' or path.strip('/')
 
@@ -298,9 +303,7 @@
         subpool = Pool(self.pool)
         while rev:
             subpool.clear()
-            rev_root = fs.revision_root(self.fs_ptr, rev, subpool())
-            node_type = fs.check_path(rev_root, path, subpool())
-            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
@@ -330,6 +333,64 @@
                 expect_deletion = True
                 rev = self.previous_rev(rev)
 
+    def get_deltas(self, old_path, old_rev, new_path, new_rev,
+                   ignore_ancestry=0):
+        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)
+        else:
+            raise TracError, ('The Base for Diff is invalid: path %s'
+                              ' doesn\'t exist in revision %s' \
+                              % (old_path, old_rev))
+        if self.has_node(new_path, new_rev):
+            new_node = self.get_node(new_path, new_rev)
+        else:
+            raise TracError, ('The Target for Diff is invalid: path %s'
+                              ' doesn\'t exist in revision %s' \
+                              % (new_path, new_rev))
+        if new_node.kind != old_node.kind:
+            raise TracError, ('Diff mismatch: Base is a %s (%s in revision %s) '
+                              'and Target is a %s (%s in revision %s).' \
+                              % (old_node.kind, old_path, old_rev,
+                                 new_node.kind, new_path, new_rev))
+        subpool = Pool(self.pool)
+        if new_node.isdir:
+            editor = DiffChangeEditor()
+            e_ptr, e_baton = delta.make_editor(editor, subpool())
+            old_root = fs.revision_root(self.fs_ptr, old_rev, subpool())
+            new_root = fs.revision_root(self.fs_ptr, new_rev, subpool())
+            def authz_cb(root, path, pool): return 1
+            text_deltas = 0 # as this is anyway re-done in Diff.py...
+            entry_props = 0 # "... typically used only for working copy updates"
+            repos.svn_repos_dir_delta(old_root, old_path, '',
+                                      new_root, new_path,
+                                      e_ptr, e_baton, authz_cb,
+                                      text_deltas,
+                                      1, # directory
+                                      entry_props,
+                                      ignore_ancestry,
+                                      subpool())
+            for path, kind, change in editor.deltas:
+                old_node = new_node = None
+                if change != Changeset.ADD:
+                    old_node = self.get_node(posixpath.join(old_path, path),
+                                             old_rev)
+                if change != Changeset.DELETE:
+                    new_node = self.get_node(posixpath.join(new_path, path),
+                                             new_rev)
+                else:
+                    kind = _kindmap[fs.check_path(old_root, old_node.path,
+                                                  subpool())]
+                yield  (old_node, new_node, kind, change)
+        else:
+            old_root = fs.revision_root(self.fs_ptr, old_rev, subpool())
+            new_root = fs.revision_root(self.fs_ptr, new_rev, subpool())
+            if fs.contents_changed(old_root, old_path, new_root, new_path,
+                                   subpool()):
+                yield (old_node, new_node, Node.FILE, Changeset.EDIT)
+
 
 class SubversionNode(Node):
 
@@ -352,8 +413,12 @@
                                                self.pool())
         self.created_path = fs.node_created_path(self.root, self.scoped_path,
                                                  self.pool())
-        # 'created_path' differs from 'path' if the last operation is a copy,
-        # and furthermore, 'path' might not exist at 'create_rev'
+        # Note: 'created_path' differs from 'path' if the last change was a copy,
+        #        and furthermore, 'path' might not exist at 'create_rev'.
+        #        The only guarantees are:
+        #          * this node exists at (path,rev)
+        #          * the node existed at (created_path,created_rev)
+        # TODO: check node id
         self.rev = self.created_rev
         
         Node.__init__(self, path, self.rev, _kindmap[node_type])
@@ -393,6 +458,9 @@
         if newer:
             yield newer
 
+#    def get_previous(self):
+#        # FIXME: redo it with fs.node_history
+
     def get_properties(self):
         props = fs.node_proplist(self.root, self.scoped_path, self.pool())
         for name,value in props.items():
@@ -487,3 +555,47 @@
 
     def _get_prop(self, name):
         return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool())
+
+
+#
+# Delta editor for diffs between arbitrary nodes
+#
+# 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 = []
+    
+    # -- svn.delta.Editor callbacks
+
+    def open_root(self, base_revision, dir_pool):
+        return ('/', Changeset.EDIT)
+
+    def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev,
+                      dir_pool):
+        self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
+        return (path, Changeset.ADD)
+
+    def open_directory(self, path, dir_baton, base_revision, dir_pool):
+        return (path, dir_baton[1])
+
+    def change_dir_prop(self, dir_baton, name, value, pool):
+        path, change = dir_baton
+        if change != Changeset.ADD:
+            self.deltas.append((path, Node.DIRECTORY, change))
+
+    def delete_entry(self, path, revision, dir_baton, pool):
+        self.deltas.append((path, None, Changeset.DELETE))
+
+    def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision,
+                 dir_pool):
+        self.deltas.append((path, Node.FILE, Changeset.ADD))
+
+    def open_file(self, path, dir_baton, dummy_rev, file_pool):
+        self.deltas.append((path, Node.FILE, Changeset.EDIT))
+
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/tests/svn_fs.py anydiff-branch/trac/versioncontrol/tests/svn_fs.py
--- trac-0.9b2/trac/versioncontrol/tests/svn_fs.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/tests/svn_fs.py	2005-09-28 10:33:03.000000000 +0200
@@ -215,6 +215,72 @@
         self.assertEqual(('tags/v1', 7, 'unknown'), history.next())
         self.assertRaises(StopIteration, history.next)
 
+    # Diffs
+
+    def _cmp_diff(self, expected, got):
+        if expected[0]:
+            old = self.repos.get_node(*expected[0])
+            self.assertEqual((old.path, old.rev), (got[0].path, got[0].rev))
+        if expected[1]:
+            new = self.repos.get_node(*expected[1])
+            self.assertEqual((new.path, new.rev), (got[1].path, got[1].rev))
+        self.assertEqual(expected[2], (got[2], got[3]))
+        
+    def test_diff_file_different_revs(self):
+        diffs = self.repos.get_deltas('trunk/README.txt', 2, 'trunk/README.txt', 3)
+        self._cmp_diff((('trunk/README.txt', 2),
+                        ('trunk/README.txt', 3),
+                        (Node.FILE, Changeset.EDIT)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_file_different_files(self):
+        diffs = self.repos.get_deltas('branches/v1x/README.txt', 12,
+                                      'branches/v1x/README2.txt', 12)
+        self._cmp_diff((('branches/v1x/README.txt', 12),
+                        ('branches/v1x/README2.txt', 12),
+                        (Node.FILE, Changeset.EDIT)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_file_no_change(self):
+        diffs = self.repos.get_deltas('trunk/README.txt', 7,
+                                      'tags/v1/README.txt', 7)
+        self.assertRaises(StopIteration, diffs.next)
+ 
+    def test_diff_dir_different_revs(self):
+        diffs = self.repos.get_deltas('trunk', 4, 'trunk', 8)
+        self._cmp_diff((None, ('trunk/dir1/dir2', 8),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('trunk/dir1/dir3', 8),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('trunk/README2.txt', 6),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self._cmp_diff((('trunk/dir2', 4), None,
+                        (Node.DIRECTORY, Changeset.DELETE)), diffs.next())
+        self._cmp_diff((('trunk/dir3', 4), None,
+                        (Node.DIRECTORY, Changeset.DELETE)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_dir_different_dirs(self):
+        diffs = self.repos.get_deltas('trunk', 1, 'branches/v1x', 12)
+        self._cmp_diff((None, ('branches/v1x/dir1', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/dir1/dir2', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/dir1/dir3', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/README.txt', 12),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/README2.txt', 12),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_dir_no_change(self):
+        diffs = self.repos.get_deltas('trunk', 7,
+                                      'tags/v1', 7)
+        self.assertRaises(StopIteration, diffs.next)
+        
+    # Changesets
+
     def test_changeset_repos_creation(self):
         chgset = self.repos.get_changeset(0)
         self.assertEqual(0, chgset.rev)
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/browser.py anydiff-branch/trac/versioncontrol/web_ui/browser.py
--- trac-0.9b2/trac/versioncontrol/web_ui/browser.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/web_ui/browser.py	2005-09-28 10:33:04.000000000 +0200
@@ -88,21 +88,29 @@
 
         repos = self.env.get_repository(req.authname)
         node = repos.get_node(path, rev)
+        rev = repos.normalize_rev(rev)
 
         hidden_properties = [p.strip() for p
                              in self.config.get('browser', 'hide_properties',
                                                 'svk:merge').split(',')]
+
         req.hdf['title'] = path
-        req.hdf['browser'] = {
+        browser_hdf = {
             'path': path,
-            'revision': rev or repos.youngest_rev,
+            'revision': rev,
             'props': dict([(util.escape(name), util.escape(value))
                            for name, value in node.get_properties().items()
-                           if not name in hidden_properties]),
-            'href': util.escape(self.env.href.browser(path, rev=rev or
-                                                      repos.youngest_rev)),
-            'log_href': util.escape(self.env.href.log(path))
-        }
+                           if not name in hidden_properties])
+            }
+        browser_hrefs = {
+            'href': self.env.href.browser(path,rev=rev),
+            'restr_changeset_href': self.env.href.changeset(node.rev, path),
+            'anydiff_href': self.env.href.anydiff(),
+            'log_href': self.env.href.log(path)
+            }
+        browser_hdf.update(dict([(key, util.escape(href)) for key, href in
+                                 browser_hrefs.items()]))
+        req.hdf['browser'] = browser_hdf
 
         path_links = get_path_links(self.env.href, path, rev)
         if len(path_links) > 1:
@@ -162,7 +170,14 @@
 
         req.hdf['browser.items'] = info
         req.hdf['browser.changes'] = changes
-
+        if node.path != '':
+            zip_href = self.env.href.diff(node.path, new=rev, old=rev,
+                                          old_path='/', # special case (#238)
+                                          format='zip')
+            add_link(req, 'alternate', zip_href, 'Zip Archive',
+                     'application/zip', 'zip')
+        
+        
     def _render_file(self, req, repos, node, rev=None):
         req.perm.assert_permission('FILE_VIEW')
 
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/changeset.py anydiff-branch/trac/versioncontrol/web_ui/changeset.py
--- trac-0.9b2/trac/versioncontrol/web_ui/changeset.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/web_ui/changeset.py	2005-09-28 10:33:03.000000000 +0200
@@ -22,20 +22,18 @@
 
 from trac import mimeview, util
 from trac.core import *
-from trac.perm import IPermissionRequestor
 from trac.Search import ISearchSource, query_to_sql, shorten_result
 from trac.Timeline import ITimelineEventProvider
 from trac.versioncontrol import Changeset, Node
 from trac.versioncontrol.svn_authz import SubversionAuthorizer
-from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff
 from trac.web import IRequestHandler
-from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
+from trac.web.chrome import INavigationContributor
 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider
+from trac.versioncontrol.web_ui.diff import AbstractDiffModule
 
+class ChangesetModule(AbstractDiffModule):
 
-class ChangesetModule(Component):
-
-    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+    implements(INavigationContributor, 
                ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource)
 
     # INavigationContributor methods
@@ -46,53 +44,17 @@
     def get_navigation_items(self, req):
         return []
 
-    # IPermissionRequestor methods
-
-    def get_permission_actions(self):
-        return ['CHANGESET_VIEW']
-
-    # IRequestHandler methods
+    # (reimplemented) IRequestHandler methods
 
     def match_request(self, req):
-        match = re.match(r'/changeset/([0-9]+)$', req.path_info)
+        match = re.match(r'/changeset/([0-9]+)(/.*)?$', req.path_info)
         if match:
             req.args['rev'] = match.group(1)
+            path = match.group(2)
+            if path:
+                req.args['path'] = path
             return 1
 
-    def process_request(self, req):
-        req.perm.assert_permission('CHANGESET_VIEW')
-
-        rev = req.args.get('rev')
-        repos = self.env.get_repository(req.authname)
-        authzperm = SubversionAuthorizer(self.env, req.authname)
-        authzperm.assert_permission_for_changeset(rev)
-
-        diff_options = get_diff_options(req)
-        if req.args.has_key('update'):
-            req.redirect(self.env.href.changeset(rev))
-
-        chgset = repos.get_changeset(rev)
-        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)
-            return
-        elif format == 'zip':
-            self._render_zip(req, repos, chgset)
-            return
-
-        self._render_html(req, repos, chgset, diff_options)
-        add_link(req, 'alternate', '?format=diff', 'Unified Diff',
-                 'text/plain', 'diff')
-        add_link(req, 'alternate', '?format=zip', 'Zip Archive',
-                 'application/zip', 'zip')
-        add_stylesheet(req, 'common/css/changeset.css')
-        add_stylesheet(req, 'common/css/diff.css')
-        add_stylesheet(req, 'common/css/code.css')
-        return 'changeset.cs', None
-
     # ITimelineEventProvider methods
 
     def get_timeline_filters(self, req):
@@ -143,229 +105,35 @@
                           message
                 rev = repos.previous_rev(rev)
 
-    # Internal methods
-
-    def _render_html(self, req, repos, chgset, diff_options):
-        """HTML version"""
-        req.hdf['title'] = '[%s]' % chgset.rev
-        req.hdf['changeset'] = {
-            'revision': chgset.rev,
-            'time': util.format_datetime(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)
-
-        edits = []
-        idx = 0
-        for path, kind, change, base_path, base_rev in chgset.get_changes():
-            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 change in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE):
-                edits.append((idx, path, kind, base_path, base_rev))
-            req.hdf['changeset.changes.%d' % idx] = info
-            idx += 1
-
-        hidden_properties = [p.strip() for p
-                             in self.config.get('browser', 'hide_properties',
-                                                'svk:merge').split(',')]
-
-        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)
-
-            # Property changes
-            old_props = old_node.get_properties()
-            new_props = new_node.get_properties()
-            changed_props = {}
-            if old_props != new_props:
-                for k,v in old_props.items():
-                    if not k in new_props:
-                        changed_props[k] = {'old': v}
-                    elif v != new_props[k]:
-                        changed_props[k] = {'old': v, 'new': new_props[k]}
-                for k,v in new_props.items():
-                    if not k in old_props:
-                        changed_props[k] = {'new': v}
-                for k in hidden_properties:
-                    if k in changed_props:
-                        del changed_props[k]
-                req.hdf['changeset.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()
-            if mimeview.is_binary(old_content):
-                continue
-            charset = mimeview.get_charset(old_node.content_type) or \
-                      default_charset
-            old_content = util.to_utf8(old_content, charset)
-
-            new_content = new_node.get_content().read()
-            if mimeview.is_binary(new_content):
-                continue
-            charset = mimeview.get_charset(new_node.content_type) or \
-                      default_charset
-            new_content = util.to_utf8(new_content, charset)
-
-            if old_content != new_content:
-                context = 3
-                for option in diff_options[1]:
-                    if option.startswith('-U'):
-                        context = int(option[2:])
-                        break
-                tabwidth = int(self.config.get('diff', 'tab_width',
-                                               self.config.get('mimeviewer',
-                                                               'tab_width')))
-                changes = hdf_diff(old_content.splitlines(),
-                                   new_content.splitlines(),
-                                   context, tabwidth,
-                                   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
-
-    def _render_diff(self, req, repos, chgset, diff_options):
-        """Raw Unified Diff version"""
-        req.send_response(200)
-        req.send_header('Content-Type', 'text/plain;charset=utf-8')
-        req.send_header('Content-Disposition',
-                        'filename=Changeset%s.diff' % req.args.get('rev'))
-        req.end_headers()
-
-        for path, kind, change, base_path, base_rev in chgset.get_changes():
-            if change == Changeset.ADD:
-                old_node = None
-            else:
-                old_node = repos.get_node(base_path or path, base_rev)
-            if change == Changeset.DELETE:
-                new_node = None
-            else:
-                new_node = repos.get_node(path, chgset.rev)
-
-            # TODO: Property changes
-
-            # Content changes
-            if kind == 'dir':
-                continue
-
-            default_charset = self.config.get('trac', 'default_charset')
-            new_content = old_content = ''
-            new_node_info = old_node_info = ('','')
-
-            if old_node:
-                charset = mimeview.get_charset(old_node.content_type) or \
-                          default_charset
-                old_content = util.to_utf8(old_node.get_content().read(),
-                                           charset)
-                old_node_info = (old_node.path, old_node.rev)
-            if mimeview.is_binary(old_content):
-                continue
-
-            if new_node:
-                charset = mimeview.get_charset(new_node.content_type) or \
-                          default_charset
-                new_content = util.to_utf8(new_node.get_content().read(),
-                                           charset)
-                new_node_info = (new_node.path, new_node.rev)
-            if mimeview.is_binary(new_content):
-                continue
-
-            if old_content != new_content:
-                context = 3
-                for option in diff_options[1]:
-                    if option.startswith('-U'):
-                        context = int(option[2:])
-                        break
-                req.write('Index: ' + path + util.CRLF)
-                req.write('=' * 67 + util.CRLF)
-                req.write('--- %s (revision %s)' % old_node_info +
-                          util.CRLF)
-                req.write('+++ %s (revision %s)' % new_node_info +
-                          util.CRLF)
-                for line in unified_diff(old_content.splitlines(),
-                                         new_content.splitlines(), context,
-                                         ignore_blank_lines='-B' in diff_options[1],
-                                         ignore_case='-i' in diff_options[1],
-                                         ignore_space_changes='-b' in diff_options[1]):
-                    req.write(line + util.CRLF)
-
-    def _render_zip(self, req, repos, chgset):
-        """ZIP archive with all the added and/or modified files."""
-        req.send_response(200)
-        req.send_header('Content-Type', 'application/zip')
-        req.send_header('Content-Disposition',
-                        'filename=Changeset%s.zip' % chgset.rev)
-        req.end_headers()
-
-        try:
-            from cStringIO import StringIO
-        except ImportError:
-            from StringIO import StringIO
-        from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
-
-        buf = StringIO()
-        zipfile = ZipFile(buf, 'w', ZIP_DEFLATED)
-        for path, kind, change, base_path, base_rev in chgset.get_changes():
-            if kind == Node.FILE and change != Changeset.DELETE:
-                node = repos.get_node(path, chgset.rev)
-                zipinfo = ZipInfo()
-                zipinfo.filename = node.path
-                zipinfo.date_time = time.gmtime(node.last_modified)[:6]
-                zipinfo.compress_type = ZIP_DEFLATED
-                zipfile.writestr(zipinfo, node.get_content().read())
-        zipfile.close()
-        req.write(buf.getvalue())
 
     # IWikiSyntaxProvider methods
     
     def get_wiki_syntax(self):
-        yield (r"!?\[\d+\]|(?:\b|!)r\d+\b(?!:\d)",
+        yield (r"!?\[\d+(?:/[^\]]*)?\]|(?:\b|!)r\d+\b(?!:\d)",
                lambda x, y, z: self._format_link(x, 'changeset',
                                                  y[0] == 'r' and y[1:]
-                                                 or y[1:-1], y))
+                                                 or y[1:-1], y, z))
 
     def get_link_resolvers(self):
         yield ('changeset', self._format_link)
 
-    def _format_link(self, formatter, ns, rev, label):
+    def _format_link(self, formatter, ns, chgset, label, fullmatch=None):
+        sep = chgset.find('/')
+        if sep > 0:
+            rev, path = chgset[:sep], chgset[sep:]
+        else:
+            rev, path = chgset, None
         cursor = formatter.db.cursor()
         cursor.execute('SELECT message FROM revision WHERE rev=%s', (rev,))
         row = cursor.fetchone()
         if row:
             return '<a class="changeset" title="%s" href="%s">%s</a>' \
                    % (util.escape(util.shorten_line(row[0])),
-                      formatter.href.changeset(rev), label)
+                      formatter.href.changeset(rev, path), label)
         else:
-            return '<a class="missing changeset" href="%s" rel="nofollow">%s</a>' \
-                   % (formatter.href.changeset(rev), label)
+            return '<a class="missing changeset" href="%s"' \
+                   ' rel="nofollow">%s</a>' \
+                   % (formatter.href.changeset(rev, path), label)
 
     # ISearchProvider methods
 
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/diff.py anydiff-branch/trac/versioncontrol/web_ui/diff.py
--- trac-0.9b2/trac/versioncontrol/web_ui/diff.py	1970-01-01 01:00:00.000000000 +0100
+++ anydiff-branch/trac/versioncontrol/web_ui/diff.py	2005-09-28 10:33:03.000000000 +0200
@@ -0,0 +1,597 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+#         Christian Boos <cboos@neuf.fr>
+
+from __future__ import generators
+import time
+import re
+import posixpath
+from urllib import urlencode
+
+from trac import mimeview, util
+from trac.core import *
+from trac.perm import IPermissionRequestor
+from trac.versioncontrol import Changeset, Node
+from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff
+from trac.versioncontrol.svn_authz import SubversionAuthorizer
+from trac.web import IRequestHandler
+from trac.web.chrome import add_link, add_stylesheet
+from trac.wiki import wiki_to_html, IWikiSyntaxProvider
+
+class DiffArgs(dict):
+    def __getattr__(self,str):
+        return self[str]
+    
+
+class ChangesPermission(Component):
+    """Simple permission provider for changes related modules."""
+    
+    implements(IPermissionRequestor)
+    
+    def get_permission_actions(self):
+        return ['CHANGESET_VIEW']
+    
+
+class AbstractDiffModule(Component):
+    """Provide flexible functionality for showing sets of differences.
+
+    If the differences shown are coming from a specific changeset,
+    then that changeset informations can be shown too.
+
+    In addition, it is possible to show only a subset of the changeset:
+    Only the changes affecting a given path will be shown.
+    This is called the ''restricted'' changeset.
+
+    But the differences can also be computed in a more general way,
+    between two arbitrary paths and/or between two arbitrary revisions.
+    In that case, there's no changeset information displayed.
+    """
+
+    abstract = True
+
+    implements(IRequestHandler)
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        raise NotImplementedError
+
+    def process_request(self, req):
+        """The appropriate mode of operation is inferred from
+        the request parameters:
+         * If `old` and `new` parameters are given, it will be an
+           arbitrary set of differences: `chgset` is False.
+           * If `old_path` is given and is different from `path`,
+             it's a generalized diff, from `old_path@old`
+             to `path@new`: `restricted` is False.
+           * Otherwise those are differences between two arbitrary revisions
+             of a given path: `restricted` is True.
+         * Otherwise, we are dealing with a changeset only: `chgset` is True.
+           * If the `path` is not empty or not the root, then only
+             the changes affecting that path (i.e. itself, children or
+             ancestors) will be considered: `restricted` is True.
+           * Otherwise, it's the full changeset: `restricted` is False.
+         
+        In any case, the given path@rev pair must exist.
+        """
+        req.perm.assert_permission('CHANGESET_VIEW')
+        
+        # -- retrieve arguments
+        path = req.args.get('path')
+        rev = req.args.get('rev')
+        old = req.args.get('old')
+        new = req.args.get('new')
+        old_path = req.args.get('old_path')
+
+        # -- normalize and check for special case
+        repos = self.env.get_repository(req.authname)
+        path = repos.normalize_path(path)
+        rev = repos.normalize_rev(rev)
+        if old_path: # Note: normalize_path now returns '/' if given 'None'
+            old_path = repos.normalize_path(old_path)
+        
+        authzperm = SubversionAuthorizer(self.env, req.authname)
+        authzperm.assert_permission_for_changeset(rev)
+
+        if old_path == path and old and old == new: # revert to Changeset
+            rev = old
+            old_path = old = new = None
+
+        diff_options = get_diff_options(req)
+
+        # -- setup the `chgset` and `restricted` flags, see docstring above.
+        chgset = not old and not new and not old_path
+        if chgset:
+            restricted = path != '' and path != '/' # (subset or not)
+        else:
+            restricted = old_path == path # (same path or not)
+
+        # -- redirect if changing the diff options
+        if req.args.has_key('update'):
+            if chgset:
+                if restricted:
+                    req.redirect(self.env.href.diff(path, rev=rev))
+                else:
+                    req.redirect(self.env.href.changeset(rev))
+            else:
+                req.redirect(self.env.href.diff(path, new=new,
+                                                old_path=old_path, old=old))
+
+        # -- preparing the diff arguments
+        if chgset:
+            prev = repos.get_node(path, rev).get_previous()
+            if prev:
+                prev_path, prev_rev = prev[:2]
+            else:
+                prev_path, prev_rev = path, repos.previous_rev(rev)
+            diff_args = DiffArgs(old_path=prev_path, old_rev=prev_rev,
+                                 new_path=path, new_rev=rev)
+        else:
+            if not new:
+                new = repos.youngest_rev
+            elif not old:
+                old = repos.youngest_rev
+            if not old_path:
+                old_path = path
+            diff_args = DiffArgs(old_path=old_path, old_rev=old,
+                                 new_path=path, new_rev=new)
+        if chgset:
+            chgset = repos.get_changeset(rev)
+            req.check_modified(chgset.date,
+                               diff_options[0] + ''.join(diff_options[1]))
+        else:
+            pass # FIXME: what date should we choose for a diff?
+
+        req.hdf['diff'] = diff_args
+
+        format = req.args.get('format')
+
+        if format in ['diff', 'zip']:
+            # choosing an appropriate filename
+            rpath = path.replace('/','_')
+            if chgset:
+                if restricted:
+                    filename = 'changeset_%s_r%s' % (rpath, rev)
+                else:
+                    filename = 'changeset_r%s' % rev
+            else:
+                if restricted:
+                    filename = 'diff-%s-from-r%s-to-r%s' \
+                                  % (rpath, old, new)
+                elif old_path == '/': # special case for download (#238)
+                    filename = '%s-r%s' % (rpath, old)
+                else:
+                    filename = 'diff-from-%s-r%s-to-%s-r%s' \
+                               % (old_path.replace('/','_'), old, rpath, new)
+            if format == 'diff':
+                self._render_diff(req, filename, repos, diff_args,
+                                  diff_options)
+                return
+            elif format == 'zip':
+                self._render_zip(req, filename, repos, diff_args)
+                return
+
+        # -- HTML format
+        self._render_html(req, repos, chgset, restricted,
+                          diff_args, diff_options)
+        if chgset:
+            diff_params = 'rev=%s' % rev
+        else:
+            diff_params = urlencode({'path': path,
+                                     'new': new,
+                                     'old_path': old_path,
+                                     'old': old})
+        add_link(req, 'alternate', '?format=diff&'+diff_params, 'Unified Diff',
+                 'text/plain', 'diff')
+        add_link(req, 'alternate', '?format=zip&'+diff_params, 'Zip Archive',
+                 'application/zip', 'zip')
+        add_stylesheet(req, 'common/css/changeset.css')
+        add_stylesheet(req, 'common/css/diff.css')
+        add_stylesheet(req, 'common/css/code.css')
+        return 'diff.cs', None
+
+
+    # Internal methods
+
+    def _render_html(self, req, repos, chgset, restricted, diff, diff_options):
+        """
+        HTML version
+        """
+        req.hdf['diff'] = {
+            'chgset': chgset and True,
+            'restricted': restricted,
+            '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: # Changeset Mode (possibly restricted on a path)
+            path, rev = diff.new_path, diff.new_rev
+
+            # -- getting the deltas from the Changeset.get_changes method
+            def get_deltas():
+                old_node = new_node = None
+                for npath, kind, change, opath, orev in chgset.get_changes():
+                    if restricted and \
+                           not (npath.startswith(path)      # npath is below
+                                or path.startswith(npath)): # npath is above
+                        continue
+                    if change != Changeset.ADD:
+                        old_node = repos.get_node(opath, orev)
+                    if change != Changeset.DELETE:
+                        new_node = repos.get_node(npath, rev)
+                    yield old_node, new_node, kind, change
+                    
+            def _changeset_title(rev):
+                if restricted:
+                    return 'Changeset %s for %s' % (rev, path)
+                else:
+                    return 'Changeset %s' % rev
+
+            title = _changeset_title(rev)
+            req.hdf['changeset'] = {
+                'revision': chgset.rev,
+                'time': util.format_datetime(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:
+                if restricted:
+                    prev = repos.get_node(path, rev).get_previous()
+                    if prev:
+                        prev_path, prev_rev = prev[:2]
+                        prev_href = self.env.href.changeset(prev_rev, prev_path)
+                    else:
+                        prev_path = prev_rev = None
+                else:
+                    prev_path = diff.old_path
+                    prev_rev = repos.previous_rev(chgset.rev)
+                    add_link(req, 'first', self.env.href.changeset(oldest_rev),
+                             'Changeset %s' % oldest_rev)
+                    prev_href = self.env.href.changeset(prev_rev)
+                if prev_rev:
+                    add_link(req, 'prev', prev_href, _changeset_title(prev_rev))
+            youngest_rev = repos.youngest_rev
+            if str(chgset.rev) != str(youngest_rev):
+                if restricted:
+                    next_rev = next_href = None
+                    # FIXME: find an effective way to find the next rev
+                else:
+                    next_rev = repos.next_rev(chgset.rev)
+                    next_href = self.env.href.changeset(next_rev)
+                    add_link(req, 'last',
+                             self.env.href.diff(path, rev=youngest_rev),
+                             'Changeset %s' % youngest_rev)
+                if next_rev:
+                    add_link(req, 'next', next_href, _changeset_title(next_rev))
+
+        else: # Diff Mode
+            # -- getting the deltas from the Repository.get_deltas method
+            def get_deltas():
+                for d in repos.get_deltas(**diff):
+                    yield d
+                    
+            reverse_href = self.env.href.diff(diff.old_path,
+                                              new=diff.old_rev,
+                                              old_path=diff.new_path,
+                                              old=diff.new_rev)
+            req.hdf['diff.reverse_href'] = reverse_href
+            title = self.title_for_diff(diff)
+        req.hdf['title'] = title
+
+        def _change_info(old_node, new_node, change):
+            info = {'change': change}
+            if old_node:
+                info['path.old'] = old_node.path
+                info['rev.old'] = old_node.rev # this is the created rev.
+                old_href = self.env.href.browser(old_node.path,
+                                                 rev=diff.old_rev)
+                # Reminder: old_node.path may not exist at old_node.rev
+                info['browser_href.old'] = old_href
+            if new_node:
+                info['path.new'] = new_node.path
+                info['rev.new'] = new_node.rev # created rev.
+                new_href = self.env.href.browser(new_node.path,
+                                                 rev=diff.new_rev)
+                # (same remark as above)
+                info['browser_href.new'] = new_href
+            return info
+
+        hidden_properties = [p.strip() for p
+                             in self.config.get('browser', 'hide_properties',
+                                                'svk:merge').split(',')]
+
+        def _prop_changes(old_node, new_node):
+            old_props = old_node.get_properties()
+            new_props = new_node.get_properties()
+            changed_props = {}
+            if old_props != new_props:
+                for k,v in old_props.items():
+                    if not k in new_props:
+                        changed_props[k] = {'old': v}
+                    elif v != new_props[k]:
+                        changed_props[k] = {'old': v, 'new': new_props[k]}
+                for k,v in new_props.items():
+                    if not k in old_props:
+                        changed_props[k] = {'new': v}
+                for k in hidden_properties:
+                    if k in changed_props:
+                        del changed_props[k]
+            return changed_props
+
+        def _content_changes(old_node, new_node):
+            """
+            Returns the list of differences.
+            The list is empty when no differences between comparable files
+            are detected, but the return value is None for non-comparable files.
+            """
+            default_charset = self.config.get('trac', 'default_charset')
+            old_content = old_node.get_content().read()            
+            if mimeview.is_binary(old_content):
+                return None
+            charset = mimeview.get_charset(old_node.content_type) or \
+                      default_charset
+            old_content = util.to_utf8(old_content, charset)
+
+            new_content = new_node.get_content().read()
+            if mimeview.is_binary(new_content):
+                return None
+            charset = mimeview.get_charset(new_node.content_type) or \
+                      default_charset
+            new_content = util.to_utf8(new_content, charset)
+
+            if old_content != new_content:
+                context = 3
+                options = diff_options[1]
+                for option in options:
+                    if option.startswith('-U'):
+                        context = int(option[2:])
+                        break
+                tabwidth = int(self.config.get('diff', 'tab_width',
+                                               self.config.get('mimeviewer',
+                                                               'tab_width')))
+                return hdf_diff(old_content.splitlines(),
+                                new_content.splitlines(),
+                                context, tabwidth,
+                                ignore_blank_lines='-B' in options,
+                                ignore_case='-i' in options,
+                                ignore_space_changes='-b' in options)
+            else:
+                return []
+
+        idx = 0
+        for old_node, new_node, kind, change in get_deltas():
+            if change != Changeset.EDIT:
+                show_entry = True
+            else:
+                show_entry = False
+                assert old_node and new_node
+                props = _prop_changes(old_node, new_node)
+                if props:
+                    req.hdf['diff.changes.%d.props' % idx] = props
+                    show_entry = True
+                if kind == Node.FILE:
+                    diffs = _content_changes(old_node, new_node)
+                    if diffs != []:
+                        if diffs:
+                            req.hdf['diff.changes.%d.diff' % idx] = diffs
+                        # elif None (means: manually compare to (previous))
+                        show_entry = True
+            if show_entry:
+                info = _change_info(old_node, new_node, change)
+                req.hdf['diff.changes.%d' % idx] = info
+            idx += 1 # the sequence should be immutable
+
+    def _render_diff(self, req, filename, repos, diff, diff_options):
+        """Raw Unified Diff version"""
+        req.send_response(200)
+        req.send_header('Content-Type', 'text/plain;charset=utf-8')
+        req.send_header('Content-Disposition',
+                        'filename=%s.diff' % filename)
+        req.end_headers()
+
+        for old_node, new_node, kind, change in repos.get_deltas(**diff):
+            # TODO: Property changes
+
+            # Content changes
+            if kind == Node.DIRECTORY:
+                continue
+
+            default_charset = self.config.get('trac', 'default_charset')
+            new_content = old_content = ''
+            new_node_info = old_node_info = ('','')
+
+            if old_node:
+                charset = mimeview.get_charset(old_node.content_type) or \
+                          default_charset
+                old_content = util.to_utf8(old_node.get_content().read(),
+                                           charset)
+                old_node_info = (old_node.path, old_node.rev)
+                if mimeview.is_binary(old_content):
+                    continue
+
+            if new_node:
+                charset = mimeview.get_charset(new_node.content_type) or \
+                          default_charset
+                new_content = util.to_utf8(new_node.get_content().read(),
+                                           charset)
+                new_node_info = (new_node.path, new_node.rev)
+                if mimeview.is_binary(new_content):
+                    continue
+                new_path = new_node.path
+            else:
+                old_node_path = repos.normalize_path(old_node.path)
+                diff_old_path = repos.normalize_path(diff.old_path)
+                new_path = posixpath.join(diff.new_path,
+                                          old_node_path[len(diff_old_path)+1:])
+
+            if old_content != new_content:
+                context = 3
+                options = diff_options[1]
+                for option in options:
+                    if option.startswith('-U'):
+                        context = int(option[2:])
+                        break
+                if not old_node_info[0]:
+                    old_node_info = new_node_info # support for 'A'dd changes
+                req.write('Index: ' + new_path + util.CRLF)
+                req.write('=' * 67 + util.CRLF)
+                req.write('--- %s (revision %s)' % old_node_info +
+                          util.CRLF)
+                req.write('+++ %s (revision %s)' % new_node_info +
+                          util.CRLF)
+                for line in unified_diff(old_content.splitlines(),
+                                         new_content.splitlines(), context,
+                                         ignore_blank_lines='-B' in options,
+                                         ignore_case='-i' in options,
+                                         ignore_space_changes='-b' in options):
+                    req.write(line + util.CRLF)
+
+    def _render_zip(self, req, filename, repos, diff):
+        """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=%s.zip' % filename)
+
+        try:
+            from cStringIO import StringIO
+        except ImportError:
+            from StringIO import StringIO
+        from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
+
+        buf = StringIO()
+        zipfile = ZipFile(buf, 'w', ZIP_DEFLATED)
+        for old_node, new_node, kind, change in repos.get_deltas(**diff):
+            if kind == Node.FILE and change != Changeset.DELETE:
+                assert new_node
+                zipinfo = ZipInfo()
+                zipinfo.filename = new_node.path
+                zipinfo.date_time = time.gmtime(new_node.last_modified)[:6]
+                zipinfo.compress_type = ZIP_DEFLATED
+                zipfile.writestr(zipinfo, new_node.get_content().read())
+        zipfile.close()
+
+        buf.seek(0, 2) # be sure to be at the end
+        req.send_header("Content-Length", buf.tell())
+        req.end_headers()
+        
+        req.write(buf.getvalue())
+
+    def title_for_diff(self, diff):
+        if diff.new_path == diff.old_path: # ''diff between 2 revisions'' mode
+            return 'Diff r%s:%s for %s' \
+                   % (diff.old_rev or 'latest', diff.new_rev or 'latest',
+                      diff.new_path or '/')
+        else:                              # ''arbitrary diff'' mode
+            return 'Diff from %s@%s to %s@%s' \
+                   % (diff.old_path or '/', diff.old_rev or 'latest',
+                      diff.new_path or '/', diff.new_rev or 'latest')
+
+
+
+class DiffModule(AbstractDiffModule):
+
+    implements(IWikiSyntaxProvider)
+
+    # (reimplemented) IRequestHandler methods
+
+    def match_request(self, req):
+        match = re.match(r'/diff(?:(/.*)|$)', req.path_info)
+        if match:
+            if match.group(1):
+                req.args['path'] = match.group(1)
+            return 1
+
+    # IWikiSyntaxProvider methods
+    
+    def get_wiki_syntax(self):
+        return []
+
+    def get_link_resolvers(self):
+        yield ('diff', self._format_link)
+
+    def _format_link(self, formatter, ns, params, label):
+        def pathrev(path):
+            if '@' in path:
+                return path.split('@', 1)
+            else:
+                return (path, None)
+        if '//' in params:
+            p1, p2 = params.split('//', 1)
+            old, new = pathrev(p1), pathrev(p2)
+            diff = DiffArgs(old_path=old[0], old_rev=old[1],
+                            new_path=new[0], new_rev=new[1])
+        else: 
+            old_path, old_rev = pathrev(params)
+            new_rev = None
+            if old_rev and ':' in old_rev:
+                old_rev, new_rev = old_rev.split(':', 1)
+            diff = DiffArgs(old_path=old_path, old_rev=old_rev,
+                            new_path=old_path, new_rev=new_rev)
+        title = self.title_for_diff(diff)
+        href = formatter.href.diff(diff.new_path, new=diff.new_rev,
+                                   old_path=diff.old_path, old=diff.old_rev)
+        return '<a class="changeset" title="%s" href="%s">%s</a>' \
+               % (title, href, label)
+
+
+class AnyDiffModule(Component):
+
+    implements(IRequestHandler)
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return re.match(r'/anydiff$', req.path_info)
+
+    def process_request(self, req):
+        # -- retrieve arguments
+        new_path = req.args.get('new_path')
+        new_rev = req.args.get('new_rev')
+        old_path = req.args.get('old_path')
+        old_rev = req.args.get('old_rev')
+
+        # -- normalize 
+        repos = self.env.get_repository(req.authname)
+        new_path = repos.normalize_path(new_path)
+        new_rev = repos.normalize_rev(new_rev)
+        old_path = repos.normalize_path(old_path)
+        old_rev = repos.normalize_rev(old_rev)
+
+        authzperm = SubversionAuthorizer(self.env, req.authname)
+        authzperm.assert_permission_for_changeset(new_rev)
+        authzperm.assert_permission_for_changeset(old_rev)
+        
+        # -- prepare rendering
+        req.hdf['anydiff'] = {
+            'new_path': new_path,
+            'new_rev': new_rev,
+            'old_path': old_path,
+            'old_rev': old_rev,
+            'diff_href': self.env.href.diff(),
+            }
+
+        return 'anydiff.cs', None
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/__init__.py anydiff-branch/trac/versioncontrol/web_ui/__init__.py
--- trac-0.9b2/trac/versioncontrol/web_ui/__init__.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/web_ui/__init__.py	2005-09-28 10:33:03.000000000 +0200
@@ -1,3 +1,4 @@
 from trac.versioncontrol.web_ui.browser import *
 from trac.versioncontrol.web_ui.changeset import *
 from trac.versioncontrol.web_ui.log import *
+from trac.versioncontrol.web_ui.diff import *
diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/log.py anydiff-branch/trac/versioncontrol/web_ui/log.py
--- trac-0.9b2/trac/versioncontrol/web_ui/log.py	2005-09-25 16:47:58.000000000 +0200
+++ anydiff-branch/trac/versioncontrol/web_ui/log.py	2005-09-28 10:33:03.000000000 +0200
@@ -67,6 +67,20 @@
         stop_rev = req.args.get('stop_rev')
         verbose = req.args.get('verbose')
         limit = LOG_LIMIT
+        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))
+
+        if old and new:
+            osep = util.unescape(old).rindex('#')
+            nsep = util.unescape(new).rindex('#')
+            old_path, old_rev = old[:osep], old[osep+1:]
+            new_path, new_rev = new[:nsep], new[nsep+1:]
+            req.redirect(self.env.href.diff(new_path, new=new_rev,
+                                            old_path=old_path, old=old_rev))
 
         req.hdf['title'] = path + ' (log)'
         req.hdf['log'] = {
@@ -83,9 +97,6 @@
         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'' history uses `get_node()`,
         # ''Path'' history uses `get_path_history()`
diff -ruN -x .svn trac-0.9b2/trac/wiki/tests/wiki-tests.txt anydiff-branch/trac/wiki/tests/wiki-tests.txt
--- trac-0.9b2/trac/wiki/tests/wiki-tests.txt	2005-09-25 16:47:59.000000000 +0200
+++ anydiff-branch/trac/wiki/tests/wiki-tests.txt	2005-09-28 10:33:05.000000000 +0200
@@ -36,10 +36,14 @@
 <a class="ext-link" href="http://www.edgewall.com/"><span class="icon"></span>http://www.edgewall.com/</a>
 </p>
 ==============================
-#1, [1], r1, {1}
+#1, {1}
+[1], r1
+[1/README.txt]
 ------------------------------
 <p>
-<a class="missing ticket" href="/ticket/1" rel="nofollow">#1</a>, <a class="missing changeset" href="/changeset/1" rel="nofollow">[1]</a>, <a class="missing changeset" href="/changeset/1" rel="nofollow">r1</a>, <a class="report" href="/report/1">{1}</a>
+<a class="missing ticket" href="/ticket/1" rel="nofollow">#1</a>, <a class="report" href="/report/1">{1}</a>
+<a class="missing changeset" href="/changeset/1" rel="nofollow">[1]</a>, <a class="missing changeset" href="/changeset/1" rel="nofollow">r1</a>
+<a class="missing changeset" href="/changeset/1/README.txt" rel="nofollow">[1/README.txt]</a>
 </p>
 ==============================
 !#1, ![1], !r1, !{1}
@@ -60,12 +64,14 @@
 [1:2], r1:2, [12:23], r12:23
 </p>
 ==============================
-ticket:1, changeset:1, report:1, source:foo/bar
+ticket:1, report:1, source:foo/bar
+changeset:1, changeset:1/README.txt
 
 Issue [ticket:1], CS[changeset:1], Listing [report:1], File [source:foo/bar]
 ------------------------------
 <p>
-<a class="missing ticket" href="/ticket/1" rel="nofollow">ticket:1</a>, <a class="missing changeset" href="/changeset/1" rel="nofollow">changeset:1</a>, <a class="report" href="/report/1">report:1</a>, <a class="source" href="/browser/foo/bar">source:foo/bar</a>
+<a class="missing ticket" href="/ticket/1" rel="nofollow">ticket:1</a>, <a class="report" href="/report/1">report:1</a>, <a class="source" href="/browser/foo/bar">source:foo/bar</a>
+<a class="missing changeset" href="/changeset/1" rel="nofollow">changeset:1</a>, <a class="missing changeset" href="/changeset/1/README.txt" rel="nofollow">changeset:1/README.txt</a>
 </p>
 <p>
 Issue <a class="missing ticket" href="/ticket/1" rel="nofollow">1</a>, CS<a class="missing changeset" href="/changeset/1" rel="nofollow">1</a>, Listing <a class="report" href="/report/1">1</a>, File <a class="source" href="/browser/foo/bar">foo/bar</a>
@@ -79,6 +85,18 @@
 <a class="source" href="/browser/foo/bar">source foo/bar</a>, <a class="ext-link" href="http://www.edgewall.com/"><span class="icon"></span>edgewall</a>
 </p>
 ==============================
+diff:trunk//branch
+diff:trunk@12//branch@23
+diff:trunk@12:23
+diff:@12:23
+------------------------------
+<p>
+<a class="changeset" title="Diff from trunk@latest to branch@latest" href="/diff/branch?old_path=trunk">diff:trunk//branch</a>
+<a class="changeset" title="Diff from trunk@12 to branch@23" href="/diff/branch?new=23&old=12&old_path=trunk">diff:trunk@12//branch@23</a>
+<a class="changeset" title="Diff r12:23 for trunk" href="/diff/trunk?new=23&old=12&old_path=trunk">diff:trunk@12:23</a>
+<a class="changeset" title="Diff r12:23 for /" href="/diff/?new=23&old=12&old_path=">diff:@12:23</a>
+</p>
+==============================
 CamelCase AlabamA ABc AlaBamA FooBar
 ------------------------------
 <p>
diff -ruN -x .svn trac-0.9b2/wiki-default/WikiStart anydiff-branch/wiki-default/WikiStart
--- trac-0.9b2/wiki-default/WikiStart	2005-09-25 16:47:56.000000000 +0200
+++ anydiff-branch/wiki-default/WikiStart	2005-09-28 10:33:01.000000000 +0200
@@ -1,44 +1,44 @@
-= Welcome to Trac 0.9b2 =
-
-Trac is a '''minimalistic''' approach to '''web-based''' management of
-'''software projects'''. Its goal is to simplify effective tracking and handling of software issues, enhancements and overall progress.
-
-All aspects of Trac have been designed with the single goal to 
-'''help developers write great software''' while '''staying out of the way'''
-and imposing as little as possible on a team's established process and
-culture.
-
-As all Wiki pages, this page is editable, this means that you can
-modify the contents of this page simply by using your
-web-browser. Simply click on the "Edit this page" link at the bottom
-of the page. WikiFormatting will give you a detailed description of
-available Wiki formatting commands.
-
-"[wiki:TracAdmin trac-admin] ''yourenvdir'' initenv" created
-a new Trac environment, containing a default set of wiki pages and some sample
-data. This newly created environment also contains 
-[wiki:TracGuide documentation] to help you get started with your project.
-
-You can use [wiki:TracAdmin trac-admin] to configure
-[http://trac.edgewall.com/ Trac] to better fit your project, especially in
-regard to ''components'', ''versions'' and ''milestones''. 
-
-
-TracGuide is a good place to start.
-
-Enjoy! [[BR]]
-''The Trac Team''
-
-== Starting Points ==
-
- * TracGuide --  Built-in Documentation
- * [http://projects.edgewall.com/trac/ The Trac project] -- Trac Open Source Project
- * [http://projects.edgewall.com/trac/wiki/TracFaq Trac FAQ] -- Frequently Asked Questions
- * TracSupport --  Trac Support
-
-For a complete list of local wiki pages, see TitleIndex.
-
-Trac is brought to you by [http://www.edgewall.com/ Edgewall Software],
-providing professional Linux and software development services to clients
-worldwide. Visit http://www.edgewall.com/ for more information.
-
+= Welcome to Trac 0.9b2-anydiff =
+
+Trac is a '''minimalistic''' approach to '''web-based''' management of
+'''software projects'''. Its goal is to simplify effective tracking and handling of software issues, enhancements and overall progress.
+
+All aspects of Trac have been designed with the single goal to 
+'''help developers write great software''' while '''staying out of the way'''
+and imposing as little as possible on a team's established process and
+culture.
+
+As all Wiki pages, this page is editable, this means that you can
+modify the contents of this page simply by using your
+web-browser. Simply click on the "Edit this page" link at the bottom
+of the page. WikiFormatting will give you a detailed description of
+available Wiki formatting commands.
+
+"[wiki:TracAdmin trac-admin] ''yourenvdir'' initenv" created
+a new Trac environment, containing a default set of wiki pages and some sample
+data. This newly created environment also contains 
+[wiki:TracGuide documentation] to help you get started with your project.
+
+You can use [wiki:TracAdmin trac-admin] to configure
+[http://trac.edgewall.com/ Trac] to better fit your project, especially in
+regard to ''components'', ''versions'' and ''milestones''. 
+
+
+TracGuide is a good place to start.
+
+Enjoy! [[BR]]
+''The Trac Team''
+
+== Starting Points ==
+
+ * TracGuide --  Built-in Documentation
+ * [http://projects.edgewall.com/trac/ The Trac project] -- Trac Open Source Project
+ * [http://projects.edgewall.com/trac/wiki/TracFaq Trac FAQ] -- Frequently Asked Questions
+ * TracSupport --  Trac Support
+
+For a complete list of local wiki pages, see TitleIndex.
+
+Trac is brought to you by [http://www.edgewall.com/ Edgewall Software],
+providing professional Linux and software development services to clients
+worldwide. Visit http://www.edgewall.com/ for more information.
+

