[viewvc-dev] [PATCH] Show property diffs in ViewVC

Alexey Neyman <[email protected]> Fri, 30 Sep 2011 10:39:46 -0700
Newsgroups gmane.comp.version-control.cvs.viewcvs.devel
Message-ID <201109301039.47124.stilor__300.199891398911$1317406555$gmane$org@att.net>
Hi all,

Attached is a patch that allows ViewVC to show property changes in 
files/directories. I have not formally described it in template authoring 
guide yet, pending approval of the approach taken.

Some highlights of the patch:
- Property diff is displayed as a separate view, 'view=pdiff'
- Most of the diff.ezt template is moved to an include/diff_display.ezt, so 
that it could be used by both views (diff/pdiff)
- The arrays of changes in diff.ezt prefixed with diff (e.g. "sidebyside" -> 
"diff.sidebyside"), to allow iterating over multiple displayable diffs. This 
is needed by pdiff view to display multiple properties.
- The format of the property diff is controlled by a new config option, 
prop_diff_format, with the same allowed values as 'diff_format'. 
prop_diff_format defaults to 'f' - given that properties are typically not 
very long, I think it makes sense to display the whole value.
- New view is linked from the log.ezt (if root type is "svn"), from revision 
view ("property changed" is now a link) and from file header (which allows to 
switch between content and property diff)
- In diff.ezt, sidebyside and unified "sub-templates" now properly display if 
the .type attribute is set to "binary-diff" or "no-changes". "binary-diff" can 
now be generated by the property diff (if property value cannot be coerced 
into utf8); idiff.sidebyside and idiff.unified now return "no-changes" if, 
naturally, there are no changes.
- In viewvc.py, some code common to both the view_diff and the new view_pdiff 
functions has been moved into separate routines (look for is_undisplayable, 
get_diff_type, diff_side_item)
- try/except block removed from view_diff - this exception is generated and 
handled by setup_diff now.
- In viewvc-install, a minor bug is fixed which resulted in traceback if 
[Enter] was pressed in response to "overwrite/do not overwrite" question.

In a hindsight, I think the pdiff view may have been made a part of the 
regular diff view (with "props changed" link pointing to an anchor in the diff 
view). Please advise if you think it would be a better way.

The changes in diff.ezt template arguments could have been avoided if it were 
possible to alias "variables" in the templates. For example, this fails:

[define changes][diff.changes][end]

as the diff.changes is an array and ezt.py throws a traceback, failing to 
convert it to a printable form. I thought about something like:

[alias changes diff.chages]

which would make "diff.changes" be used wherever "changes" variable is 
encountered. Again, please advise if such new template command would be 
useful.

Regards,
Alexey.

Regards,
Alexey.

------------------------------------------------------
http://viewvc.tigris.org/ds/viewMessage.do?dsForumId=4251&dsMessageId=2848329

To unsubscribe from this discussion, e-mail: [[email protected]].
prop-diff.diff (text/x-patch, 50.9 KB)
Index: conf/viewvc.conf.dist
===================================================================
--- conf/viewvc.conf.dist	(revision 2598)
+++ conf/viewvc.conf.dist	(working copy)
@@ -338,8 +338,8 @@
 ## in this comma-delited list will not be served (or, will return an
 ## error on attempted access).
 ##
-## Valid items for this list include: "annotate", "co", "diff", "markup",
-## "roots", "tar".
+## Valid items for this list include: "annotate", "co", "diff", "pdiff",
+## "markup", "roots", "tar".
 ##
 ## ----------+---------------------------------------------------------
 ##    VIEW   |                       DESCRIPTION
@@ -360,6 +360,10 @@
 ##           | two revisions of a versioned file in a variety of
 ##           | different user-selectable formats.
 ## ----------+---------------------------------------------------------
+##  pdiff    | The 'pdiff' view displays line-based differences between
+##           | the properties of a file/directory in two different
+##           | revisions.
+## ----------+---------------------------------------------------------
 ##  markup   | The 'markup' view shows the contents of a single
 ##           | revision of a versioned file, with syntax highlighting
 ##           | where possible and enabled.  It can also optionally
@@ -375,7 +379,7 @@
 ##           | directory and its contents (recursively).
 ## ----------+---------------------------------------------------------
 ##
-#allowed_views = annotate, diff, markup, roots
+#allowed_views = annotate, diff, pdiff, markup, roots
 
 ## authorizer: The name of the ViewVC authorizer plugin to use when
 ## authorizing access to repository contents.  This value must be the
@@ -516,6 +520,16 @@
 ##
 #diff_format = h
 
+## prop_diff_format: Default property diff format
+##   h      Human readable
+##   u      Unified diff
+##   c      Context diff
+##   s      Side by side
+##   l      Long human readable (more context)
+##   f      Full human readable (entire file)
+##
+#prop_diff_format = f
+
 ## hr_breakable: Diff view line breaks
 ##   1     lines break at spaces
 ##   0     no line breaking
Index: templates/log.ezt
===================================================================
--- templates/log.ezt	(revision 2598)
+++ templates/log.ezt	(working copy)
@@ -105,42 +105,52 @@
   [is entries.state "dead"]
     <br /><strong><em>FILE REMOVED</em></strong>
   [else]
-   [is pathtype "file"]
-    [if-any entries.prev]
-      <br />Diff to <a href="[entries.diff_to_prev_href]">previous [entries.prev]</a>
-      [if-any human_readable]
-      [else]
-        (<a href="[entries.diff_to_prev_href]&amp;diff_format=h">colored</a>)
+    [is pathtype "file"]
+      [if-any entries.prev]
+	<br />Diff to <a href="[entries.diff_to_prev_href]">previous [entries.prev]</a>
+	[if-any human_readable]
+	[else]
+	  (<a href="[entries.diff_to_prev_href]&amp;diff_format=h">colored</a>)
+	[end]
       [end]
-    [end]
 
-    [is roottype "cvs"]
-      [if-any entries.branch_point]
-        , to <a href="[entries.diff_to_branch_href]">branch point [entries.branch_point]</a>
-        [if-any human_readable]
-        [else]
-           (<a href="[entries.diff_to_branch_href]&amp;diff_format=h">colored</a>)
-        [end]
+      [is roottype "cvs"]
+	[if-any entries.branch_point]
+	  , to <a href="[entries.diff_to_branch_href]">branch point [entries.branch_point]</a>
+	  [if-any human_readable]
+	  [else]
+	     (<a href="[entries.diff_to_branch_href]&amp;diff_format=h">colored</a>)
+	  [end]
+	[end]
+    
+	[if-any entries.next_main]
+	  , to <a href="[entries.diff_to_main_href]">next main [entries.next_main]</a>
+	  [if-any human_readable]
+	  [else]
+	     (<a href="[entries.diff_to_main_href]&amp;diff_format=h">colored</a>)
+	  [end]
+	[end]
       [end]
-  
-      [if-any entries.next_main]
-        , to <a href="[entries.diff_to_main_href]">next main [entries.next_main]</a>
-        [if-any human_readable]
-        [else]
-           (<a href="[entries.diff_to_main_href]&amp;diff_format=h">colored</a>)
-        [end]
+
+      [if-any entries.diff_to_sel_href]
+	[if-any entries.prev], [else]<br />Diff[end]
+	  to <a href="[entries.diff_to_sel_href]">selected [rev_selected]</a>
+	[if-any human_readable]
+	[else]
+	  (<a href="[entries.diff_to_sel_href]&amp;diff_format=h">colored</a>)
+	[end]
       [end]
     [end]
+    [is roottype "svn"]
+      [if-any entries.prev]
+        <br />Property diff to <a href="[entries.pdiff_to_prev_href]">previous [entries.prev]</a>
+      [end]
 
-    [if-any entries.diff_to_sel_href]
-      [if-any entries.prev], [else]<br />Diff[end]
-        to <a href="[entries.diff_to_sel_href]">selected [rev_selected]</a>
-      [if-any human_readable]
-      [else]
-        (<a href="[entries.diff_to_sel_href]&amp;diff_format=h">colored</a>)
+      [if-any entries.pdiff_to_sel_href]
+        [if-any entries.prev], [else]<br />Property diff[end]
+          to <a href="[entries.pdiff_to_sel_href]">selected [rev_selected]</a>
       [end]
     [end]
-   [end]
   [end]
 
 <pre class="vc_log">[entries.log]</pre>
Index: templates/include/file_header.ezt
===================================================================
--- templates/include/file_header.ezt	(revision 2598)
+++ templates/include/file_header.ezt	(working copy)
@@ -7,9 +7,15 @@
 [if-any graph_href]
   | <a href="[graph_href]"><img src="[docroot]/images/cvsgraph_16x16.png" class="vc_icon" alt="View Revision Graph" /> Revision Graph</a>
 [end]
-[is view "diff"]
+[if-any patch_href]
   | <a href="[patch_href]"><img src="[docroot]/images/diff.png" class="vc_icon" alt="View Patch" /> Patch</a>
 [end]
+[if-any diff_href]
+  | <a href="[diff_href]"><img src="[docroot]/images/diff.png" class="vc_icon" alt="View Diff" /> Diff</a>
+[end]
+[if-any pdiff_href]
+  | <a href="[pdiff_href]"><img src="[docroot]/images/diff.png" class="vc_icon" alt="View Property Diff" /> Property Diff</a>
+[end]
 [else]
 <a href="[view_href]"><img src="[docroot]/images/dir.png" class="vc_icon" alt="View Directory Listing" /> Directory Listing</a>
 [end]
Index: templates/include/diff_display.ezt
===================================================================
--- templates/include/diff_display.ezt	(revision 2594)
+++ templates/include/diff_display.ezt	(working copy)
@@ -1,21 +1,11 @@
-[# setup page definitions]
-  [define page_title]Diff of /[where][end]
-  [define help_href][docroot]/help_rootview.html[end]
-[# end]
-
-[include "include/header.ezt" "diff"]
-[include "include/file_header.ezt"]
-
-<h3 style="text-align:center;"></h3>
-
-[if-any raw_diff]
-  <pre class="vc_raw_diff">[raw_diff]</pre>
+[if-any diff.raw_diff]
+  <pre class="vc_raw_diff">[diff.raw_diff]</pre>
 [end]
 
 [define left_view_href][if-any left.prefer_markup][left.view_href][else][if-any left.download_href][left.download_href][end][end][end]
 [define right_view_href][if-any right.prefer_markup][right.view_href][else][if-any right.download_href][right.download_href][end][end][end]
 
-[if-any changes]
+[if-any diff.changes]
 <table cellspacing="0" cellpadding="0">
   <tr class="vc_diff_header">
     <th style="width:6%;"></th>
@@ -33,54 +23,54 @@
     </th>
   </tr>
 
-  [for changes]
-    [is changes.type "header"]
-      <tr class="vc_diff_chunk_header" id="h[changes.line_info_right]">
+  [for diff.changes]
+    [is diff.changes.type "header"]
+      <tr class="vc_diff_chunk_header" id="h[diff.changes.line_info_right]">
         <td style="width:6%;"><strong>#</strong></td>
         <td style="width:47%;">
-          <strong>Line [changes.line_info_left]</strong>&nbsp;
-          <span class="vc_diff_chunk_extra">[changes.line_info_extra]</span>
+          <strong>Line [diff.changes.line_info_left]</strong>&nbsp;
+          <span class="vc_diff_chunk_extra">[diff.changes.line_info_extra]</span>
         </td>
         <td style="width:47%;">
-          <strong>Line [changes.line_info_right]</strong>&nbsp;
-          <span class="vc_diff_chunk_extra">[changes.line_info_extra]</span>
+          <strong>Line [diff.changes.line_info_right]</strong>&nbsp;
+          <span class="vc_diff_chunk_extra">[diff.changes.line_info_extra]</span>
         </td>
       </tr>
     [else]
-      [is changes.type "add"]
+      [is diff.changes.type "add"]
         <tr>
-          <td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
+          <td class="vc_diff_line_number" id="l[diff.changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[diff.changes.line_number]">[diff.changes.line_number]</a>[else][diff.changes.line_number][end]</td>
           <td class="vc_diff_empty">&nbsp;</td>
-          <td class="vc_diff_add">&nbsp;[changes.right]</td>
+          <td class="vc_diff_add">&nbsp;[diff.changes.right]</td>
         </tr>
       [else]
-        [is changes.type "remove"]
+        [is diff.changes.type "remove"]
           <tr>
             <td></td>
-            <td class="vc_diff_remove">&nbsp;[changes.left]</td>
+            <td class="vc_diff_remove">&nbsp;[diff.changes.left]</td>
             <td class="vc_diff_empty">&nbsp;</td>
           </tr>
         [else]
-          [is changes.type "change"]
+          [is diff.changes.type "change"]
             <tr>
-              [if-any changes.have_right]
-                <td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
+              [if-any diff.changes.have_right]
+                <td class="vc_diff_line_number" id="l[diff.changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[diff.changes.line_number]">[diff.changes.line_number]</a>[else][diff.changes.line_number][end]</td>
               [else]
                 <td></td>
               [end]
-              [if-any changes.have_left]
-                <td class="vc_diff_change">&nbsp;[changes.left]</td>
+              [if-any diff.changes.have_left]
+                <td class="vc_diff_change">&nbsp;[diff.changes.left]</td>
               [else]
                 <td class="vc_diff_change_empty">&nbsp;</td>
               [end]
-              [if-any changes.have_right]
-                <td class="vc_diff_change">&nbsp;[changes.right]</td>
+              [if-any diff.changes.have_right]
+                <td class="vc_diff_change">&nbsp;[diff.changes.right]</td>
               [else]
                 <td class="vc_diff_change_empty">&nbsp;</td>
               [end]
             </tr>
           [else]
-            [is changes.type "no-changes"]
+            [is diff.changes.type "no-changes"]
               <tr>
                 <td colspan="3">&nbsp;</td>
               </tr>
@@ -89,16 +79,16 @@
                 <strong>- No changes -</strong><br />&nbsp; </td>
               </tr>
             [else]
-              [is changes.type "binary-diff"]
+              [is diff.changes.type "binary-diff"]
                 <tr>
                   <td colspan="3">&nbsp;</td>
                 </tr>
                 <tr class="vc_diff_empty">
                   <td colspan="3" style="text-align:center;"><br />
-                  <strong>- Binary file revisions differ -</strong><br />&nbsp; </td>
+                  <strong>- Binary content differs -</strong><br />&nbsp; </td>
                 </tr>
               [else]
-                [is changes.type "error"]
+                [is diff.changes.type "error"]
                   <tr>
                     <td colspan="3">&nbsp;</td>
                   </tr>
@@ -112,9 +102,9 @@
                   </tr>
                 [else]
                   <tr>
-                    <td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
-                    <td class="vc_diff_nochange">&nbsp;[changes.left]</td>
-                    <td class="vc_diff_nochange">&nbsp;[changes.right]</td>
+                    <td class="vc_diff_line_number" id="l[diff.changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[diff.changes.line_number]">[diff.changes.line_number]</a>[else][diff.changes.line_number][end]</td>
+                    <td class="vc_diff_nochange">&nbsp;[diff.changes.left]</td>
+                    <td class="vc_diff_nochange">&nbsp;[diff.changes.right]</td>
                   </tr>
                 [end]
               [end]
@@ -127,10 +117,10 @@
 </table>
 [end]
 
-[if-any sidebyside]
+[if-any diff.sidebyside]
   <table class="vc_idiff">
-    <colgroup><col /><col class="content" /></colgroup>
-    <colgroup><col /><col class="content" /></colgroup>
+    <colgroup><col style="width: 1%"/><col class="content"/></colgroup>
+    <colgroup><col style="width: 1%"/><col class="content"/></colgroup>
     <thead>
       <tr>
         <th colspan="2">    
@@ -144,25 +134,44 @@
       </tr>
     </thead>
     <tbody>
-      [for sidebyside]
-        [if-any sidebyside.gap]
-          <tr>
-            <th>&hellip;</th><th></th>
-            <th>&hellip;</th><th></th>
-          </tr>
-        [end]
-        <tr>
-          [for sidebyside.columns]
-            <th>[sidebyside.columns.line_number]</th><td[if-any sidebyside.columns.line_number][else] class="vc_idiff_empty"[end]>[for sidebyside.columns.segments][if-any sidebyside.columns.segments.type]<span class="vc_idiff_[sidebyside.columns.segments.type]">[sidebyside.columns.segments.text]</span>[else][sidebyside.columns.segments.text][end][end]</td>
-          [end]
-        </tr>
+      [for diff.sidebyside]
+        [is diff.sidebyside.type "no-changes"]
+	  <tr class="vc_diff_empty">
+	    <td colspan="4" style="text-align:center;"><br />
+	    <strong>- No changes -</strong><br />&nbsp; </td>
+	  </tr>
+        [else]
+	  [is diff.sidebyside.type "binary-diff"]
+	    <tr class="vc_diff_empty">
+	      <td colspan="4" style="text-align:center;"><br />
+	      <strong>- Binary content differs -</strong><br />&nbsp; </td>
+	    </tr>
+	  [else]
+	    [if-any diff.sidebyside.gap]
+	      <tr>
+		<th>&hellip;</th><th></th>
+		<th>&hellip;</th><th></th>
+	      </tr>
+	    [end]
+	    <tr>
+	      [for diff.sidebyside.columns]
+		<th>[diff.sidebyside.columns.line_number]</th><td[if-any diff.sidebyside.columns.line_number][else] class="vc_idiff_empty"[end]>[for diff.sidebyside.columns.segments][if-any diff.sidebyside.columns.segments.type]<span class="vc_idiff_[diff.sidebyside.columns.segments.type]">[diff.sidebyside.columns.segments.text]</span>[else][diff.sidebyside.columns.segments.text][end][end]</td>
+	      [end]
+	    </tr>
+	  [end]
+	[end]
       [end]
     </tbody>
   </table>
 [end]
 
-[if-any unified]
+[if-any diff.unified]
   <table class="vc_idiff">
+    <colgroup>
+      <col style="width: 1%"/>
+      <col style="width: 1%"/>
+      <col class="content"/>
+    </colgroup>
     <thead>
       <tr>
         <th>r[left.rev]</th>
@@ -171,70 +180,34 @@
       </tr>
     </thead>
     <tbody>
-      [for unified]
-        [if-any unified.gap]
-          <tr>
-            <th>&hellip;</th>
-            <th>&hellip;</th>
-            <th></th>
-          </tr>
-        [end]
-        <tr>
-          <th>[unified.left_number]</th>
-          <th>[unified.right_number]</th>
-          <td[if-any unified.type] class="vc_idiff_[unified.type]"[end]>[for unified.segments][if-any unified.segments.type]<span class="vc_idiff_[unified.segments.type]">[unified.segments.text]</span>[else][unified.segments.text][end][end]</td>
-        </tr>
+      [for diff.unified]
+        [is diff.unified.type "no-changes"]
+	  <tr class="vc_diff_empty">
+	    <td colspan="3" style="text-align:center;"><br />
+	    <strong>- No changes -</strong><br />&nbsp; </td>
+	  </tr>
+        [else]
+	  [is diff.unified.type "binary-diff"]
+	    <tr class="vc_diff_empty">
+	      <td colspan="3" style="text-align:center;"><br />
+	      <strong>- Binary content differs -</strong><br />&nbsp; </td>
+	    </tr>
+	  [else]
+	    [if-any diff.unified.gap]
+	      <tr>
+		<th>&hellip;</th>
+		<th>&hellip;</th>
+		<th></th>
+	      </tr>
+	    [end]
+	    <tr>
+	      <th>[diff.unified.left_number]</th>
+	      <th>[diff.unified.right_number]</th>
+	      <td[if-any diff.unified.type] class="vc_idiff_[diff.unified.type]"[end]>[for diff.unified.segments][if-any diff.unified.segments.type]<span class="vc_idiff_[diff.unified.segments.type]">[diff.unified.segments.text]</span>[else][diff.unified.segments.text][end][end]</td>
+	    </tr>
+	  [end]
+	[end]
       [end]
     </tbody>
   </table>
 [end]
-
-<hr style="margin-top:1em;" />
-
-<table cellpadding="10" class="auto">
-  <tr>
-    <td>
-      <form method="get" action="[diff_format_action]">
-        <div>
-          [for diff_format_hidden_values]<input type="hidden" name="[diff_format_hidden_values.name]" value="[diff_format_hidden_values.value]"/>[end]
-          <select name="diff_format" onchange="submit()">
-            <option value="h" [is diff_format "h"]selected="selected"[end]>Colored Diff</option>
-            <option value="l" [is diff_format "l"]selected="selected"[end]>Long Colored Diff</option>
-            <option value="f" [is diff_format "f"]selected="selected"[end]>Full Colored Diff</option>
-            <option value="u" [is diff_format "u"]selected="selected"[end]>Unidiff</option>
-            <option value="c" [is diff_format "c"]selected="selected"[end]>Context Diff</option>
-            <option value="s" [is diff_format "s"]selected="selected"[end]>Side by Side</option>
-          </select>
-          <input type="submit" value="Show" />
-          </div>
-      </form>
-    </td>
-    <td>
-[if-any raw_diff]
-      &nbsp;
-[else]
-      <table style="border:solid gray 1px;" class="auto">
-        <tr>
-          <td>Legend:<br />
-            <table cellspacing="0" cellpadding="1">
-              <tr>
-                <td style="text-align:center;" class="vc_diff_remove">Removed from v.[left.rev]</td>
-                <td class="vc_diff_empty">&nbsp;</td>
-              </tr>
-              <tr>
-                <td style="text-align:center;" colspan="2" class="vc_diff_change">changed lines</td>
-              </tr>
-              <tr>
-                <td class="vc_diff_empty">&nbsp;</td>
-                <td style="text-align:center;" class="vc_diff_add">Added in v.[right.rev]</td>
-              </tr>
-            </table>
-          </td>
-        </tr>
-      </table>
-[end]
-    </td>
-  </tr>
-</table>
-
-[include "include/footer.ezt"]
Index: templates/pdiff.ezt
===================================================================
--- templates/pdiff.ezt	(revision 0)
+++ templates/pdiff.ezt	(revision 0)
@@ -0,0 +1,66 @@
+[# setup page definitions]
+  [define page_title]Property diff of /[where][end]
+  [define help_href][docroot]/help_rootview.html[end]
+[# end]
+
+[include "include/header.ezt" "diff"]
+[include "include/file_header.ezt"]
+
+[if-any diff]
+  [for diff]
+    <h3>[diff.name] <em>([if-any diff.is_added]added[else][if-any diff.is_removed]removed[else]changed[end][end])</em></h3>
+    [include "include/diff_display.ezt"]
+  [end]
+
+<hr style="margin-top:1em;" />
+
+<table cellpadding="10" class="auto">
+  <tr>
+    <td>
+      <form method="get" action="[diff_format_action]">
+        <div>
+          [for diff_format_hidden_values]<input type="hidden" name="[diff_format_hidden_values.name]" value="[diff_format_hidden_values.value]"/>[end]
+          <select name="diff_format" onchange="submit()">
+            <option value="h" [is diff_format "h"]selected="selected"[end]>Colored Diff</option>
+            <option value="l" [is diff_format "l"]selected="selected"[end]>Long Colored Diff</option>
+            <option value="f" [is diff_format "f"]selected="selected"[end]>Full Colored Diff</option>
+            <option value="u" [is diff_format "u"]selected="selected"[end]>Unidiff</option>
+            <option value="c" [is diff_format "c"]selected="selected"[end]>Context Diff</option>
+            <option value="s" [is diff_format "s"]selected="selected"[end]>Side by Side</option>
+          </select>
+          <input type="submit" value="Show" />
+          </div>
+      </form>
+    </td>
+    <td>
+[if-any hide_legend]
+      &nbsp;
+[else]
+      <table style="border:solid gray 1px;" class="auto">
+        <tr>
+          <td>Legend:<br />
+            <table cellspacing="0" cellpadding="1">
+              <tr>
+                <td style="text-align:center;" class="vc_diff_remove">Removed from v.[left.rev]</td>
+                <td class="vc_diff_empty">&nbsp;</td>
+              </tr>
+              <tr>
+                <td style="text-align:center;" colspan="2" class="vc_diff_change">changed lines</td>
+              </tr>
+              <tr>
+                <td class="vc_diff_empty">&nbsp;</td>
+                <td style="text-align:center;" class="vc_diff_add">Added in v.[right.rev]</td>
+              </tr>
+            </table>
+          </td>
+        </tr>
+      </table>
+[end]
+    </td>
+  </tr>
+</table>
+[else]
+<h3><em>( no property changes )</em></h3>
+[end]
+
+[include "include/footer.ezt"]
Index: templates/revision.ezt
===================================================================
--- templates/revision.ezt	(revision 2598)
+++ templates/revision.ezt	(working copy)
@@ -61,7 +61,7 @@
       </td>
       <td>[if-any changes.log_href]<a href="[changes.log_href]" title="View Log">[end][changes.action][if-any changes.log_href]</a>[end]
           [if-any changes.text_mods], [if-any changes.diff_href]<a href="[changes.diff_href]" title="View Diff">[end]text changed[if-any changes.diff_href]</a>[end][end]
-          [if-any changes.prop_mods], props changed[end]
+          [if-any changes.prop_mods], [if-any changes.pdiff_href]<a href="[changes.pdiff_href]" title="View Property Diff">[end]props changed[if-any changes.pdiff_href]</a>[end][end]
       </td>
     </tr>
    [end]
Index: templates/diff.ezt
===================================================================
--- templates/diff.ezt	(revision 2598)
+++ templates/diff.ezt	(working copy)
@@ -5,190 +5,8 @@
 
 [include "include/header.ezt" "diff"]
 [include "include/file_header.ezt"]
+[include "include/diff_display.ezt"]
 
-<h3 style="text-align:center;"></h3>
-
-[if-any raw_diff]
-  <pre class="vc_raw_diff">[raw_diff]</pre>
-[end]
-
-[define left_view_href][if-any left.prefer_markup][left.view_href][else][if-any left.download_href][left.download_href][end][end][end]
-[define right_view_href][if-any right.prefer_markup][right.view_href][else][if-any right.download_href][right.download_href][end][end][end]
-
-[if-any changes]
-<table cellspacing="0" cellpadding="0">
-  <tr class="vc_diff_header">
-    <th style="width:6%;"></th>
-    <th style="width:47%; vertical-align:top;">
-      [is left.path right.path][else][left.path][end]
-      revision [if-any left_view_href]<a href="[left_view_href]">[end][left.rev][if-any left_view_href]</a>[end][if-any left.author] by <em>[left.author]</em>[end],
-      [left.date]
-      [if-any left.tag]<br />Tag: [left.tag][end]
-    </th>
-    <th style="width:47%; vertical-align:top;">
-      [is left.path right.path][else][right.path][end]
-      revision [if-any right_view_href]<a href="[right_view_href]">[end][right.rev][if-any right_view_href]</a>[end][if-any right.author] by <em>[right.author]</em>[end],
-      [right.date]
-      [if-any right.tag]<br />Tag: [right.tag][end]
-    </th>
-  </tr>
-
-  [for changes]
-    [is changes.type "header"]
-      <tr class="vc_diff_chunk_header" id="h[changes.line_info_right]">
-        <td style="width:6%;"><strong>#</strong></td>
-        <td style="width:47%;">
-          <strong>Line [changes.line_info_left]</strong>&nbsp;
-          <span class="vc_diff_chunk_extra">[changes.line_info_extra]</span>
-        </td>
-        <td style="width:47%;">
-          <strong>Line [changes.line_info_right]</strong>&nbsp;
-          <span class="vc_diff_chunk_extra">[changes.line_info_extra]</span>
-        </td>
-      </tr>
-    [else]
-      [is changes.type "add"]
-        <tr>
-          <td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
-          <td class="vc_diff_empty">&nbsp;</td>
-          <td class="vc_diff_add">&nbsp;[changes.right]</td>
-        </tr>
-      [else]
-        [is changes.type "remove"]
-          <tr>
-            <td></td>
-            <td class="vc_diff_remove">&nbsp;[changes.left]</td>
-            <td class="vc_diff_empty">&nbsp;</td>
-          </tr>
-        [else]
-          [is changes.type "change"]
-            <tr>
-              [if-any changes.have_right]
-                <td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
-              [else]
-                <td></td>
-              [end]
-              [if-any changes.have_left]
-                <td class="vc_diff_change">&nbsp;[changes.left]</td>
-              [else]
-                <td class="vc_diff_change_empty">&nbsp;</td>
-              [end]
-              [if-any changes.have_right]
-                <td class="vc_diff_change">&nbsp;[changes.right]</td>
-              [else]
-                <td class="vc_diff_change_empty">&nbsp;</td>
-              [end]
-            </tr>
-          [else]
-            [is changes.type "no-changes"]
-              <tr>
-                <td colspan="3">&nbsp;</td>
-              </tr>
-              <tr class="vc_diff_empty">
-                <td colspan="3" style="text-align:center;"><br />
-                <strong>- No changes -</strong><br />&nbsp; </td>
-              </tr>
-            [else]
-              [is changes.type "binary-diff"]
-                <tr>
-                  <td colspan="3">&nbsp;</td>
-                </tr>
-                <tr class="vc_diff_empty">
-                  <td colspan="3" style="text-align:center;"><br />
-                  <strong>- Binary file revisions differ -</strong><br />&nbsp; </td>
-                </tr>
-              [else]
-                [is changes.type "error"]
-                  <tr>
-                    <td colspan="3">&nbsp;</td>
-                  </tr>
-                  <tr class="vc_diff_empty">
-                    <td colspan="3" style="text-align:center;"> <br />
-                    <strong>- ViewVC depends on rcsdiff and GNU diff to create 
-                    this page.  ViewVC cannot find GNU diff. Even if you 
-                    have GNU diff installed, the rcsdiff program must be 
-                    configured and compiled with the GNU diff location.
-                    -</strong> <br />&nbsp; </td>
-                  </tr>
-                [else]
-                  <tr>
-                    <td class="vc_diff_line_number" id="l[changes.line_number]">[if-any right.annotate_href]<a href="[right.annotate_href]#l[changes.line_number]">[changes.line_number]</a>[else][changes.line_number][end]</td>
-                    <td class="vc_diff_nochange">&nbsp;[changes.left]</td>
-                    <td class="vc_diff_nochange">&nbsp;[changes.right]</td>
-                  </tr>
-                [end]
-              [end]
-            [end]
-          [end]
-        [end]
-      [end]
-    [end]
-  [end]
-</table>
-[end]
-
-[if-any sidebyside]
-  <table class="vc_idiff">
-    <colgroup><col /><col class="content" /></colgroup>
-    <colgroup><col /><col class="content" /></colgroup>
-    <thead>
-      <tr>
-        <th colspan="2">    
-          [is left.path right.path][else][left.path][end]
-          Revision [left.rev]
-        </th>
-        <th colspan="2">
-          [is left.path right.path][else][right.path][end]
-          Revision [right.rev]
-        </th>
-      </tr>
-    </thead>
-    <tbody>
-      [for sidebyside]
-        [if-any sidebyside.gap]
-          <tr>
-            <th>&hellip;</th><th></th>
-            <th>&hellip;</th><th></th>
-          </tr>
-        [end]
-        <tr>
-          [for sidebyside.columns]
-            <th>[sidebyside.columns.line_number]</th><td[if-any sidebyside.columns.line_number][else] class="vc_idiff_empty"[end]>[for sidebyside.columns.segments][if-any sidebyside.columns.segments.type]<span class="vc_idiff_[sidebyside.columns.segments.type]">[sidebyside.columns.segments.text]</span>[else][sidebyside.columns.segments.text][end][end]</td>
-          [end]
-        </tr>
-      [end]
-    </tbody>
-  </table>
-[end]
-
-[if-any unified]
-  <table class="vc_idiff">
-    <thead>
-      <tr>
-        <th>r[left.rev]</th>
-        <th>r[right.rev]</th>
-        <th></th>
-      </tr>
-    </thead>
-    <tbody>
-      [for unified]
-        [if-any unified.gap]
-          <tr>
-            <th>&hellip;</th>
-            <th>&hellip;</th>
-            <th></th>
-          </tr>
-        [end]
-        <tr>
-          <th>[unified.left_number]</th>
-          <th>[unified.right_number]</th>
-          <td[if-any unified.type] class="vc_idiff_[unified.type]"[end]>[for unified.segments][if-any unified.segments.type]<span class="vc_idiff_[unified.segments.type]">[unified.segments.text]</span>[else][unified.segments.text][end][end]</td>
-        </tr>
-      [end]
-    </tbody>
-  </table>
-[end]
-
 <hr style="margin-top:1em;" />
 
 <table cellpadding="10" class="auto">
@@ -210,7 +28,7 @@
       </form>
     </td>
     <td>
-[if-any raw_diff]
+[if-any diff.raw_diff]
       &nbsp;
 [else]
       <table style="border:solid gray 1px;" class="auto">
Index: lib/viewvc.py
===================================================================
--- lib/viewvc.py	(revision 2598)
+++ lib/viewvc.py	(working copy)
@@ -82,6 +82,7 @@
 CHUNK_SIZE = 8192
 
 # for rcsdiff processing of header
+_RCSDIFF_NO_CHANGES = "no-changes"
 _RCSDIFF_IS_BINARY = 'binary-diff'
 _RCSDIFF_ERROR = 'error'
 
@@ -533,12 +534,6 @@
     if view_func is view_annotate and params.get('annotate') is not None:
       view_func = None
 
-    # no need to explicitly specify diff view when
-    # there's r1 and r2 parameters
-    if (view_func is view_diff and params.get('r1') is not None
-        and params.get('r2') is not None):
-      view_func = None
-
     # no need to explicitly specify checkout view when it's the default
     # view or when checkout_magic is enabled
     if view_func is view_checkout:
@@ -1387,6 +1382,9 @@
     'download_href' : None,
     'download_text_href' : None,
     'graph_href': None,
+    'patch_href': None,
+    'diff_href': None,
+    'pdiff_href': None,
     'kv'  : request.kv,
     'lockinfo' : None,
     'log_href' : None,
@@ -1689,25 +1687,27 @@
 def make_comma_sep_list_string(items):
   return ', '.join(map(lambda x: x.name, items))
 
+def is_undisplayable(val):
+  try:
+    unicode(val)
+    return ezt.boolean(0)
+  except:
+    return ezt.boolean(1)
+
 def get_itemprops(request, path_parts, rev):
   itemprops = request.repos.itemprops(path_parts, rev)
   propnames = itemprops.keys()
   propnames.sort()
   props = []
   for name in propnames:
-    value = format_log(request, itemprops[name])
-    undisplayable = ezt.boolean(0)
     # skip non-utf8 property names
-    try:
-      unicode(name, 'utf8')
-    except:
+    if is_undisplayable(name):
       continue
+    value = format_log(request, itemprops[name])
     # note non-utf8 property values
-    try:
-      unicode(value, 'utf8')
-    except:
+    undisplayable = is_undisplayable(value)
+    if undisplayable:
       value = None
-      undisplayable = ezt.boolean(1)
     props.append(_item(name=name, value=value, undisplayable=undisplayable))
   return props
 
@@ -2442,6 +2442,8 @@
     entry.diff_to_prev_href = None
     entry.diff_to_branch_href = None
     entry.diff_to_main_href = None
+    entry.pdiff_to_sel_href = None
+    entry.pdiff_to_prev_href = None
         
     if request.roottype == 'cvs':
       prev = rev.prev or rev.parent
@@ -2530,6 +2532,12 @@
                                 'r2': entry.rev,
                                 'diff_format': None},
                         escape=1)
+      entry.pdiff_to_prev_href = \
+        request.get_url(view_func=view_pdiff,
+                        params={'r1': entry.prev,
+                                'r2': entry.rev,
+                                'diff_format': None},
+                        escape=1)
     if selected_rev and \
            selected_rev != str(entry.rev) and \
            selected_rev != str(entry.prev) and \
@@ -2541,6 +2549,12 @@
                                 'r2': entry.rev,
                                 'diff_format': None},
                         escape=1)
+      entry.pdiff_to_sel_href = \
+        request.get_url(view_func=view_pdiff,
+                        params={'r1': selected_rev,
+                                'r2': entry.rev,
+                                'diff_format': None},
+                        escape=1)
 
     if entry.next_main:
       entry.diff_to_main_href = \
@@ -2975,7 +2989,7 @@
     if not line:
       if self.state == 'no-changes':
         self.state = 'done'
-        return _item(type='no-changes')
+        return _item(type=_RCSDIFF_NO_CHANGES)
 
       # see if there are lines to flush
       if self.left_col or self.right_col:
@@ -3242,35 +3256,11 @@
   copy_stream(fp, server_fp)
   fp.close()
 
-
-def view_diff(request):
-  if 'diff' not in request.cfg.options.allowed_views:
-    raise debug.ViewVCException('Diff generation is disabled',
-                                 '403 Forbidden')
-
-  cfg = request.cfg
-  query_dict = request.query_dict
-  p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request)
-  
-  # since templates are in use and subversion allows changes to the dates,
-  # we can't provide a strong etag
-  if check_freshness(request, None, '%s-%s' % (rev1, rev2), weak=1):
-    return
-
-  # TODO: Is the slice necessary, or is limit enough?
-  log_entry1 = request.repos.itemlog(p1, rev1, vclib.SORTBY_REV, 0, 1, {})[-1]
-  log_entry2 = request.repos.itemlog(p2, rev2, vclib.SORTBY_REV, 0, 1, {})[-1]
-
-  ago1 = log_entry1.date is not None \
-         and html_time(request, log_entry1.date, 1) or None
-  ago2 = log_entry2.date is not None \
-         and html_time(request, log_entry2.date, 2) or None
-  
+def get_diff_type(format):
   diff_type = None
   diff_options = {}
   human_readable = 0
 
-  format = query_dict.get('diff_format', cfg.options.diff_format)
   if format == 'c':
     diff_type = vclib.CONTEXT
   elif format == 's':
@@ -3291,45 +3281,97 @@
   else:
     raise debug.ViewVCException('Diff format %s not understood'
                                  % format, '400 Bad Request')
+  return (diff_type, diff_options, human_readable)
 
+
+def diff_side_item(request, path_comp, rev, sym, content_links=1):
+  # TODO: Is the slice necessary, or is limit enough?
+  log_entry = request.repos.itemlog(path_comp, rev, vclib.SORTBY_REV, 0, 1, {})[-1]
+  ago = log_entry.date is not None \
+         and html_time(request, log_entry.date, 1) or None
+  path_joined = _path_join(path_comp)
+  i = _item(date=make_time_string(log_entry.date, request.cfg),
+		 author=log_entry.author,
+		 log=format_log(request, log_entry.log),
+		 size=log_entry.size,
+		 ago=ago,
+		 path=path_joined,
+		 rev=rev,
+		 tag=sym)
+  if content_links:
+    fvi = get_file_view_info(request, path_joined, rev)
+    i.view_href = fvi.view_href
+    i.download_href = fvi.download_href
+    i.download_text_href = fvi.download_text_href
+    i.annotate_href = fvi.annotate_href
+    i.revision_href = fvi.revision_href
+    i.prefer_markup = fvi.prefer_markup
+  else:
+    i.view_href = None
+    i.download_href = None
+    i.download_text_href = None
+    i.annotate_href = None
+    i.revision_href = None
+    i.prefer_markup = ezt.boolean(0)
+  return log_entry, i
+
+# Return whether idiff routines should be used. That module
+# only supports unified and human-readable diff formats. Moreover,
+# unless intraline diffs are not requested - fall back to
+# using GNU diff.
+def use_idiff(cfg, diff_type, human_readable):
+  return (cfg.options.hr_intraline and idiff
+      and ((human_readable and idiff.sidebyside)
+	  or (not human_readable and diff_type == vclib.UNIFIED)))
+
+def view_diff(request):
+  if 'diff' not in request.cfg.options.allowed_views:
+    raise debug.ViewVCException('Diff generation is disabled',
+                                 '403 Forbidden')
+
+  cfg = request.cfg
+  query_dict = request.query_dict
+  p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request)
+  path_left = _path_join(p1)
+  path_right = _path_join(p2)
+  
+  # since templates are in use and subversion allows changes to the dates,
+  # we can't provide a strong etag
+  if check_freshness(request, None, '%s-%s' % (rev1, rev2), weak=1):
+    return
+
+  diff_format = query_dict.get('diff_format', cfg.options.diff_format)
+  diff_type, diff_options, human_readable = get_diff_type(diff_format)
+
   if human_readable:
     diff_options['funout'] = cfg.options.hr_funout
     diff_options['ignore_white'] = cfg.options.hr_ignore_white
     diff_options['ignore_keyword_subst'] = cfg.options.hr_ignore_keyword_subst
-  try:
-    fp = sidebyside = unified = None
-    if (cfg.options.hr_intraline and idiff
-        and ((human_readable and idiff.sidebyside)
-             or (not human_readable and diff_type == vclib.UNIFIED))):
-      f1 = request.repos.openfile(p1, rev1, {})[0]
-      try:
-        lines_left = f1.readlines()
-      finally:
-        f1.close()
 
-      f2 = request.repos.openfile(p2, rev2, {})[0]
-      try:
-        lines_right = f2.readlines()
-      finally:
-        f2.close()
+  changes = fp = sidebyside = unified = None
+  date1 = date2 = raw_diff_fp = None
 
-      if human_readable:
-        sidebyside = idiff.sidebyside(lines_left, lines_right,
-                                      diff_options.get("context", 5))
-      else:
-        unified = idiff.unified(lines_left, lines_right,
-                                diff_options.get("context", 2))
-    else: 
-      fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type, diff_options)
-  except vclib.InvalidRevision:
-    raise debug.ViewVCException('Invalid path(s) or revision(s) passed '
-                                 'to diff', '400 Bad Request')
-  path_left = _path_join(p1)
-  path_right = _path_join(p2)
+  if use_idiff(cfg, diff_type, human_readable):
+    f1 = request.repos.openfile(p1, rev1, {})[0]
+    try:
+      lines_left = f1.readlines()
+    finally:
+      f1.close()
 
-  date1 = date2 = raw_diff_fp = None
-  changes = []
-  if fp:
+    f2 = request.repos.openfile(p2, rev2, {})[0]
+    try:
+      lines_right = f2.readlines()
+    finally:
+      f2.close()
+
+    if human_readable:
+      sidebyside = idiff.sidebyside(lines_left, lines_right,
+				    diff_options.get("context", 5))
+    else:
+      unified = idiff.unified(lines_left, lines_right,
+			      diff_options.get("context", 2))
+  else: 
+    fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type, diff_options)
     date1, date2, flag, headers = diff_parse_headers(fp, diff_type,
                                                      path_left, path_right,
                                                      rev1, rev2, sym1, sym2)
@@ -3346,57 +3388,173 @@
   diff_format_action, diff_format_hidden_values = \
     request.get_form(params=no_format_params)
 
-  fvi = get_file_view_info(request, path_left, rev1)
-  left = _item(date=make_time_string(log_entry1.date, cfg),
-               author=log_entry1.author,
-               log=format_log(request, log_entry1.log),
-               size=log_entry1.size,
-               ago=ago1,
-               path=path_left,
-               rev=rev1,
-               tag=sym1,
-               view_href=fvi.view_href,
-               download_href=fvi.download_href,
-               download_text_href=fvi.download_text_href,
-               annotate_href=fvi.annotate_href,
-               revision_href=fvi.revision_href,
-               prefer_markup=fvi.prefer_markup)
-    
-  fvi = get_file_view_info(request, path_right, rev2)
-  right = _item(date=make_time_string(log_entry2.date, cfg),
-                author=log_entry2.author,
-                log=format_log(request, log_entry2.log),
-                size=log_entry2.size,
-                ago=ago2,
-                path=path_right,
-                rev=rev2,
-                tag=sym2,
-                view_href=fvi.view_href,
-                download_href=fvi.download_href,
-                download_text_href=fvi.download_text_href,
-                annotate_href=fvi.annotate_href,
-                revision_href=fvi.revision_href,
-                prefer_markup=fvi.prefer_markup)
+  log_left, left = diff_side_item(request, p1, rev1, sym1)
+  log_right, right = diff_side_item(request, p2, rev2, sym2)
 
   data = common_template_data(request)
   data.merge(ezt.TemplateData({
     'left' : left,
     'right' : right,
-    'raw_diff' : raw_diff_fp,
-    'changes' : changes,
-    'sidebyside': sidebyside,
-    'unified': unified,
-    'diff_format' : request.query_dict.get('diff_format',
-                                           cfg.options.diff_format),
+    'diff' : _item(raw_diff=raw_diff_fp,
+		changes=changes,
+		sidebyside=sidebyside,
+		unified=unified,
+		undisplayable=ezt.boolean(0)),
     'patch_href' : request.get_url(view_func=view_patch,
                                    params=no_format_params,
                                    escape=1),
+    'pdiff_href' : request.get_url(view_func=view_pdiff,
+                                   params=no_format_params,
+                                   escape=1),
+    'diff_format' : diff_format,
     'diff_format_action' : diff_format_action,
     'diff_format_hidden_values' : diff_format_hidden_values,
     }))
   generate_page(request, "diff", data)
 
+def view_pdiff(request):
+  if 'pdiff' not in request.cfg.options.allowed_views:
+    raise debug.ViewVCException('Property diff generation is disabled',
+                                 '403 Forbidden')
+  cfg = request.cfg
+  pathtype = request.pathtype
+  query_dict = request.query_dict
+  p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request)
 
+  # since templates are in use and subversion allows changes to the dates,
+  # we can't provide a strong etag
+  if check_freshness(request, None, '%s-%s' % (rev1, rev2), weak=1):
+    return
+
+  diff_format = query_dict.get('diff_format', cfg.options.diff_format)
+  diff_type, diff_options, human_readable = get_diff_type(diff_format)
+
+  if human_readable:
+    diff_options['ignore_white'] = cfg.options.hr_ignore_white
+
+  path_left = _path_join(p1)
+  path_right = _path_join(p2)
+
+  log_left, left = diff_side_item(request, p1, rev1, sym1, 0)
+  log_right, right = diff_side_item(request, p2, rev2, sym2, 0)
+
+  props_left = request.repos.itemprops(p1, rev1)
+  props_right = request.repos.itemprops(p2, rev2)
+  diff = []
+  hide_legend = ezt.boolean(0)
+
+  def uniq(lst):
+    h = {}
+    for e in lst:
+      h[e] = 1
+    return sorted(h.keys())
+
+  for p in uniq(props_left.keys() + props_right.keys()):
+    # Skip non-utf8 property names
+    if is_undisplayable(p):
+      continue
+    # Get the values and see if there's a change
+    val_left = props_left.get(p)
+    val_right = props_right.get(p)
+    is_added = ezt.boolean(0)
+    is_removed = ezt.boolean(0)
+    if val_left is None:
+      is_added = ezt.boolean(1)
+    elif val_right is None:
+      is_removed = ezt.boolean(1)
+    elif val_left == val_right:
+      continue
+    # Determine if old/new value is displayable and if it is, split to lines
+    undisplayable = 0
+    raw_diff_fp = changes = sidebyside = unified = None
+    if use_idiff(cfg, diff_format, human_readable):
+      lines_left = []
+      lines_right = []
+      if val_left is not None:
+	if is_undisplayable(val_left):
+	  undisplayable = 1
+	else:
+	  lines_left = val_left.splitlines()
+      if val_right is not None:
+	if is_undisplayable(val_right):
+	  undisplayable = 1
+	else:
+	  lines_right = val_right.splitlines()
+      if human_readable:
+	if undisplayable:
+	  sidebyside = [ _item(type=_RCSDIFF_IS_BINARY) ]
+	else:
+	  sidebyside = idiff.sidebyside(lines_left, lines_right,
+	      diff_options.get("context", 5))
+      else:
+	if undisplayable:
+	  unified = [ _item(type=_RCSDIFF_IS_BINARY) ]
+	else:
+	  unified = idiff.unified(lines_left, lines_right,
+	      diff_options.get("context", 2))
+    else:
+      def prop_temp(val):
+	fname = tempfile.mktemp()
+	fp = open(fname, "wb")
+	if val:
+	  fp.write(val)
+	fp.close()
+	return fname
+
+      def prop_path(f,n):
+	return "%s:property(%s)" % (f, n)
+ 
+      temp1 = prop_temp(val_left)
+      temp2 = prop_temp(val_right)
+      ppath1 = prop_path(path_left, p)
+      ppath2 = prop_path(path_right, p)
+      info1 = (ppath1, log_left.date, rev1)
+      info2 = (ppath2, log_right.date, rev2)
+      diff_args = vclib._diff_args(diff_type, diff_options)
+      fp = vclib._diff_fp(temp1, temp2, info1, info2,
+	  cfg.utilities.diff or 'diff', diff_opts=diff_args)
+      date1, date2, flag, headers = diff_parse_headers(fp, diff_type,
+		ppath1, ppath2, rev1, rev2, sym1, sym2)
+      if human_readable:
+	if flag is not None:
+	  changes = [ _item(type=flag) ]
+	else:
+	  changes = DiffSource(fp, cfg)
+      else:
+	hide_legend = ezt.boolean(1)
+	raw_diff_fp = MarkupPipeWrapper(fp, request.server.escape(headers), None, 1)
+
+    diff.append(_item(name=p,
+        is_added=is_added,
+        is_removed=is_removed,
+	sidebyside=sidebyside,
+	unified=unified,
+	raw_diff=raw_diff_fp,
+	changes=changes))
+
+  no_format_params = request.query_dict.copy()
+  no_format_params['diff_format'] = None
+  diff_format_action, diff_format_hidden_values = \
+    request.get_form(params=no_format_params)
+
+  data = common_template_data(request)
+  data.merge(ezt.TemplateData({
+    'left' : left,
+    'right' : right,
+    'diff' : diff,
+    'patch_href' : request.get_url(view_func=view_patch,
+                                   params=no_format_params,
+                                   escape=1),
+    'diff_href' : request.get_url(view_func=view_diff,
+                                   params=no_format_params,
+                                   escape=1),
+    'diff_format' : diff_format,
+    'diff_format_action' : diff_format_action,
+    'diff_format_hidden_values' : diff_format_hidden_values,
+    'hide_legend' : hide_legend
+  }))
+  generate_page(request, "pdiff", data)
+
 def generate_tarball_header(out, name, size=0, mode=None, mtime=0,
                             uid=0, gid=0, typefrag=None, linkname='',
                             uname='viewvc', gname='viewvc',
@@ -3613,19 +3771,14 @@
   propnames.sort()
   props = []
   for name in propnames:
-    value = format_log(request, revprops[name])
-    undisplayable = ezt.boolean(0)
     # skip non-utf8 property names
-    try:
-      unicode(name, 'utf8')
-    except:
+    if is_undisplayable(name):
       continue
+    value = format_log(request, revprops[name])
     # note non-utf8 property values
-    try:
-      unicode(value, 'utf8')
-    except:
+    undisplayable = is_undisplayable(value)
+    if undisplayable:
       value = None
-      undisplayable = ezt.boolean(1)
     props.append(_item(name=name, value=value, undisplayable=undisplayable))
   
   # Sort the changes list by path.
@@ -3702,6 +3855,15 @@
                                                    'r2' : str(change.base_rev),
                                                    },
                                            escape=1)
+      if change.props_changed:
+        change.pdiff_href = request.get_url(view_func=view_pdiff,
+                                           where=path, 
+                                           pathtype=change.pathtype,
+                                           params={'pathrev' : str(rev),
+                                                   'r1' : str(rev),
+                                                   'r2' : str(change.base_rev),
+                                                   },
+                                           escape=1)
     
 
     # use same variable names as the log template
@@ -4365,6 +4527,7 @@
   'log':       view_log,
   'markup':    view_markup,
   'patch':     view_patch,
+  'pdiff':     view_pdiff,
   'query':     view_query,
   'queryform': view_queryform,
   'revision':  view_revision,
Index: lib/config.py
===================================================================
--- lib/config.py	(revision 2598)
+++ lib/config.py	(working copy)
@@ -393,7 +393,7 @@
 
     self.options.root_as_url_component = 1
     self.options.checkout_magic = 0
-    self.options.allowed_views = ['annotate', 'diff', 'markup', 'roots']
+    self.options.allowed_views = ['annotate', 'diff', 'pdiff', 'markup', 'roots']
     self.options.authorizer = None
     self.options.mangle_email_addresses = 0
     self.options.default_file_view = "log"
@@ -408,6 +408,7 @@
     self.options.hide_errorful_entries = 0
     self.options.log_sort = 'date'
     self.options.diff_format = 'h'
+    self.options.prop_diff_format = 'f'
     self.options.hide_cvsroot = 1
     self.options.hr_breakable = 1
     self.options.hr_funout = 1
Index: lib/idiff.py
===================================================================
--- lib/idiff.py	(revision 2598)
+++ lib/idiff.py	(working copy)
@@ -22,6 +22,8 @@
 import ezt
 import sapi
 
+_RCSDIFF_NO_CHANGES = "no-changes"
+
 def sidebyside(fromlines, tolines, context):
   """Generate side by side diff"""
 
@@ -29,6 +31,7 @@
   line_strip = lambda line: line.rstrip("\n")
   fromlines = map(line_strip, fromlines)
   tolines = map(line_strip, tolines)
+  had_changes = 0
 
   gap = False
   for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context):
@@ -37,8 +40,11 @@
     else:
       from_item = _mdiff_split(flag, fromdata)
       to_item = _mdiff_split(flag, todata)
-      yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item))
+      had_changes = 1
+      yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item), type="intraline")
       gap = False
+  if not had_changes:
+    yield _item(type=_RCSDIFF_NO_CHANGES)
 
 _re_mdiff = re.compile("\0([+-^])(.*?)\1")
 
@@ -71,19 +77,26 @@
 
   diff = difflib.Differ().compare(fromlines, tolines)
   lastrow = None
+  had_changes = 0
 
   for row in _trim_context(diff, context):
     if row[0].startswith("? "):
+      had_changes = 1
       yield _differ_split(lastrow, row[0])
       lastrow = None
     else:
       if lastrow:
+	had_changes = 1
         yield _differ_split(lastrow, None)
       lastrow = row
 
   if lastrow:
+    had_changes = 1
     yield _differ_split(lastrow, None)
 
+  if not had_changes:
+    yield _item(type=_RCSDIFF_NO_CHANGES)
+
 def _trim_context(lines, context_size):
   """Trim context lines that don't surround changes from Differ results
 
Index: viewvc-install
===================================================================
--- viewvc-install	(revision 2598)
+++ viewvc-install	(working copy)
@@ -207,6 +207,8 @@
                         temp = raw_input("Do you want to [O]verwrite, [D]o "
                                          "not overwrite, or [V]iew "
                                          "differences? ")
+		    if len(temp) == 0:
+			continue
                     temp = temp[0].lower()
                     if temp == "v" and ext not in BINARY_FILE_EXTS:
                         print """