Edgewall Software

Ticket #2028: trac-diff-for-0.9.3.2.2.patch

File trac-diff-for-0.9.3.2.2.patch, 85.5 KB (added by jouvin@…, 6 years ago)
  • htdocs/css/browser.css

    diff -Nru htdocs/css/browser.css htdocs/css/browser.css
     
    4646#dirlist td.rev { text-align: right } 
    4747#dirlist td.change { font-size: 85%; vertical-align: middle; white-space: nowrap } 
    4848 
     49/* Log */ 
     50tr.diff input {  
     51 padding: 0 1em 0 1em; 
     52 margin: 0;  
     53} 
     54 
     55div.buttons { 
     56 clear: left; 
     57} 
     58 
     59#anydiff { 
     60 margin: 0 0 1em; 
     61 float: left; 
     62} 
     63#anydiff form, #anydiff div, #anydiff h2 { 
     64 display: inline; 
     65} 
     66#anydiff input {  
     67 vertical-align: baseline; 
     68 margin: 0 -0.5em 0 1em; 
     69} 
     70 
     71 
    4972/* Styles for the revision log table 
    5073   (extends the styles for "table.listing") */ 
    5174#chglist { margin-top: 0 } 
  • htdocs/css/changeset.css

    diff -Nru htdocs/css/changeset.css htdocs/css/changeset.css
     
    2626 
    2727.diff ul.props { font-size: 90%; list-style: disc; margin: .5em 0 0; padding: 0 .5em 1em 2em } 
    2828.diff ul.props li { margin: 0; padding: 0 } 
     29 
     30 
     31#title dl { 
     32 display: inline; 
     33 font-size: 110% 
     34} 
     35#title dt {  
     36  font-size: 110%; 
     37  font-weight: bold; 
     38  display: inline;  
     39  margin-left: 3em; 
     40} 
     41#title dd {  
     42  display: inline; 
     43  margin-left: 0.4em; 
     44} 
  • templates/anydiff.cs

    diff -Nru templates/anydiff.cs templates/anydiff.cs
     
     1<?cs include "header.cs"?> 
     2 
     3<div id="ctxtnav" class="nav"> 
     4 <h2>Navigation</h2><?cs 
     5 with:links = chrome.links ?> 
     6  <ul> 
     7  </ul><?cs 
     8 /with ?> 
     9</div> 
     10 
     11<div id="content" class="changeset"> 
     12 <div id="title"> 
     13    <h1>Select Base and Target for Diff:</h1> 
     14 </div> 
     15 
     16 <div id="anydiff"> 
     17  <form action="<?cs var:anydiff.diff_href ?>" method="post"> 
     18   <table> 
     19    <tr> 
     20     <th><label for="old_path">From:</label></th> 
     21     <td> 
     22      <input type="text" id="old_path" name="old_path" value="<?cs 
     23         var:anydiff.old_path ?>" size="44" /> 
     24      <label for="old_rev">at Revision:</label> 
     25      <input type="text" id="old_rev" name="old" value="<?cs 
     26         var:anydiff.old_rev ?>" size="4" /> 
     27     </td> 
     28    </tr> 
     29    <tr> 
     30     <th><label for="new_path">To:</label></th> 
     31     <td> 
     32      <input type="text" id="new_path" name="path" value="<?cs 
     33         var:anydiff.new_path ?>" size="44" /> 
     34      <label for="new_rev">at Revision:</label> 
     35      <input type="text" id="new_rev" name="new" value="<?cs 
     36         var:anydiff.new_rev ?>" size="4" /> 
     37     </td> 
     38    </tr> 
     39   </table> 
     40   <div class="buttons"> 
     41      <input type="submit" value="View changes" /> 
     42   </div> 
     43  </form> 
     44 </div> 
     45</div> 
     46 
     47<?cs include "footer.cs"?> 
  • templates/browser.cs

    diff -Nru templates/browser.cs templates/browser.cs
     
    33 
    44<div id="ctxtnav" class="nav"> 
    55 <ul> 
    6   <li class="last"><a href="<?cs var:browser.log_href ?>">Revision Log</a></li> 
     6  <li class="first"><a href="<?cs var:browser.restr_changeset_href ?>"> 
     7   Last Change</a></li> 
     8  <li class="last"><a href="<?cs var:browser.log_href ?>"> 
     9   Revision Log</a></li> 
    710 </ul> 
    811</div> 
    912 
     13 
    1014<div id="content" class="browser"> 
    1115 <h1><?cs call:browser_path_links(browser.path, browser) ?></h1> 
    1216 
    1317 <div id="jumprev"> 
    14   <form action="" method="get"><div> 
    15    <label for="rev">View revision:</label> 
    16    <input type="text" id="rev" name="rev" value="<?cs 
    17      var:browser.revision ?>" size="4" /> 
    18   </div></form> 
     18  <form action="" method="get"> 
     19   <div> 
     20    <label for="rev">View revision:</label> 
     21    <input type="text" id="rev" name="rev" value="<?cs 
     22       var:browser.revision ?>" size="4" /> 
     23   </div> 
     24  </form> 
    1925 </div> 
    2026 
    2127 <?cs if:browser.is_dir ?> 
     
    114120  ?>/TracBrowser">TracBrowser</a> for help on using the browser. 
    115121 </div> 
    116122 
     123  <div id="anydiff"><?cs 
     124   if len(browser.path) > #1 ?> 
     125    <form action="<?cs var:browser.anydiff_href ?>" method="get"> 
     126     <input type="hidden" name="new_path" value="<?cs var:browser.path ?>" /> 
     127     <input type="hidden" name="old_path" value="<?cs var:browser.path ?>" /> 
     128     <input type="hidden" name="new_rev" value="<?cs var:browser.revision ?>" /> 
     129     <input type="hidden" name="old_rev" value="<?cs var:browser.revision ?>" /> 
     130     <div class="buttons"> 
     131      <input type="submit" value="View changes..." title="Prepare an Arbitrary Diff" /> 
     132     </div> 
     133    </form><?cs 
     134   /if ?> 
     135  </div> 
     136 
    117137</div> 
    118138<?cs include:"footer.cs"?> 
  • templates/changeset.cs

    diff -Nru templates/changeset.cs templates/changeset.cs
     
    1 <?cs include "header.cs"?> 
    2 <?cs include "macros.cs"?> 
    3  
    4 <div id="ctxtnav" class="nav"> 
    5  <h2>Changeset Navigation</h2><?cs 
    6  with:links = chrome.links ?> 
    7   <ul><?cs 
    8    if:len(links.prev) ?> 
    9     <li class="first<?cs if:!len(links.next) ?> last<?cs /if ?>"> 
    10      &larr; <a class="prev" href="<?cs var:links.prev.0.href ?>" title="<?cs 
    11        var:links.prev.0.title ?>">Previous Changeset</a> 
    12     </li><?cs 
    13    /if ?><?cs 
    14    if:len(links.next) ?> 
    15     <li class="<?cs if:len(links.prev) ?>first <?cs /if ?>last"> 
    16      <a class="next" href="<?cs var:links.next.0.href ?>" title="<?cs 
    17        var:links.next.0.title ?>">Next Changeset</a> &rarr; 
    18     </li><?cs 
    19    /if ?> 
    20   </ul><?cs 
    21  /with ?> 
    22 </div> 
    23  
    24 <div id="content" class="changeset"> 
    25 <h1>Changeset <?cs var:changeset.revision ?></h1> 
    26  
    27 <?cs each:change = changeset.changes ?><?cs 
    28  if:len(change.diff) ?><?cs 
    29   set:has_diffs = 1 ?><?cs 
    30  /if ?><?cs 
    31 /each ?><?cs if:has_diffs || diff.options.ignoreblanklines  
    32   || diff.options.ignorecase || diff.options.ignorewhitespace ?> 
    33 <form method="post" id="prefs" action=""> 
    34  <div> 
    35   <label for="style">View differences</label> 
    36   <select id="style" name="style"> 
    37    <option value="inline"<?cs 
    38      if:diff.style == 'inline' ?> selected="selected"<?cs 
    39      /if ?>>inline</option> 
    40    <option value="sidebyside"<?cs 
    41      if:diff.style == 'sidebyside' ?> selected="selected"<?cs 
    42      /if ?>>side by side</option> 
    43   </select> 
    44   <div class="field"> 
    45    Show <input type="text" name="contextlines" id="contextlines" size="2" 
    46      maxlength="3" value="<?cs var:diff.options.contextlines ?>" /> 
    47    <label for="contextlines">lines around each change</label> 
    48   </div> 
    49   <fieldset id="ignore"> 
    50    <legend>Ignore:</legend> 
    51    <div class="field"> 
    52     <input type="checkbox" id="blanklines" name="ignoreblanklines"<?cs 
    53       if:diff.options.ignoreblanklines ?> checked="checked"<?cs /if ?> /> 
    54     <label for="blanklines">Blank lines</label> 
    55    </div> 
    56    <div class="field"> 
    57     <input type="checkbox" id="case" name="ignorecase"<?cs 
    58       if:diff.options.ignorecase ?> checked="checked"<?cs /if ?> /> 
    59     <label for="case">Case changes</label> 
    60    </div> 
    61    <div class="field"> 
    62     <input type="checkbox" id="whitespace" name="ignorewhitespace"<?cs 
    63       if:diff.options.ignorewhitespace ?> checked="checked"<?cs /if ?> /> 
    64     <label for="whitespace">White space changes</label> 
    65    </div> 
    66   </fieldset> 
    67   <div class="buttons"> 
    68    <input type="submit" name="update" value="Update" /> 
    69   </div> 
    70  </div> 
    71 </form><?cs /if ?> 
    72  
    73 <?cs def:node_change(item,cl,kind) ?><?cs  
    74   set:ndiffs = len(item.diff) ?><?cs 
    75   set:nprops = len(item.props) ?> 
    76   <div class="<?cs var:cl ?>"></div><?cs  
    77   if:cl == "rem" ?> 
    78    <a title="Show what was removed (rev. <?cs var:item.rev.old ?>)" href="<?cs 
    79      var:item.browser_href.old ?>"><?cs var:item.path.old ?></a><?cs 
    80   else ?> 
    81    <a title="Show entry in browser" href="<?cs 
    82      var:item.browser_href.new ?>"><?cs var:item.path.new ?></a><?cs 
    83   /if ?> 
    84   <span class="comment">(<?cs var:kind ?>)</span><?cs 
    85   if:item.path.old && item.change == 'copy' || item.change == 'move' ?> 
    86    <small><em>(<?cs var:kind ?> from <a href="<?cs 
    87     var:item.browser_href.old ?>" title="Show original file (rev. <?cs 
    88     var:item.rev.old ?>)"><?cs var:item.path.old ?></a>)</em></small><?cs 
    89   /if ?><?cs 
    90   if:$ndiffs + $nprops > #0 ?> 
    91     (<a href="#file<?cs var:name(item) ?>" title="Show differences"><?cs 
    92       if:$ndiffs > #0 ?><?cs var:ndiffs ?>&nbsp;diff<?cs if:$ndiffs > #1 ?>s<?cs /if ?><?cs  
    93       /if ?><?cs 
    94       if:$ndiffs && $nprops ?>, <?cs /if ?><?cs  
    95       if:$nprops > #0 ?><?cs var:nprops ?>&nbsp;prop<?cs if:$nprops > #1 ?>s<?cs /if ?><?cs 
    96       /if ?></a>)<?cs 
    97   elif:cl == "mod" ?> 
    98     (<a href="<?cs var:item.browser_href.old ?>" 
    99         title="Show previous version in browser">previous</a>)<?cs 
    100   /if ?> 
    101 <?cs /def ?> 
    102  
    103 <dl id="overview"> 
    104  <dt class="time">Timestamp:</dt> 
    105  <dd class="time"><?cs var:changeset.time ?></dd> 
    106  <dt class="author">Author:</dt> 
    107  <dd class="author"><?cs var:changeset.author ?></dd> 
    108  <dt class="message">Message:</dt> 
    109  <dd class="message" id="searchable"><?cs 
    110   alt:changeset.message ?>&nbsp;<?cs /alt ?></dd> 
    111  <dt class="files">Files:</dt> 
    112  <dd class="files"> 
    113   <ul><?cs each:item = changeset.changes ?> 
    114    <li><?cs 
    115     if:item.change == 'add' ?><?cs 
    116      call:node_change(item, 'add', 'added') ?><?cs 
    117     elif:item.change == 'delete' ?><?cs 
    118      call:node_change(item, 'rem', 'deleted') ?><?cs 
    119     elif:item.change == 'copy' ?><?cs 
    120      call:node_change(item, 'cp', 'copied') ?><?cs 
    121     elif:item.change == 'move' ?><?cs 
    122      call:node_change(item, 'mv', 'moved') ?><?cs 
    123     elif:item.change == 'edit' ?><?cs 
    124      call:node_change(item, 'mod', 'modified') ?><?cs 
    125     /if ?> 
    126    </li> 
    127   <?cs /each ?></ul> 
    128  </dd> 
    129 </dl> 
    130  
    131 <div class="diff"> 
    132  <div id="legend"> 
    133   <h3>Legend:</h3> 
    134   <dl> 
    135    <dt class="unmod"></dt><dd>Unmodified</dd> 
    136    <dt class="add"></dt><dd>Added</dd> 
    137    <dt class="rem"></dt><dd>Removed</dd> 
    138    <dt class="mod"></dt><dd>Modified</dd> 
    139    <dt class="cp"></dt><dd>Copied</dd> 
    140    <dt class="mv"></dt><dd>Moved</dd> 
    141   </dl> 
    142  </div> 
    143  <ul class="entries"><?cs 
    144  each:item = changeset.changes ?><?cs 
    145   if:len(item.diff) || len(item.props) ?><li class="entry" id="file<?cs 
    146    var:name(item) ?>"><h2><a href="<?cs 
    147    var:item.browser_href.new ?>" title="Show new revision <?cs 
    148    var:item.rev.new ?> of this file in browser"><?cs 
    149    var:item.path.new ?></a></h2><?cs 
    150    if:len(item.props) ?><ul class="props"><?cs 
    151     each:prop = item.props ?><li>Property <strong><?cs 
    152      var:name(prop) ?></strong> <?cs 
    153      if:prop.old && prop.new ?>changed from <?cs 
    154      elif:!prop.old ?>set<?cs 
    155      else ?>deleted<?cs 
    156      /if ?><?cs 
    157      if:prop.old && prop.new ?><em><tt><?cs var:prop.old ?></tt></em><?cs /if ?><?cs 
    158      if:prop.new ?> to <em><tt><?cs var:prop.new ?></tt></em><?cs /if ?></li><?cs 
    159     /each ?></ul><?cs 
    160    /if ?><?cs 
    161    if:len(item.diff) ?><table class="<?cs 
    162     var:diff.style ?>" summary="Differences" cellspacing="0"><?cs 
    163     if:diff.style == 'sidebyside' ?> 
    164      <colgroup class="l"><col class="lineno" /><col class="content" /></colgroup> 
    165      <colgroup class="r"><col class="lineno" /><col class="content" /></colgroup> 
    166      <thead><tr> 
    167       <th colspan="2"><a href="<?cs 
    168        var:item.browser_href.old ?>" title="Show old rev. <?cs 
    169        var:item.rev.old ?> of <?cs var:item.path.old ?>">Revision <?cs 
    170        var:item.rev.old ?></a></th> 
    171       <th colspan="2"><a href="<?cs 
    172        var:item.browser_href.new ?>" title="Show new rev. <?cs 
    173        var:item.rev.new ?> of <?cs var:item.path.new ?>">Revision <?cs 
    174        var:item.rev.new ?></a></th> 
    175       </tr> 
    176      </thead><?cs 
    177      each:change = item.diff ?><tbody><?cs 
    178       call:diff_display(change, diff.style) ?></tbody><?cs 
    179       if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr> 
    180        <th>&hellip;</th><td>&nbsp;</td><th>&hellip;</th><td>&nbsp;</td> 
    181       </tr></tbody><?cs /if ?><?cs 
    182      /each ?><?cs 
    183     else ?> 
    184      <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup> 
    185      <thead><tr> 
    186       <th title="Revision <?cs var:item.rev.old ?>"><a href="<?cs 
    187        var:item.browser_href.old ?>" title="Show old version of <?cs 
    188        var:item.path.old ?>">r<?cs var:item.rev.old ?></a></th> 
    189       <th title="Revision <?cs var:item.rev.new ?>"><a href="<?cs 
    190        var:item.browser_href.new ?>" title="Show new version of <?cs 
    191        var:item.path.new ?>">r<?cs var:item.rev.new ?></a></th> 
    192       <th>&nbsp;</th></tr> 
    193      </thead><?cs 
    194      each:change = item.diff ?><?cs 
    195       call:diff_display(change, diff.style) ?><?cs 
    196       if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr> 
    197        <th>&hellip;</th><th>&hellip;</th><td>&nbsp;</td> 
    198       </tr></tbody><?cs /if ?><?cs 
    199      /each ?><?cs 
    200     /if ?></table><?cs 
    201    /if ?></li><?cs 
    202   /if ?><?cs 
    203  /each ?></ul> 
    204 </div> 
    205  
    206 </div> 
    207 <?cs include "footer.cs"?> 
  • templates/diff.cs

    diff -Nru templates/diff.cs templates/diff.cs
     
     1<?cs include "header.cs"?> 
     2<?cs include "macros.cs"?> 
     3 
     4<div id="ctxtnav" class="nav"> 
     5 <h2>Navigation</h2><?cs 
     6 with:links = chrome.links ?> 
     7  <ul><?cs 
     8   if:diff.chgset ?><?cs 
     9    if:len(links.prev) ?> 
     10     <li class="first<?cs if:!len(links.next) ?> last<?cs /if ?>"> 
     11     &larr; <a class="prev" href="<?cs var:links.prev.0.href ?>" title="<?cs 
     12        var:links.prev.0.title ?>">Previous <?cs  
     13         if:diff.restricted ?>Change<?cs else ?>Changeset<?cs /if ?></a> 
     14     </li><?cs 
     15    /if ?><?cs 
     16    if:len(links.next) ?> 
     17     <li class="<?cs if:len(links.prev) ?>first <?cs /if ?>last"> 
     18      <a class="next" href="<?cs var:links.next.0.href ?>" title="<?cs 
     19        var:links.next.0.title ?>">Next <?cs  
     20         if:diff.restricted ?>Change<?cs else ?>Changeset<?cs /if ?></a> &rarr; 
     21     </li><?cs 
     22    /if ?><?cs 
     23   else ?> 
     24    <li class="first"><a href="<?cs var:diff.reverse_href ?>">Reverse Diff</a></li><?cs 
     25   /if ?> 
     26  </ul><?cs 
     27 /with ?> 
     28</div> 
     29 
     30<div id="content" class="changeset"> 
     31 <div id="title"><?cs 
     32  if:diff.chgset ?><?cs 
     33   if:diff.restricted ?> 
     34    <h1>Changeset <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>"> 
     35      <?cs var:diff.new_rev ?></a>  
     36     for <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>"> 
     37      <?cs var:diff.new_path ?></a>  
     38    </h1><?cs 
     39   else ?> 
     40    <h1>Changeset <?cs var:diff.new_rev ?></h1><?cs 
     41   /if ?><?cs 
     42  else ?><?cs 
     43    if:diff.restricted ?> 
     44    <h1>Changes in <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>"> 
     45      <?cs var:diff.new_path ?></a> 
     46     from revision <a title="Show full changeset" href="<?cs var:diff.href.old_rev ?>"> 
     47      <?cs var:diff.old_rev ?></a> 
     48     to <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>"> 
     49      <?cs var:diff.new_rev ?></a> 
     50    </h1><?cs 
     51   else ?> 
     52    <h1>Changes from <a title="Show entry in browser" href="<?cs var:diff.href.old_path ?>"> 
     53      <?cs var:diff.old_path ?></a>  
     54     at revision <a title="Show full changeset" href="<?cs var:diff.href.old_rev ?>"> 
     55      <?cs var:diff.old_rev ?></a> 
     56     to <a title="Show entry in browser" href="<?cs var:diff.href.new_path ?>"> 
     57     <?cs var:diff.new_path ?></a>  
     58     at revision <a title="Show full changeset" href="<?cs var:diff.href.new_rev ?>"> 
     59     <?cs var:diff.new_rev ?></a> 
     60    </h1><?cs 
     61   /if ?><?cs 
     62  /if ?> 
     63 </div> 
     64 
     65<?cs each:change = diff.changes ?><?cs 
     66 if:len(change.diff) ?><?cs 
     67  set:has_diffs = 1 ?><?cs 
     68 /if ?><?cs 
     69/each ?><?cs if:has_diffs || diff.options.ignoreblanklines  
     70  || diff.options.ignorecase || diff.options.ignorewhitespace ?> 
     71<form method="post" id="prefs" action=""> 
     72 <div><?cs  
     73  if:!diff.chgset ?> 
     74   <input type="hidden" name="old_path" value="<?cs var:diff.old_path ?>" /> 
     75   <input type="hidden" name="path" value="<?cs var:diff.new_path ?>" /> 
     76   <input type="hidden" name="old" value="<?cs var:diff.old_rev ?>" /> 
     77   <input type="hidden" name="new" value="<?cs var:diff.new_rev ?>" /><?cs 
     78  /if ?> 
     79  <label for="style">View differences</label> 
     80  <select id="style" name="style"> 
     81   <option value="inline"<?cs 
     82     if:diff.style == 'inline' ?> selected="selected"<?cs 
     83     /if ?>>inline</option> 
     84   <option value="sidebyside"<?cs 
     85     if:diff.style == 'sidebyside' ?> selected="selected"<?cs 
     86     /if ?>>side by side</option> 
     87  </select> 
     88  <div class="field"> 
     89   Show <input type="text" name="contextlines" id="contextlines" size="2" 
     90     maxlength="3" value="<?cs var:diff.options.contextlines ?>" /> 
     91   <label for="contextlines">lines around each change</label> 
     92  </div> 
     93  <fieldset id="ignore"> 
     94   <legend>Ignore:</legend> 
     95   <div class="field"> 
     96    <input type="checkbox" id="blanklines" name="ignoreblanklines"<?cs 
     97      if:diff.options.ignoreblanklines ?> checked="checked"<?cs /if ?> /> 
     98    <label for="blanklines">Blank lines</label> 
     99   </div> 
     100   <div class="field"> 
     101    <input type="checkbox" id="case" name="ignorecase"<?cs 
     102      if:diff.options.ignorecase ?> checked="checked"<?cs /if ?> /> 
     103    <label for="case">Case changes</label> 
     104   </div> 
     105   <div class="field"> 
     106    <input type="checkbox" id="whitespace" name="ignorewhitespace"<?cs 
     107      if:diff.options.ignorewhitespace ?> checked="checked"<?cs /if ?> /> 
     108    <label for="whitespace">White space changes</label> 
     109   </div> 
     110  </fieldset> 
     111  <div class="buttons"> 
     112   <input type="submit" name="update" value="Update" /> 
     113  </div> 
     114 </div> 
     115</form><?cs /if ?> 
     116 
     117<?cs def:node_change(item,cl,kind) ?><?cs  
     118  set:ndiffs = len(item.diff) ?><?cs 
     119  set:nprops = len(item.props) ?> 
     120  <div class="<?cs var:cl ?>"></div><?cs  
     121  if:cl == "rem" ?> 
     122   <a title="Show what was removed (rev. <?cs var:item.rev.old ?>)" href="<?cs 
     123     var:item.browser_href.old ?>"><?cs var:item.path.old ?></a><?cs 
     124  else ?> 
     125   <a title="Show entry in browser" href="<?cs 
     126     var:item.browser_href.new ?>"><?cs var:item.path.new ?></a><?cs 
     127  /if ?> 
     128  <span class="comment">(<?cs var:kind ?>)</span><?cs 
     129  if:item.path.old && item.change == 'copy' || item.change == 'move' ?> 
     130   <small><em>(<?cs var:kind ?> from <a href="<?cs 
     131    var:item.browser_href.old ?>" title="Show original file (rev. <?cs 
     132    var:item.rev.old ?>)"><?cs var:item.path.old ?></a>)</em></small><?cs 
     133  /if ?><?cs 
     134  if:$ndiffs + $nprops > #0 ?> 
     135    (<a href="#file<?cs var:name(item) ?>" title="Show differences"><?cs 
     136      if:$ndiffs > #0 ?><?cs var:ndiffs ?>&nbsp;diff<?cs if:$ndiffs > #1 ?>s<?cs /if ?><?cs  
     137      /if ?><?cs 
     138      if:$ndiffs && $nprops ?>, <?cs /if ?><?cs  
     139      if:$nprops > #0 ?><?cs var:nprops ?>&nbsp;prop<?cs if:$nprops > #1 ?>s<?cs /if ?><?cs 
     140      /if ?></a>)<?cs 
     141  elif:cl == "mod" ?> 
     142    (<a href="<?cs var:item.browser_href.old ?>" 
     143        title="Show previous version in browser">previous</a>)<?cs 
     144  /if ?> 
     145<?cs /def ?> 
     146 
     147<dl id="overview"><?cs 
     148 if:diff.chgset ?> 
     149 <dt class="time">Timestamp:</dt> 
     150 <dd class="time"><?cs var:changeset.time ?></dd> 
     151 <dt class="author">Author:</dt> 
     152 <dd class="author"><?cs var:changeset.author ?></dd> 
     153 <dt class="message">Message:</dt> 
     154 <dd class="message" id="searchable"><?cs 
     155  alt:changeset.message ?>&nbsp;<?cs /alt ?></dd><?cs 
     156 /if ?> 
     157 <dt class="files"><?cs  
     158  if:len(diff.changes) > #0 ?> 
     159   Files:<?cs 
     160  else ?> 
     161   (None)<?cs 
     162  /if ?> 
     163 </dt> 
     164 <dd class="files"> 
     165  <ul><?cs each:item = diff.changes ?> 
     166   <li><?cs 
     167    if:item.change == 'add' ?><?cs 
     168     call:node_change(item, 'add', 'added') ?><?cs 
     169    elif:item.change == 'delete' ?><?cs 
     170     call:node_change(item, 'rem', 'deleted') ?><?cs 
     171    elif:item.change == 'copy' ?><?cs 
     172     call:node_change(item, 'cp', 'copied') ?><?cs 
     173    elif:item.change == 'move' ?><?cs 
     174     call:node_change(item, 'mv', 'moved') ?><?cs 
     175    elif:item.change == 'edit' ?><?cs 
     176     call:node_change(item, 'mod', 'modified') ?><?cs 
     177    /if ?> 
     178   </li> 
     179  <?cs /each ?></ul> 
     180 </dd> 
     181</dl> 
     182 
     183<div class="diff"> 
     184 <div id="legend"> 
     185  <h3>Legend:</h3> 
     186  <dl> 
     187   <dt class="unmod"></dt><dd>Unmodified</dd> 
     188   <dt class="add"></dt><dd>Added</dd> 
     189   <dt class="rem"></dt><dd>Removed</dd> 
     190   <dt class="mod"></dt><dd>Modified</dd> 
     191   <dt class="cp"></dt><dd>Copied</dd> 
     192   <dt class="mv"></dt><dd>Moved</dd> 
     193  </dl> 
     194 </div> 
     195 <ul class="entries"><?cs 
     196 each:item = diff.changes ?><?cs 
     197  if:len(item.diff) || len(item.props) ?><li class="entry" id="file<?cs 
     198   var:name(item) ?>"><h2><a href="<?cs 
     199   var:item.browser_href.new ?>" title="Show new revision <?cs 
     200   var:item.rev.new ?> of this file in browser"><?cs 
     201   var:item.path.new ?></a></h2><?cs 
     202   if:len(item.props) ?><ul class="props"><?cs 
     203    each:prop = item.props ?><li>Property <strong><?cs 
     204     var:name(prop) ?></strong> <?cs 
     205     if:prop.old && prop.new ?>changed from <?cs 
     206     elif:!prop.old ?>set<?cs 
     207     else ?>deleted<?cs 
     208     /if ?><?cs 
     209     if:prop.old && prop.new ?><em><tt><?cs var:prop.old ?></tt></em><?cs /if ?><?cs 
     210     if:prop.new ?> to <em><tt><?cs var:prop.new ?></tt></em><?cs /if ?></li><?cs 
     211    /each ?></ul><?cs 
     212   /if ?><?cs 
     213   if:len(item.diff) ?><table class="<?cs 
     214    var:diff.style ?>" summary="Differences" cellspacing="0"><?cs 
     215    if:diff.style == 'sidebyside' ?> 
     216     <colgroup class="l"><col class="lineno" /><col class="content" /></colgroup> 
     217     <colgroup class="r"><col class="lineno" /><col class="content" /></colgroup> 
     218     <thead><tr> 
     219      <th colspan="2"><a href="<?cs 
     220       var:item.browser_href.old ?>" title="Show old rev. <?cs 
     221       var:item.rev.old ?> of <?cs var:item.path.old ?>">Revision <?cs 
     222       var:item.rev.old ?></a></th> 
     223      <th colspan="2"><a href="<?cs 
     224       var:item.browser_href.new ?>" title="Show new rev. <?cs 
     225       var:item.rev.new ?> of <?cs var:item.path.new ?>">Revision <?cs 
     226       var:item.rev.new ?></a></th> 
     227      </tr> 
     228     </thead><?cs 
     229     each:change = item.diff ?><tbody><?cs 
     230      call:diff_display(change, diff.style) ?></tbody><?cs 
     231      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr> 
     232       <th>&hellip;</th><td>&nbsp;</td><th>&hellip;</th><td>&nbsp;</td> 
     233      </tr></tbody><?cs /if ?><?cs 
     234     /each ?><?cs 
     235    else ?> 
     236     <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup> 
     237     <thead><tr> 
     238      <th title="Revision <?cs var:item.rev.old ?>"><a href="<?cs 
     239       var:item.browser_href.old ?>" title="Show old version of <?cs 
     240       var:item.path.old ?>">r<?cs var:item.rev.old ?></a></th> 
     241      <th title="Revision <?cs var:item.rev.new ?>"><a href="<?cs 
     242       var:item.browser_href.new ?>" title="Show new version of <?cs 
     243       var:item.path.new ?>">r<?cs var:item.rev.new ?></a></th> 
     244      <th>&nbsp;</th></tr> 
     245     </thead><?cs 
     246     each:change = item.diff ?><?cs 
     247      call:diff_display(change, diff.style) ?><?cs 
     248      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr> 
     249       <th>&hellip;</th><th>&hellip;</th><td>&nbsp;</td> 
     250      </tr></tbody><?cs /if ?><?cs 
     251     /each ?><?cs 
     252    /if ?></table><?cs 
     253   /if ?></li><?cs 
     254  /if ?><?cs 
     255 /each ?></ul> 
     256</div> 
     257 
     258</div> 
     259<?cs include "footer.cs"?> 
  • templates/log.cs

    diff -Nru templates/log.cs templates/log.cs
     
    33 
    44<div id="ctxtnav" class="nav"> 
    55 <ul> 
    6   <li class="last"><a href="<?cs 
    7     var:log.browser_href ?>">View Latest Revision</a></li><?cs 
     6  <li class="last"> 
     7   <a href="<?cs var:log.browser_href ?>">View Latest Revision</a> 
     8  </li><?cs 
    89  if:len(chrome.links.prev) ?> 
    910   <li class="first<?cs if:!len(chrome.links.next) ?> last<?cs /if ?>"> 
    1011    &larr; <a href="<?cs var:chrome.links.prev.0.href ?>" title="<?cs 
     
    6162          title="Warning: by updating, you will clear the page history" /> 
    6263  </div> 
    6364 </form> 
     65 
    6466 <div class="diff"> 
    6567  <div id="legend"> 
    6668   <h3>Legend:</h3> 
     
    7476   </dl> 
    7577  </div> 
    7678 </div> 
     79 
     80 <form action="<?cs var:log.href ?>" method="post"> 
     81  <div class="buttons"><input type="submit" value="View changes"  
     82       title="Diff from Old Revision to New Revision (select them below)" /> 
     83 </div> 
    7784 <table id="chglist" class="listing"> 
    7885  <thead> 
    7986   <tr> 
     87    <th>Old</th> 
     88    <th>New</th> 
    8089    <th class="change"></th> 
    8190    <th class="data">Date</th> 
    8291    <th class="rev">Rev</th> 
     
    8796  </thead> 
    8897  <tbody><?cs 
    8998   set:indent = #1 ?><?cs 
     99   set:idx = #0 ?><?cs 
    90100   each:item = log.items ?><?cs 
    91101    if:item.copyfrom_path ?> 
    92102     <tr class="<?cs if:name(item) % #2 ?>even<?cs else ?>odd<?cs /if ?>"> 
    93       <td class="copyfrom_path" colspan="6" style="padding-left: <?cs var:indent ?>em"> 
     103      <td class="copyfrom_path" colspan="8" style="padding-left: <?cs var:indent ?>em"> 
    94104       copied from <a href="<?cs var:item.browser_href ?>"?><?cs var:item.copyfrom_path ?></a>: 
    95105      </td> 
    96106     </tr><?cs 
     
    99109      set:indent = #1 ?><?cs 
    100110    /if ?> 
    101111    <tr class="<?cs if:name(item) % #2 ?>even<?cs else ?>odd<?cs /if ?>"> 
     112     <td><input type="radio" name="old"  
     113                value="<?cs var:item.path ?>#<?cs var:item.rev ?>" <?cs 
     114          if:idx == #1 ?> checked="checked" <?cs /if ?> /></td> 
     115     <td><input type="radio" name="new"  
     116                value="<?cs var:item.path ?>#<?cs var:item.rev ?>" <?cs 
     117          if:idx == #0 ?> checked="checked" <?cs /if ?> /></td> 
    102118     <td class="change" style="padding-left:<?cs var:indent ?>em"> 
    103119      <a title="View log starting at this revision" href="<?cs var:item.log_href ?>"> 
    104120       <span class="<?cs var:item.change ?>"></span> 
     
    117133     <td class="author"><?cs var:log.changes[item.rev].author ?></td> 
    118134     <td class="summary"><?cs var:log.changes[item.rev].message ?></td> 
    119135    </tr><?cs 
     136    set:idx = idx + 1 ?><?cs 
    120137   /each ?> 
    121138  </tbody> 
    122  </table><?cs 
     139 </table> 
     140 <div class="buttons"><input type="submit" value="View changes"  
     141      title="Diff from Old Revision to New Revision (select them above)" /> 
     142 </div> 
     143 </form><?cs 
    123144 if:len(links.prev) || len(links.next) ?><div id="paging" class="nav"><ul><?cs 
    124145  if:len(links.prev) ?><li class="first<?cs 
    125146   if:!len(links.next) ?> last<?cs /if ?>">&larr; <a href="<?cs 
  • templates/wiki.cs

    diff -Nru templates/wiki.cs templates/wiki.cs
     
    154154    var:wiki.page_name ?></a></h1> 
    155155  <?cs if:len(wiki.history) ?><form method="get" action=""> 
    156156   <input type="hidden" name="action" value="diff" /> 
     157   <div class="buttons"> 
     158    <input type="submit" value="View changes" /> 
     159   </div> 
    157160   <table id="wikihist" class="listing" summary="Change history"> 
    158161    <thead><tr> 
    159162     <th class="diff"></th> 
  • trac/__init__.py

    diff -Nru trac/__init__.py trac/__init__.py
     
    1010""" 
    1111__docformat__ = 'epytext en' 
    1212 
    13 __version__ = '0.9.3' 
     13__version__ = '0.9.3-trac-diff' 
    1414__url__ = 'http://trac.edgewall.com/' 
    1515__copyright__ = '(C) 2003-2006 Edgewall Software' 
    1616__license__ = 'BSD' 
  • trac/versioncontrol/api.py

    diff -Nru trac/versioncontrol/api.py trac/versioncontrol/api.py
     
    122122        'None' is a valid revision value and represents the youngest revision. 
    123123        """ 
    124124        return NotImplementedError 
    125          
     125 
     126    def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 
     127        """ 
     128        Generator that yields change tuples (old_node, new_node, kind, change) 
     129        for each node change between the two arbitrary (path,rev) pairs. 
     130 
     131        The old_node is assumed to be None when the change is an ADD, 
     132        the new_node is assumed to be None when the change is a DELETE. 
     133        """ 
     134        raise NotImplementedError 
     135 
    126136 
    127137class Node(object): 
    128138    """ 
     
    164174        """ 
    165175        raise NotImplementedError 
    166176 
     177    def get_previous(self): 
     178        """ 
     179        Return the (path, rev, chg) tuple corresponding to the previous 
     180        revision for that node. 
     181        """ 
     182        skip = True 
     183        for p in self.get_history(2): 
     184            if skip: 
     185                skip = False 
     186            else: 
     187                return p 
     188 
    167189    def get_properties(self): 
    168190        """ 
    169191        Returns a dictionary containing the properties (meta-data) of the node. 
  • trac/versioncontrol/cache.py

    diff -Nru trac/versioncontrol/cache.py trac/versioncontrol/cache.py
     
    129129    def normalize_rev(self, rev): 
    130130        return self.repos.normalize_rev(rev) 
    131131 
     132    def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 
     133        return self.repos.get_changes(old_path, old_rev, new_path, new_rev, ignore_ancestry) 
     134 
    132135 
    133136class CachedChangeset(Changeset): 
    134137 
  • trac/versioncontrol/diff.py

    diff -Nru trac/versioncontrol/diff.py trac/versioncontrol/diff.py
     
    217217                           ignore_space_changes) 
    218218    for group in _group_opcodes(opcodes, context): 
    219219        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] 
     220        if i1 == 0 and i2 == 0: 
     221            i1, i2 = -1, -1 # support for 'A'dd changes 
    220222        yield '@@ -%d,%d +%d,%d @@' % (i1 + 1, i2 - i1, j1 + 1, j2 - j1) 
    221223        for tag, i1, i2, j1, j2 in group: 
    222224            if tag == 'equal': 
  • trac/versioncontrol/svn_fs.py

    diff -Nru trac/versioncontrol/svn_fs.py trac/versioncontrol/svn_fs.py
     
    346346                expect_deletion = True 
    347347                rev = self.previous_rev(rev) 
    348348 
     349    def get_changes(self, old_path, old_rev, new_path, new_rev, 
     350                   ignore_ancestry=0): 
     351        old_node = new_node = None 
     352        old_rev = self.normalize_rev(old_rev) 
     353        new_rev = self.normalize_rev(new_rev) 
     354        if self.has_node(old_path, old_rev): 
     355            old_node = self.get_node(old_path, old_rev) 
     356        else: 
     357            raise TracError, ('The Base for Diff is invalid: path %s' 
     358                              ' doesn\'t exist in revision %s' \ 
     359                              % (old_path, old_rev)) 
     360        if self.has_node(new_path, new_rev): 
     361            new_node = self.get_node(new_path, new_rev) 
     362        else: 
     363            raise TracError, ('The Target for Diff is invalid: path %s' 
     364                              ' doesn\'t exist in revision %s' \ 
     365                              % (new_path, new_rev)) 
     366        if new_node.kind != old_node.kind: 
     367            raise TracError, ('Diff mismatch: Base is a %s (%s in revision %s) ' 
     368                              'and Target is a %s (%s in revision %s).' \ 
     369                              % (old_node.kind, old_path, old_rev, 
     370                                 new_node.kind, new_path, new_rev)) 
     371        subpool = Pool(self.pool) 
     372        if new_node.isdir: 
     373            editor = DiffChangeEditor() 
     374            e_ptr, e_baton = delta.make_editor(editor, subpool()) 
     375            old_root = fs.revision_root(self.fs_ptr, old_rev, subpool()) 
     376            new_root = fs.revision_root(self.fs_ptr, new_rev, subpool()) 
     377            def authz_cb(root, path, pool): return 1 
     378            text_deltas = 0 # as this is anyway re-done in Diff.py... 
     379            entry_props = 0 # "... typically used only for working copy updates" 
     380            repos.svn_repos_dir_delta(old_root, 
     381                                      (self.scope + old_path).strip('/'), '', 
     382                                      new_root, 
     383                                      (self.scope + new_path).strip('/'), 
     384                                      e_ptr, e_baton, authz_cb, 
     385                                      text_deltas, 
     386                                      1, # directory 
     387                                      entry_props, 
     388                                      ignore_ancestry, 
     389                                      subpool()) 
     390            for path, kind, change in editor.deltas: 
     391                old_node = new_node = None 
     392                if change != Changeset.ADD: 
     393                    old_node = self.get_node(posixpath.join(old_path, path), 
     394                                             old_rev) 
     395                if change != Changeset.DELETE: 
     396                    new_node = self.get_node(posixpath.join(new_path, path), 
     397                                             new_rev) 
     398                else: 
     399                    kind = _kindmap[fs.check_path(old_root, 
     400                                                  self.scope + old_node.path, 
     401                                                  subpool())] 
     402                yield  (old_node, new_node, kind, change) 
     403        else: 
     404            old_root = fs.revision_root(self.fs_ptr, old_rev, subpool()) 
     405            new_root = fs.revision_root(self.fs_ptr, new_rev, subpool()) 
     406            if fs.contents_changed(old_root, self.scope + old_path, 
     407                                   new_root, self.scope + new_path, 
     408                                   subpool()): 
     409                yield (old_node, new_node, Node.FILE, Changeset.EDIT) 
     410 
    349411 
    350412class SubversionNode(Node): 
    351413 
     
    368430                                               self.pool()) 
    369431        self.created_path = fs.node_created_path(self.root, self.scoped_path, 
    370432                                                 self.pool()) 
    371         # 'created_path' differs from 'path' if the last operation is a copy, 
    372         # and furthermore, 'path' might not exist at 'create_rev' 
     433        # Note: 'created_path' differs from 'path' if the last change was a copy, 
     434        #        and furthermore, 'path' might not exist at 'create_rev'. 
     435        #        The only guarantees are: 
     436        #          * this node exists at (path,rev) 
     437        #          * the node existed at (created_path,created_rev) 
     438        # TODO: check node id 
    373439        self.rev = self.created_rev 
    374440         
    375441        Node.__init__(self, path, self.rev, _kindmap[node_type]) 
     
    414480        if newer: 
    415481            yield newer 
    416482 
     483#    def get_previous(self): 
     484#        # FIXME: redo it with fs.node_history 
     485 
    417486    def get_properties(self): 
    418487        props = fs.node_proplist(self.root, self.scoped_path, self.pool()) 
    419488        for name,value in props.items(): 
     
    513582 
    514583    def _get_prop(self, name): 
    515584        return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool()) 
     585 
     586 
     587# 
     588# Delta editor for diffs between arbitrary nodes 
     589# 
     590# Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used 
     591#         because 'repos.svn_repos_dir_delta' *doesn't* provide it. 
     592# 
     593# Note 2: the 'dir_baton' is the path of the parent directory 
     594# 
     595 
     596class DiffChangeEditor(delta.Editor):  
     597 
     598    def __init__(self): 
     599        self.deltas = [] 
     600     
     601    # -- svn.delta.Editor callbacks 
     602 
     603    def open_root(self, base_revision, dir_pool): 
     604        return ('/', Changeset.EDIT) 
     605 
     606    def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, 
     607                      dir_pool): 
     608        self.deltas.append((path, Node.DIRECTORY, Changeset.ADD)) 
     609        return (path, Changeset.ADD) 
     610 
     611    def open_directory(self, path, dir_baton, base_revision, dir_pool): 
     612        return (path, dir_baton[1]) 
     613 
     614    def change_dir_prop(self, dir_baton, name, value, pool): 
     615        path, change = dir_baton 
     616        if change != Changeset.ADD: 
     617            self.deltas.append((path, Node.DIRECTORY, change)) 
     618 
     619    def delete_entry(self, path, revision, dir_baton, pool): 
     620        self.deltas.append((path, None, Changeset.DELETE)) 
     621 
     622    def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, 
     623                 dir_pool): 
     624        self.deltas.append((path, Node.FILE, Changeset.ADD)) 
     625 
     626    def open_file(self, path, dir_baton, dummy_rev, file_pool): 
     627        self.deltas.append((path, Node.FILE, Changeset.EDIT)) 
     628 
  • trac/versioncontrol/tests/svn_fs.py

    diff -Nru trac/versioncontrol/tests/svn_fs.py trac/versioncontrol/tests/svn_fs.py
     
    221221        self.assertEqual(('tags/v1', 7, 'unknown'), history.next()) 
    222222        self.assertRaises(StopIteration, history.next) 
    223223 
     224    # Diffs 
     225 
     226    def _cmp_diff(self, expected, got): 
     227        if expected[0]: 
     228            old = self.repos.get_node(*expected[0]) 
     229            self.assertEqual((old.path, old.rev), (got[0].path, got[0].rev)) 
     230        if expected[1]: 
     231            new = self.repos.get_node(*expected[1]) 
     232            self.assertEqual((new.path, new.rev), (got[1].path, got[1].rev)) 
     233        self.assertEqual(expected[2], (got[2], got[3])) 
     234         
     235    def test_diff_file_different_revs(self): 
     236        diffs = self.repos.get_changes('trunk/README.txt', 2, 'trunk/README.txt', 3) 
     237        self._cmp_diff((('trunk/README.txt', 2), 
     238                        ('trunk/README.txt', 3), 
     239                        (Node.FILE, Changeset.EDIT)), diffs.next()) 
     240        self.assertRaises(StopIteration, diffs.next) 
     241 
     242    def test_diff_file_different_files(self): 
     243        diffs = self.repos.get_changes('branches/v1x/README.txt', 12, 
     244                                      'branches/v1x/README2.txt', 12) 
     245        self._cmp_diff((('branches/v1x/README.txt', 12), 
     246                        ('branches/v1x/README2.txt', 12), 
     247                        (Node.FILE, Changeset.EDIT)), diffs.next()) 
     248        self.assertRaises(StopIteration, diffs.next) 
     249 
     250    def test_diff_file_no_change(self): 
     251        diffs = self.repos.get_changes('trunk/README.txt', 7, 
     252                                      'tags/v1/README.txt', 7) 
     253        self.assertRaises(StopIteration, diffs.next) 
     254  
     255    def test_diff_dir_different_revs(self): 
     256        diffs = self.repos.get_changes('trunk', 4, 'trunk', 8) 
     257        self._cmp_diff((None, ('trunk/dir1/dir2', 8), 
     258                        (Node.DIRECTORY, Changeset.ADD)), diffs.next()) 
     259        self._cmp_diff((None, ('trunk/dir1/dir3', 8), 
     260                        (Node.DIRECTORY, Changeset.ADD)), diffs.next()) 
     261        self._cmp_diff((None, ('trunk/README2.txt', 6), 
     262                        (Node.FILE, Changeset.ADD)), diffs.next()) 
     263        self._cmp_diff((('trunk/dir2', 4), None, 
     264                        (Node.DIRECTORY, Changeset.DELETE)), diffs.next()) 
     265        self._cmp_diff((('trunk/dir3', 4), None, 
     266                        (Node.DIRECTORY, Changeset.DELETE)), diffs.next()) 
     267        self.assertRaises(StopIteration, diffs.next) 
     268 
     269    def test_diff_dir_different_dirs(self): 
     270        diffs = self.repos.get_changes('trunk', 1, 'branches/v1x', 12) 
     271        self._cmp_diff((None, ('branches/v1x/dir1', 12), 
     272                        (Node.DIRECTORY, Changeset.ADD)), diffs.next()) 
     273        self._cmp_diff((None, ('branches/v1x/dir1/dir2', 12), 
     274                        (Node.DIRECTORY, Changeset.ADD)), diffs.next()) 
     275        self._cmp_diff((None, ('branches/v1x/dir1/dir3', 12), 
     276                        (Node.DIRECTORY, Changeset.ADD)), diffs.next()) 
     277        self._cmp_diff((None, ('branches/v1x/README.txt', 12), 
     278                        (Node.FILE, Changeset.ADD)), diffs.next()) 
     279        self._cmp_diff((None, ('branches/v1x/README2.txt', 12), 
     280                        (Node.FILE, Changeset.ADD)), diffs.next()) 
     281        self.assertRaises(StopIteration, diffs.next) 
     282 
     283    def test_diff_dir_no_change(self): 
     284        diffs = self.repos.get_changes('trunk', 7, 
     285                                      'tags/v1', 7) 
     286        self.assertRaises(StopIteration, diffs.next) 
     287         
     288    # Changesets 
     289 
    224290    def test_changeset_repos_creation(self): 
    225291        chgset = self.repos.get_changeset(0) 
    226292        self.assertEqual(0, chgset.rev) 
  • trac/versioncontrol/web_ui/browser.py

    diff -Nru trac/versioncontrol/web_ui/browser.py trac/versioncontrol/web_ui/browser.py
     
    9191 
    9292        repos = self.env.get_repository(req.authname) 
    9393        node = get_existing_node(self.env, repos, path, rev) 
     94        rev = repos.normalize_rev(rev) 
    9495 
    9596        hidden_properties = [p.strip() for p 
    9697                             in self.config.get('browser', 'hide_properties', 
    9798                                                'svk:merge').split(',')] 
     99 
    98100        req.hdf['title'] = path 
    99         req.hdf['browser'] = { 
     101        browser_hdf = { 
    100102            'path': path, 
    101             'revision': rev or repos.youngest_rev, 
     103            'revision': rev, 
    102104            'props': dict([(name, value) 
    103105                           for name, value in node.get_properties().items() 
    104                            if not name in hidden_properties]), 
    105             'href': self.env.href.browser(path, rev=rev or 
    106                                           repos.youngest_rev), 
    107             'log_href': self.env.href.log(path, rev=rev or None) 
    108         } 
     106                           if not name in hidden_properties]) 
     107            } 
     108        browser_hrefs = { 
     109            'href': self.env.href.browser(path, rev=rev), 
     110            'restr_changeset_href': self.env.href.changeset(node.rev, path), 
     111            'anydiff_href': self.env.href.anydiff(), 
     112            'log_href': self.env.href.log(path, rev=rev) 
     113            } 
     114        browser_hdf.update(dict([(key, util.escape(href)) for key, href in 
     115                                 browser_hrefs.items()])) 
     116        req.hdf['browser'] = browser_hdf 
    109117 
    110118        path_links = get_path_links(self.env.href, path, rev) 
    111119        if len(path_links) > 1: 
     
    165173 
    166174        req.hdf['browser.items'] = info 
    167175        req.hdf['browser.changes'] = changes 
    168  
     176        if node.path != '': 
     177            zip_href = self.env.href.diff(node.path, new=rev, old=rev, 
     178                                          old_path='/', # special case (#238) 
     179                                          format='zip') 
     180            add_link(req, 'alternate', zip_href, 'Zip Archive', 
     181                     'application/zip', 'zip') 
     182         
     183         
    169184    def _render_file(self, req, repos, node, rev=None): 
    170185        req.perm.assert_permission('FILE_VIEW') 
    171186 
  • trac/versioncontrol/web_ui/changeset.py

    diff -Nru trac/versioncontrol/web_ui/changeset.py trac/versioncontrol/web_ui/changeset.py
     
    2222 
    2323from trac import mimeview, util 
    2424from trac.core import * 
    25 from trac.perm import IPermissionRequestor 
    2625from trac.Search import ISearchSource, query_to_sql, shorten_result 
    2726from trac.Timeline import ITimelineEventProvider 
    2827from trac.versioncontrol import Changeset, Node 
    2928from trac.versioncontrol.svn_authz import SubversionAuthorizer 
    30 from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff 
    3129from trac.web import IRequestHandler 
    32 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 
     30from trac.web.chrome import INavigationContributor 
    3331from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider 
     32from trac.versioncontrol.web_ui.diff import AbstractDiffModule 
    3433 
     34class ChangesetModule(AbstractDiffModule): 
    3535 
    36 class ChangesetModule(Component): 
    37  
    38     implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
     36    implements(INavigationContributor,  
    3937               ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource) 
    4038 
    4139    # INavigationContributor methods 
     
    4644    def get_navigation_items(self, req): 
    4745        return [] 
    4846 
    49     # IPermissionRequestor methods 
    50  
    51     def get_permission_actions(self): 
    52         return ['CHANGESET_VIEW'] 
    53  
    54     # IRequestHandler methods 
     47    # (reimplemented) IRequestHandler methods 
    5548 
    5649    def match_request(self, req): 
    57         match = re.match(r'/changeset/([0-9]+)$', req.path_info) 
     50        match = re.match(r'/changeset/([0-9]+)(/.*)?$', req.path_info) 
    5851        if match: 
    5952            req.args['rev'] = match.group(1) 
     53            path = match.group(2) 
     54            if path: 
     55                req.args['path'] = path 
    6056            return 1 
    6157 
    62     def process_request(self, req): 
    63         req.perm.assert_permission('CHANGESET_VIEW') 
    64  
    65         rev = req.args.get('rev') 
    66         repos = self.env.get_repository(req.authname) 
    67         authzperm = SubversionAuthorizer(self.env, req.authname) 
    68         authzperm.assert_permission_for_changeset(rev) 
    69  
    70         diff_options = get_diff_options(req) 
    71         if req.args.has_key('update'): 
    72             req.redirect(self.env.href.changeset(rev)) 
    73  
    74         chgset = repos.get_changeset(rev) 
    75         req.check_modified(chgset.date, 
    76                            diff_options[0] + ''.join(diff_options[1])) 
    77  
    78         format = req.args.get('format') 
    79         if format == 'diff': 
    80             self._render_diff(req, repos, chgset, diff_options) 
    81             return 
    82         elif format == 'zip': 
    83             self._render_zip(req, repos, chgset) 
    84             return 
    85  
    86         self._render_html(req, repos, chgset, diff_options) 
    87         add_link(req, 'alternate', '?format=diff', 'Unified Diff', 
    88                  'text/plain', 'diff') 
    89         add_link(req, 'alternate', '?format=zip', 'Zip Archive', 
    90                  'application/zip', 'zip') 
    91         add_stylesheet(req, 'common/css/changeset.css') 
    92         add_stylesheet(req, 'common/css/diff.css') 
    93         add_stylesheet(req, 'common/css/code.css') 
    94         return 'changeset.cs', None 
    95  
    9658    # ITimelineEventProvider methods 
    9759 
    9860    def get_timeline_filters(self, req): 
     
    145107                          util.Markup(message) 
    146108                rev = repos.previous_rev(rev) 
    147109 
    148     # Internal methods 
    149  
    150     def _render_html(self, req, repos, chgset, diff_options): 
    151         """HTML version""" 
    152         req.hdf['title'] = '[%s]' % chgset.rev 
    153         req.hdf['changeset'] = { 
    154             'revision': chgset.rev, 
    155             'time': util.format_datetime(chgset.date), 
    156             'author': chgset.author or 'anonymous', 
    157             'message': wiki_to_html(chgset.message or '--', self.env, req, 
    158                                     escape_newlines=True) 
    159         } 
    160  
    161         oldest_rev = repos.oldest_rev 
    162         if chgset.rev != oldest_rev: 
    163             add_link(req, 'first', self.env.href.changeset(oldest_rev), 
    164                      'Changeset %s' % oldest_rev) 
    165             previous_rev = repos.previous_rev(chgset.rev) 
    166             add_link(req, 'prev', self.env.href.changeset(previous_rev), 
    167                      'Changeset %s' % previous_rev) 
    168         youngest_rev = repos.youngest_rev 
    169         if str(chgset.rev) != str(youngest_rev): 
    170             next_rev = repos.next_rev(chgset.rev) 
    171             add_link(req, 'next', self.env.href.changeset(next_rev), 
    172                      'Changeset %s' % next_rev) 
    173             add_link(req, 'last', self.env.href.changeset(youngest_rev), 
    174                      'Changeset %s' % youngest_rev) 
    175  
    176         edits = [] 
    177         idx = 0 
    178         for path, kind, change, base_path, base_rev in chgset.get_changes(): 
    179             info = {'change': change} 
    180             if base_path: 
    181                 info['path.old'] = base_path 
    182                 info['rev.old'] = base_rev 
    183                 info['browser_href.old'] = self.env.href.browser(base_path, 
    184                                                                  rev=base_rev) 
    185             if path: 
    186                 info['path.new'] = path 
    187                 info['rev.new'] = chgset.rev 
    188                 info['browser_href.new'] = self.env.href.browser(path, 
    189                                                                  rev=chgset.rev) 
    190             if change in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE): 
    191                 edits.append((idx, path, kind, base_path, base_rev)) 
    192             req.hdf['changeset.changes.%d' % idx] = info 
    193             idx += 1 
    194  
    195         hidden_properties = [p.strip() for p 
    196                              in self.config.get('browser', 'hide_properties', 
    197                                                 'svk:merge').split(',')] 
    198  
    199         for idx, path, kind, base_path, base_rev in edits: 
    200             old_node = repos.get_node(base_path or path, base_rev) 
    201             new_node = repos.get_node(path, chgset.rev) 
    202  
    203             # Property changes 
    204             old_props = old_node.get_properties() 
    205             new_props = new_node.get_properties() 
    206             changed_props = {} 
    207             if old_props != new_props: 
    208                 for k,v in old_props.items(): 
    209                     if not k in new_props: 
    210                         changed_props[k] = {'old': v} 
    211                     elif v != new_props[k]: 
    212                         changed_props[k] = {'old': v, 'new': new_props[k]} 
    213                 for k,v in new_props.items(): 
    214                     if not k in old_props: 
    215                         changed_props[k] = {'new': v} 
    216                 for k in hidden_properties: 
    217                     if k in changed_props: 
    218                         del changed_props[k] 
    219                 req.hdf['changeset.changes.%d.props' % idx] = changed_props 
    220  
    221             if kind == Node.DIRECTORY: 
    222                 continue 
    223  
    224             # Content changes 
    225             default_charset = self.config.get('trac', 'default_charset') 
    226             old_content = old_node.get_content().read() 
    227             if mimeview.is_binary(old_content): 
    228                 continue 
    229             charset = mimeview.get_charset(old_node.content_type) 
    230             if not charset: 
    231                 charset = mimeview.detect_unicode(old_content) 
    232             old_content = util.to_utf8(old_content, charset or default_charset) 
    233  
    234             new_content = new_node.get_content().read() 
    235             if mimeview.is_binary(new_content): 
    236                 continue 
    237             charset = mimeview.get_charset(new_node.content_type) 
    238             if not charset: 
    239                 charset = mimeview.detect_unicode(new_content) 
    240             new_content = util.to_utf8(new_content, charset or default_charset) 
    241  
    242             if old_content != new_content: 
    243                 context = 3 
    244                 for option in diff_options[1]: 
    245                     if option.startswith('-U'): 
    246                         context = int(option[2:]) 
    247                         break 
    248                 if context < 0: 
    249                     context = None 
    250                 tabwidth = int(self.config.get('diff', 'tab_width', 
    251                                                self.config.get('mimeviewer', 
    252                                                                'tab_width'))) 
    253                 changes = hdf_diff(old_content.splitlines(), 
    254                                    new_content.splitlines(), 
    255                                    context, tabwidth, 
    256                                    ignore_blank_lines='-B' in diff_options[1], 
    257                                    ignore_case='-i' in diff_options[1], 
    258                                    ignore_space_changes='-b' in diff_options[1]) 
    259                 req.hdf['changeset.changes.%d.diff' % idx] = changes 
    260  
    261     def _render_diff(self, req, repos, chgset, diff_options): 
    262         """Raw Unified Diff version""" 
    263         req.send_response(200) 
    264         req.send_header('Content-Type', 'text/plain;charset=utf-8') 
    265         req.send_header('Content-Disposition', 'inline;' 
    266                         'filename=Changeset%s.diff' % chgset.rev) 
    267         req.end_headers() 
    268  
    269         for path, kind, change, base_path, base_rev in chgset.get_changes(): 
    270             if change == Changeset.ADD: 
    271                 old_node = None 
    272             else: 
    273                 old_node = repos.get_node(base_path or path, base_rev) 
    274             if change == Changeset.DELETE: 
    275                 new_node = None 
    276             else: 
    277                 new_node = repos.get_node(path, chgset.rev) 
    278  
    279             # TODO: Property changes 
    280  
    281             # Content changes 
    282             if kind == 'dir': 
    283                 continue 
    284  
    285             default_charset = self.config.get('trac', 'default_charset') 
    286             new_content = old_content = '' 
    287             new_node_info = old_node_info = ('','') 
    288  
    289             if old_node: 
    290                 charset = mimeview.get_charset(old_node.content_type) or \ 
    291                           default_charset 
    292                 old_content = util.to_utf8(old_node.get_content().read(), 
    293                                            charset) 
    294                 old_node_info = (old_node.path, old_node.rev) 
    295             if mimeview.is_binary(old_content): 
    296                 continue 
    297  
    298             if new_node: 
    299                 charset = mimeview.get_charset(new_node.content_type) or \ 
    300                           default_charset 
    301                 new_content = util.to_utf8(new_node.get_content().read(), 
    302                                            charset) 
    303                 new_node_info = (new_node.path, new_node.rev) 
    304             if mimeview.is_binary(new_content): 
    305                 continue 
    306  
    307             if old_content != new_content: 
    308                 context = 3 
    309                 for option in diff_options[1]: 
    310                     if option.startswith('-U'): 
    311                         context = int(option[2:]) 
    312                         break 
    313                 req.write('Index: ' + path + util.CRLF) 
    314                 req.write('=' * 67 + util.CRLF) 
    315                 req.write('--- %s (revision %s)' % old_node_info + 
    316                           util.CRLF) 
    317                 req.write('+++ %s (revision %s)' % new_node_info + 
    318                           util.CRLF) 
    319                 for line in unified_diff(old_content.splitlines(), 
    320                                          new_content.splitlines(), context, 
    321                                          ignore_blank_lines='-B' in diff_options[1], 
    322                                          ignore_case='-i' in diff_options[1], 
    323                                          ignore_space_changes='-b' in diff_options[1]): 
    324                     req.write(line + util.CRLF) 
    325  
    326     def _render_zip(self, req, repos, chgset): 
    327         """ZIP archive with all the added and/or modified files.""" 
    328         req.send_response(200) 
    329         req.send_header('Content-Type', 'application/zip') 
    330         req.send_header('Content-Disposition', 'attachment;' 
    331                         'filename=Changeset%s.zip' % chgset.rev) 
    332         req.end_headers() 
    333  
    334         try: 
    335             from cStringIO import StringIO 
    336         except ImportError: 
    337             from StringIO import StringIO 
    338         from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 
    339  
    340         buf = StringIO() 
    341         zipfile = ZipFile(buf, 'w', ZIP_DEFLATED) 
    342         for path, kind, change, base_path, base_rev in chgset.get_changes(): 
    343             if kind == Node.FILE and change != Changeset.DELETE: 
    344                 node = repos.get_node(path, chgset.rev) 
    345                 zipinfo = ZipInfo() 
    346                 zipinfo.filename = node.path 
    347                 zipinfo.date_time = time.gmtime(node.last_modified)[:6] 
    348                 zipinfo.compress_type = ZIP_DEFLATED 
    349                 zipfile.writestr(zipinfo, node.get_content().read()) 
    350         zipfile.close() 
    351         req.write(buf.getvalue()) 
    352  
    353110    # IWikiSyntaxProvider methods 
    354111     
    355112    def get_wiki_syntax(self): 
    356         yield (r"!?\[\d+\]|(?:\b|!)r\d+\b(?!:\d)", 
     113        yield (r"!?\[\d+(?:/[^\]]*)?\]|(?:\b|!)r\d+\b(?!:\d)", 
    357114               lambda x, y, z: self._format_link(x, 'changeset', 
    358115                                                 y[0] == 'r' and y[1:] 
    359                                                  or y[1:-1], y)) 
     116                                                 or y[1:-1], y, z)) 
    360117 
    361118    def get_link_resolvers(self): 
    362119        yield ('changeset', self._format_link) 
    363120 
    364     def _format_link(self, formatter, ns, rev, label): 
     121    def _format_link(self, formatter, ns, chgset, label, fullmatch=None): 
     122        sep = chgset.find('/') 
     123        if sep > 0: 
     124            rev, path = chgset[:sep], chgset[sep:] 
     125        else: 
     126            rev, path = chgset, None 
    365127        cursor = formatter.db.cursor() 
    366128        cursor.execute('SELECT message FROM revision WHERE rev=%s', (rev,)) 
    367129        row = cursor.fetchone() 
    368130        if row: 
    369131            return '<a class="changeset" title="%s" href="%s">%s</a>' \ 
    370132                   % (util.escape(util.shorten_line(row[0])), 
    371                       formatter.href.changeset(rev), label) 
     133                      formatter.href.changeset(rev, path), label) 
    372134        else: 
    373             return '<a class="missing changeset" href="%s" rel="nofollow">%s</a>' \ 
    374                    % (formatter.href.changeset(rev), label) 
     135            return '<a class="missing changeset" href="%s"' \ 
     136                   ' rel="nofollow">%s</a>' \ 
     137                   % (formatter.href.changeset(rev, path), label) 
    375138 
    376139    # ISearchProvider methods 
    377140 
  • trac/versioncontrol/web_ui/diff.py

    diff -Nru trac/versioncontrol/web_ui/diff.py trac/versioncontrol/web_ui/diff.py
     
     1# -*- coding: iso-8859-1 -*- 
     2# 
     3# Copyright (C) 2003-2005 Edgewall Software 
     4# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com> 
     5# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de> 
     6# All rights reserved. 
     7# 
     8# This software is licensed as described in the file COPYING, which 
     9# you should have received as part of this distribution. The terms 
     10# are also available at http://trac.edgewall.com/license.html. 
     11# 
     12# This software consists of voluntary contributions made by many 
     13# individuals. For the exact contribution history, see the revision 
     14# history and logs, available at http://projects.edgewall.com/trac/. 
     15# 
     16# Author: Jonas Borgström <jonas@edgewall.com> 
     17#         Christopher Lenz <cmlenz@gmx.de> 
     18#         Christian Boos <cboos@neuf.fr> 
     19 
     20import time 
     21import re 
     22import posixpath 
     23from urllib import urlencode 
     24 
     25from trac import mimeview, util 
     26from trac.core import * 
     27from trac.perm import IPermissionRequestor 
     28from trac.versioncontrol import Changeset, Node 
     29from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff 
     30from trac.versioncontrol.svn_authz import SubversionAuthorizer 
     31from trac.web import IRequestHandler 
     32from trac.web.chrome import add_link, add_stylesheet 
     33from trac.wiki import wiki_to_html, IWikiSyntaxProvider 
     34 
     35class DiffArgs(dict): 
     36    def __getattr__(self,str): 
     37        return self[str] 
     38     
     39 
     40class ChangesPermission(Component): 
     41    """Simple permission provider for changes related modules.""" 
     42     
     43    implements(IPermissionRequestor) 
     44     
     45    def get_permission_actions(self): 
     46        return ['CHANGESET_VIEW'] 
     47     
     48 
     49class AbstractDiffModule(Component): 
     50    """Provide flexible functionality for showing sets of differences. 
     51 
     52    If the differences shown are coming from a specific changeset, 
     53    then that changeset informations can be shown too. 
     54 
     55    In addition, it is possible to show only a subset of the changeset: 
     56    Only the changes affecting a given path will be shown. 
     57    This is called the ''restricted'' changeset. 
     58 
     59    But the differences can also be computed in a more general way, 
     60    between two arbitrary paths and/or between two arbitrary revisions. 
     61    In that case, there's no changeset information displayed. 
     62    """ 
     63 
     64    abstract = True 
     65 
     66    implements(IRequestHandler) 
     67 
     68    # IRequestHandler methods 
     69 
     70    def match_request(self, req): 
     71        raise NotImplementedError 
     72 
     73    def process_request(self, req): 
     74        """The appropriate mode of operation is inferred from 
     75        the request parameters: 
     76         * If `old` and `new` parameters are given, it will be an 
     77           arbitrary set of differences: `chgset` is False. 
     78           * If `old_path` is given and is different from `path`, 
     79             it's a generalized diff, from `old_path@old` 
     80             to `path@new`: `restricted` is False. 
     81           * Otherwise those are differences between two arbitrary revisions 
     82             of a given path: `restricted` is True. 
     83         * Otherwise, we are dealing with a changeset only: `chgset` is True. 
     84           * If the `path` is not empty or not the root, then only 
     85             the changes affecting that path (i.e. itself, children or 
     86             ancestors) will be considered: `restricted` is True. 
     87           * Otherwise, it's the full changeset: `restricted` is False. 
     88          
     89        In any case, the given path@rev pair must exist. 
     90        """ 
     91        req.perm.assert_permission('CHANGESET_VIEW') 
     92         
     93        # -- retrieve arguments 
     94        path = req.args.get('path') 
     95        rev = req.args.get('rev') 
     96        old = req.args.get('old') 
     97        new = req.args.get('new') 
     98        old_path = req.args.get('old_path') 
     99 
     100        # -- normalize and check for special case 
     101        repos = self.env.get_repository(req.authname) 
     102        path = repos.normalize_path(path) 
     103        rev = repos.normalize_rev(rev) 
     104        if old_path: # Note: normalize_path now returns '/' if given 'None' 
     105            old_path = repos.normalize_path(old_path) 
     106         
     107        authzperm = SubversionAuthorizer(self.env, req.authname) 
     108        authzperm.assert_permission_for_changeset(rev) 
     109 
     110        if old_path == path and old and old == new: # revert to Changeset 
     111            rev = old 
     112            old_path = old = new = None 
     113 
     114        diff_options = get_diff_options(req) 
     115 
     116        # -- setup the `chgset` and `restricted` flags, see docstring above. 
     117        chgset = not old and not new and not old_path 
     118        if chgset: 
     119            restricted = path != '' and path != '/' # (subset or not) 
     120        else: 
     121            restricted = old_path == path # (same path or not) 
     122 
     123        # -- redirect if changing the diff options 
     124        if req.args.has_key('update'): 
     125            if chgset: 
     126                if restricted: 
     127                    req.redirect(self.env.href.diff(path, rev=rev)) 
     128                else: 
     129                    req.redirect(self.env.href.changeset(rev)) 
     130            else: 
     131                req.redirect(self.env.href.diff(path, new=new, 
     132                                                old_path=old_path, old=old)) 
     133 
     134        # -- preparing the diff arguments 
     135        if chgset: 
     136            prev = repos.get_node(path, rev).get_previous() 
     137            if prev: 
     138                prev_path, prev_rev = prev[:2] 
     139            else: 
     140                prev_path, prev_rev = path, repos.previous_rev(rev) 
     141            diff_args = DiffArgs(old_path=prev_path, old_rev=prev_rev, 
     142                                 new_path=path, new_rev=rev) 
     143        else: 
     144            if not new: 
     145                new = repos.youngest_rev 
     146            elif not old: 
     147                old = repos.youngest_rev 
     148            if not old_path: 
     149                old_path = path 
     150            diff_args = DiffArgs(old_path=old_path, old_rev=old, 
     151                                 new_path=path, new_rev=new) 
     152        if chgset: 
     153            chgset = repos.get_changeset(rev) 
     154            req.check_modified(chgset.date, 
     155                               diff_options[0] + ''.join(diff_options[1])) 
     156        else: 
     157            pass # FIXME: what date should we choose for a diff? 
     158 
     159        req.hdf['diff'] = diff_args 
     160 
     161        format = req.args.get('format') 
     162 
     163        if format in ['diff', 'zip']: 
     164            # choosing an appropriate filename 
     165            rpath = path.replace('/','_') 
     166            if chgset: 
     167                if restricted: 
     168                    filename = 'changeset_%s_r%s' % (rpath, rev) 
     169                else: 
     170                    filename = 'changeset_r%s' % rev 
     171            else: 
     172                if restricted: 
     173                    filename = 'diff-%s-from-r%s-to-r%s' \ 
     174                                  % (rpath, old, new) 
     175                elif old_path == '/': # special case for download (#238) 
     176                    filename = '%s-r%s' % (rpath, old) 
     177                else: 
     178                    filename = 'diff-from-%s-r%s-to-%s-r%s' \ 
     179                               % (old_path.replace('/','_'), old, rpath, new) 
     180            if format == 'diff': 
     181                self._render_diff(req, filename, repos, diff_args, 
     182                                  diff_options) 
     183                return 
     184            elif format == 'zip': 
     185                self._render_zip(req, filename, repos, diff_args) 
     186                return 
     187 
     188        # -- HTML format 
     189        self._render_html(req, repos, chgset, restricted, 
     190                          diff_args, diff_options) 
     191        if chgset: 
     192            diff_params = 'rev=%s' % rev 
     193        else: 
     194            diff_params = urlencode({'path': path, 
     195                                     'new': new, 
     196                                     'old_path': old_path, 
     197                                     'old': old}) 
     198        add_link(req, 'alternate', '?format=diff&'+diff_params, 'Unified Diff', 
     199                 'text/plain', 'diff') 
     200        add_link(req, 'alternate', '?format=zip&'+diff_params, 'Zip Archive', 
     201                 'application/zip', 'zip') 
     202        add_stylesheet(req, 'common/css/changeset.css') 
     203        add_stylesheet(req, 'common/css/diff.css') 
     204        add_stylesheet(req, 'common/css/code.css') 
     205        return 'diff.cs', None 
     206 
     207 
     208    # Internal methods 
     209 
     210    def _render_html(self, req, repos, chgset, restricted, diff, diff_options): 
     211        """ 
     212        HTML version 
     213        """ 
     214        req.hdf['diff'] = { 
     215            'chgset': chgset and True, 
     216            'restricted': restricted, 
     217            'href': { 'new_rev': self.env.href.changeset(diff.new_rev), 
     218                      'old_rev': self.env.href.changeset(diff.old_rev), 
     219                      'new_path': self.env.href.browser(diff.new_path, 
     220                                                        rev=diff.new_rev), 
     221                      'old_path': self.env.href.browser(diff.old_path, 
     222                                                        rev=diff.old_rev) 
     223                      } 
     224            } 
     225         
     226        if chgset: # Changeset Mode (possibly restricted on a path) 
     227            path, rev = diff.new_path, diff.new_rev 
     228 
     229            # -- getting the change summary from the Changeset.get_changes method 
     230            def get_changes(): 
     231                old_node = new_node = None 
     232                for npath, kind, change, opath, orev in chgset.get_changes(): 
     233                    if restricted and \ 
     234                           not (npath.startswith(path)      # npath is below 
     235                                or path.startswith(npath)): # npath is above 
     236                        continue 
     237                    if change != Changeset.ADD: 
     238                        old_node = repos.get_node(opath, orev) 
     239                    if change != Changeset.DELETE: 
     240                        new_node = repos.get_node(npath, rev) 
     241                    yield old_node, new_node, kind, change 
     242                     
     243            def _changeset_title(rev): 
     244                if restricted: 
     245                    return 'Changeset %s for %s' % (rev, path) 
     246                else: 
     247                    return 'Changeset %s' % rev 
     248 
     249            title = _changeset_title(rev) 
     250            req.hdf['changeset'] = { 
     251                'revision': chgset.rev, 
     252                'time': util.format_datetime(chgset.date), 
     253                'author': util.escape(chgset.author or 'anonymous'), 
     254                'message': wiki_to_html(chgset.message or '--', self.env, req, 
     255                                        escape_newlines=True) 
     256                } 
     257            oldest_rev = repos.oldest_rev 
     258            if chgset.rev != oldest_rev: 
     259                if restricted: 
     260                    prev = repos.get_node(path, rev).get_previous() 
     261                    if prev: 
     262                        prev_path, prev_rev = prev[:2] 
     263                        prev_href = self.env.href.changeset(prev_rev, prev_path) 
     264                    else: 
     265                        prev_path = prev_rev = None 
     266                else: 
     267                    prev_path = diff.old_path 
     268                    prev_rev = repos.previous_rev(chgset.rev) 
     269                    add_link(req, 'first', self.env.href.changeset(oldest_rev), 
     270                             'Changeset %s' % oldest_rev) 
     271                    prev_href = self.env.href.changeset(prev_rev) 
     272                if prev_rev: 
     273                    add_link(req, 'prev', prev_href, _changeset_title(prev_rev)) 
     274            youngest_rev = repos.youngest_rev 
     275            if str(chgset.rev) != str(youngest_rev): 
     276                if restricted: 
     277                    next_rev = next_href = None 
     278                    # FIXME: find an effective way to find the next rev 
     279                else: 
     280                    next_rev = repos.next_rev(chgset.rev) 
     281                    next_href = self.env.href.changeset(next_rev) 
     282                    add_link(req, 'last', 
     283                             self.env.href.diff(path, rev=youngest_rev), 
     284                             'Changeset %s' % youngest_rev) 
     285                if next_rev: 
     286                    add_link(req, 'next', next_href, _changeset_title(next_rev)) 
     287 
     288        else: # Diff Mode 
     289            # -- getting the change summary from the Repository.get_changes method 
     290            def get_changes(): 
     291                for d in repos.get_changes(**diff): 
     292                    yield d 
     293                     
     294            reverse_href = self.env.href.diff(diff.old_path, 
     295                                              new=diff.old_rev, 
     296                                              old_path=diff.new_path, 
     297                                              old=diff.new_rev) 
     298            req.hdf['diff.reverse_href'] = reverse_href 
     299            title = self.title_for_diff(diff) 
     300        req.hdf['title'] = title 
     301 
     302        def _change_info(old_node, new_node, change): 
     303            info = {'change': change} 
     304            if old_node: 
     305                info['path.old'] = old_node.path 
     306                info['rev.old'] = old_node.rev # this is the created rev. 
     307                old_href = self.env.href.browser(old_node.path, 
     308                                                 rev=diff.old_rev) 
     309                # Reminder: old_node.path may not exist at old_node.rev 
     310                info['browser_href.old'] = old_href 
     311            if new_node: 
     312                info['path.new'] = new_node.path 
     313                info['rev.new'] = new_node.rev # created rev. 
     314                new_href = self.env.href.browser(new_node.path, 
     315                                                 rev=diff.new_rev) 
     316                # (same remark as above) 
     317                info['browser_href.new'] = new_href 
     318            return info 
     319 
     320        hidden_properties = [p.strip() for p 
     321                             in self.config.get('browser', 'hide_properties', 
     322                                                'svk:merge').split(',')] 
     323 
     324        def _prop_changes(old_node, new_node): 
     325            old_props = old_node.get_properties() 
     326            new_props = new_node.get_properties() 
     327            changed_props = {} 
     328            if old_props != new_props: 
     329                for k,v in old_props.items(): 
     330                    if not k in new_props: 
     331                        changed_props[k] = {'old': v} 
     332                    elif v != new_props[k]: 
     333                        changed_props[k] = {'old': v, 'new': new_props[k]} 
     334                for k,v in new_props.items(): 
     335                    if not k in old_props: 
     336                        changed_props[k] = {'new': v} 
     337                for k in hidden_properties: 
     338                    if k in changed_props: 
     339                        del changed_props[k] 
     340            return changed_props 
     341 
     342        def _content_changes(old_node, new_node): 
     343            """ 
     344            Returns the list of differences. 
     345            The list is empty when no differences between comparable files 
     346            are detected, but the return value is None for non-comparable files. 
     347            """ 
     348            default_charset = self.config.get('trac', 'default_charset') 
     349            old_content = old_node.get_content().read()             
     350            if mimeview.is_binary(old_content): 
     351                return None 
     352            charset = mimeview.get_charset(old_node.content_type) 
     353            if not charset: 
     354                charset = mimeview.detect_unicode(old_content) 
     355            old_content = util.to_utf8(old_content, charset or default_charset) 
     356 
     357            new_content = new_node.get_content().read() 
     358            if mimeview.is_binary(new_content): 
     359                return None 
     360            charset = mimeview.get_charset(new_node.content_type) 
     361            if not charset: 
     362                charset = mimeview.detect_unicode(new_content) 
     363            new_content = util.to_utf8(new_content, charset or default_charset) 
     364 
     365            if old_content != new_content: 
     366                context = 3 
     367                options = diff_options[1] 
     368                for option in options: 
     369                    if option.startswith('-U'): 
     370                        context = int(option[2:]) 
     371                        break 
     372                if context < 0: 
     373                    context = None 
     374                tabwidth = int(self.config.get('diff', 'tab_width', 
     375                                               self.config.get('mimeviewer', 
     376                                                               'tab_width'))) 
     377                return hdf_diff(old_content.splitlines(), 
     378                                new_content.splitlines(), 
     379                                context, tabwidth, 
     380                                ignore_blank_lines='-B' in options, 
     381                                ignore_case='-i' in options, 
     382                                ignore_space_changes='-b' in options) 
     383            else: 
     384                return [] 
     385 
     386        idx = 0 
     387        for old_node, new_node, kind, change in get_changes(): 
     388            if change != Changeset.EDIT: 
     389                show_entry = True 
     390            else: 
     391                show_entry = False 
     392                assert old_node and new_node 
     393                props = _prop_changes(old_node, new_node) 
     394                if props: 
     395                    req.hdf['diff.changes.%d.props' % idx] = props 
     396                    show_entry = True 
     397                if kind == Node.FILE: 
     398                    diffs = _content_changes(old_node, new_node) 
     399                    if diffs != []: 
     400                        if diffs: 
     401                            req.hdf['diff.changes.%d.diff' % idx] = diffs 
     402                        # elif None (means: manually compare to (previous)) 
     403                        show_entry = True 
     404            if show_entry: 
     405                info = _change_info(old_node, new_node, change) 
     406                req.hdf['diff.changes.%d' % idx] = info 
     407            idx += 1 # the sequence should be immutable 
     408 
     409    def _render_diff(self, req, filename, repos, diff, diff_options): 
     410        """Raw Unified Diff version""" 
     411        req.send_response(200) 
     412        req.send_header('Content-Type', 'text/plain;charset=utf-8') 
     413        req.send_header('Content-Disposition', 
     414                        'filename=%s.diff' % filename) 
     415        req.end_headers() 
     416 
     417        for old_node, new_node, kind, change in repos.get_changes(**diff): 
     418            # TODO: Property changes 
     419 
     420            # Content changes 
     421            if kind == Node.DIRECTORY: 
     422                continue 
     423 
     424            default_charset = self.config.get('trac', 'default_charset') 
     425            new_content = old_content = '' 
     426            new_node_info = old_node_info = ('','') 
     427 
     428            if old_node: 
     429                charset = mimeview.get_charset(old_node.content_type) or \ 
     430                          default_charset 
     431                old_content = util.to_utf8(old_node.get_content().read(), 
     432                                           charset) 
     433                old_node_info = (old_node.path, old_node.rev) 
     434                if mimeview.is_binary(old_content): 
     435                    continue 
     436 
     437            if new_node: 
     438                charset = mimeview.get_charset(new_node.content_type) or \ 
     439                          default_charset 
     440                new_content = util.to_utf8(new_node.get_content().read(), 
     441                                           charset) 
     442                new_node_info = (new_node.path, new_node.rev) 
     443                if mimeview.is_binary(new_content): 
     444                    continue 
     445                new_path = new_node.path 
     446            else: 
     447                old_node_path = repos.normalize_path(old_node.path) 
     448                diff_old_path = repos.normalize_path(diff.old_path) 
     449                new_path = posixpath.join(diff.new_path, 
     450                                          old_node_path[len(diff_old_path)+1:]) 
     451 
     452            if old_content != new_content: 
     453                context = 3 
     454                options = diff_options[1] 
     455                for option in options: 
     456                    if option.startswith('-U'): 
     457                        context = int(option[2:]) 
     458                        break 
     459                if not old_node_info[0]: 
     460                    old_node_info = new_node_info # support for 'A'dd changes 
     461                req.write('Index: ' + new_path + util.CRLF) 
     462                req.write('=' * 67 + util.CRLF) 
     463                req.write('--- %s (revision %s)' % old_node_info + 
     464                          util.CRLF) 
     465                req.write('+++ %s (revision %s)' % new_node_info + 
     466                          util.CRLF) 
     467                for line in unified_diff(old_content.splitlines(), 
     468                                         new_content.splitlines(), context, 
     469                                         ignore_blank_lines='-B' in options, 
     470                                         ignore_case='-i' in options, 
     471                                         ignore_space_changes='-b' in options): 
     472                    req.write(line + util.CRLF) 
     473 
     474    def _render_zip(self, req, filename, repos, diff): 
     475        """ZIP archive with all the added and/or modified files.""" 
     476        new_rev = diff.new_rev 
     477        req.send_response(200) 
     478        req.send_header('Content-Type', 'application/zip') 
     479        req.send_header('Content-Disposition', 
     480                        'filename=%s.zip' % filename) 
     481 
     482        try: 
     483            from cStringIO import StringIO 
     484        except ImportError: 
     485            from StringIO import StringIO 
     486        from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 
     487 
     488        buf = StringIO() 
     489        zipfile = ZipFile(buf, 'w', ZIP_DEFLATED) 
     490        for old_node, new_node, kind, change in repos.get_changes(**diff): 
     491            if kind == Node.FILE and change != Changeset.DELETE: 
     492                assert new_node 
     493                zipinfo = ZipInfo() 
     494                zipinfo.filename = new_node.path 
     495                zipinfo.date_time = time.gmtime(new_node.last_modified)[:6] 
     496                zipinfo.compress_type = ZIP_DEFLATED 
     497                zipfile.writestr(zipinfo, new_node.get_content().read()) 
     498        zipfile.close() 
     499 
     500        buf.seek(0, 2) # be sure to be at the end 
     501        req.send_header("Content-Length", buf.tell()) 
     502        req.end_headers() 
     503         
     504        req.write(buf.getvalue()) 
     505 
     506    def title_for_diff(self, diff): 
     507        if diff.new_path == diff.old_path: # ''diff between 2 revisions'' mode 
     508            return 'Diff r%s:%s for %s' \ 
     509                   % (diff.old_rev or 'latest', diff.new_rev or 'latest', 
     510                      diff.new_path or '/') 
     511        else:                              # ''arbitrary diff'' mode 
     512            return 'Diff from %s@%s to %s@%s' \ 
     513                   % (diff.old_path or '/', diff.old_rev or 'latest', 
     514                      diff.new_path or '/', diff.new_rev or 'latest') 
     515 
     516 
     517 
     518class DiffModule(AbstractDiffModule): 
     519 
     520    implements(IWikiSyntaxProvider) 
     521 
     522    # (reimplemented) IRequestHandler methods 
     523 
     524    def match_request(self, req): 
     525        match = re.match(r'/diff(?:(/.*)|$)', req.path_info) 
     526        if match: 
     527            if match.group(1): 
     528                req.args['path'] = match.group(1) 
     529            return 1 
     530 
     531    # IWikiSyntaxProvider methods 
     532     
     533    def get_wiki_syntax(self): 
     534        return [] 
     535 
     536    def get_link_resolvers(self): 
     537        yield ('diff', self._format_link) 
     538 
     539    def _format_link(self, formatter, ns, params, label): 
     540        def pathrev(path): 
     541            if '@' in path: 
     542                return path.split('@', 1) 
     543            else: 
     544                return (path, None) 
     545        if '//' in params: 
     546            p1, p2 = params.split('//', 1) 
     547            old, new = pathrev(p1), pathrev(p2) 
     548            diff = DiffArgs(old_path=old[0], old_rev=old[1], 
     549                            new_path=new[0], new_rev=new[1]) 
     550        else:  
     551            old_path, old_rev = pathrev(params) 
     552            new_rev = None 
     553            if old_rev and ':' in old_rev: 
     554                old_rev, new_rev = old_rev.split(':', 1) 
     555            diff = DiffArgs(old_path=old_path, old_rev=old_rev, 
     556                            new_path=old_path, new_rev=new_rev) 
     557        title = self.title_for_diff(diff) 
     558        href = formatter.href.diff(diff.new_path, new=diff.new_rev, 
     559                                   old_path=diff.old_path, old=diff.old_rev) 
     560        return '<a class="changeset" title="%s" href="%s">%s</a>' \ 
     561               % (title, href, label) 
     562 
     563 
     564class AnyDiffModule(Component): 
     565 
     566    implements(IRequestHandler) 
     567 
     568    # IRequestHandler methods 
     569 
     570    def match_request(self, req): 
     571        return re.match(r'/anydiff$', req.path_info) 
     572 
     573    def process_request(self, req): 
     574        # -- retrieve arguments 
     575        new_path = req.args.get('new_path') 
     576        new_rev = req.args.get('new_rev') 
     577        old_path = req.args.get('old_path') 
     578        old_rev = req.args.get('old_rev') 
     579 
     580        # -- normalize  
     581        repos = self.env.get_repository(req.authname) 
     582        new_path = repos.normalize_path(new_path) 
     583        new_rev = repos.normalize_rev(new_rev) 
     584        old_path = repos.normalize_path(old_path) 
     585        old_rev = repos.normalize_rev(old_rev) 
     586 
     587        authzperm = SubversionAuthorizer(self.env, req.authname) 
     588        authzperm.assert_permission_for_changeset(new_rev) 
     589        authzperm.assert_permission_for_changeset(old_rev) 
     590         
     591        # -- prepare rendering 
     592        req.hdf['anydiff'] = { 
     593            'new_path': new_path, 
     594            'new_rev': new_rev, 
     595            'old_path': old_path, 
     596            'old_rev': old_rev, 
     597            'diff_href': self.env.href.diff(), 
     598            } 
     599 
     600        return 'anydiff.cs', None 
  • trac/versioncontrol/web_ui/__init__.py

    diff -Nru trac/versioncontrol/web_ui/__init__.py trac/versioncontrol/web_ui/__init__.py
     
    11from trac.versioncontrol.web_ui.browser import * 
    22from trac.versioncontrol.web_ui.changeset import * 
    33from trac.versioncontrol.web_ui.log import * 
     4from trac.versioncontrol.web_ui.diff import * 
  • trac/versioncontrol/web_ui/log.py

    diff -Nru trac/versioncontrol/web_ui/log.py trac/versioncontrol/web_ui/log.py
     
    6767        stop_rev = req.args.get('stop_rev') 
    6868        verbose = req.args.get('verbose') 
    6969        limit = LOG_LIMIT 
     70        old = req.args.get('old') 
     71        new = req.args.get('new') 
     72 
     73        repos = self.env.get_repository(req.authname) 
     74        normpath = repos.normalize_path(path) 
     75        rev = str(repos.normalize_rev(rev)) 
     76 
     77        if old and new: 
     78            osep = util.unescape(old).rindex('#') 
     79            nsep = util.unescape(new).rindex('#') 
     80            old_path, old_rev = old[:osep], old[osep+1:] 
     81            new_path, new_rev = new[:nsep], new[nsep+1:] 
     82            req.redirect(self.env.href.diff(new_path, new=new_rev, 
     83                                            old_path=old_path, old=old_rev)) 
    7084 
    7185        req.hdf['title'] = path + ' (log)' 
    7286        req.hdf['log'] = { 
     
    8498        if path_links: 
    8599            add_link(req, 'up', path_links[-1]['href'], 'Parent directory') 
    86100 
    87         repos = self.env.get_repository(req.authname) 
    88         normpath = repos.normalize_path(path) 
    89         rev = str(repos.normalize_rev(rev)) 
    90101 
    91102        # ''Node history'' uses `Node.history()`, 
    92103        # ''Path history'' uses `Repository.get_path_history()` 
  • trac/wiki/tests/wiki-tests.txt

    diff -Nru trac/wiki/tests/wiki-tests.txt trac/wiki/tests/wiki-tests.txt
     
    8888------------------------------ 
    8989[1:2], r1:2, [12:23], r12:23 
    9090============================== 
    91 ticket:1, changeset:1, report:1, source:foo/bar 
     91ticket:1, report:1, source:foo/bar 
     92changeset:1, changeset:1/README.txt 
    9293 
    9394Issue [ticket:1], CS[changeset:1], Listing [report:1], File [source:foo/bar] 
    9495------------------------------ 
    9596<p> 
    96 <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> 
     97<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> 
     98<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> 
    9799</p> 
    98100<p> 
    99101Issue <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> 
     
    118120</p> 
    119121------------------------------ 
    120122============================== 
     123diff:trunk//branch 
     124diff:trunk@12//branch@23 
     125diff:trunk@12:23 
     126diff:@12:23 
     127------------------------------ 
     128<p> 
     129<a class="changeset" title="Diff from trunk@latest to branch@latest" href="/diff/branch?old_path=trunk">diff:trunk//branch</a> 
     130<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> 
     131<a class="changeset" title="Diff r12:23 for trunk" href="/diff/trunk?new=23&old=12&old_path=trunk">diff:trunk@12:23</a> 
     132<a class="changeset" title="Diff r12:23 for /" href="/diff/?new=23&old=12&old_path=">diff:@12:23</a> 
     133</p> 
     134------------------------------ 
     135============================== 
    121136Add-on to changeset:123: 
    122137Some change. 
    123138ticket:1 
  • wiki-default/WikiStart

    diff -Nru wiki-default/WikiStart wiki-default/WikiStart
     
    1 = Welcome to Trac 0.9.3 = 
     1= Welcome to Trac 0.9.3-trac-diff = 
    22 
    33Trac is a '''minimalistic''' approach to '''web-based''' management of 
    44'''software projects'''. Its goal is to simplify effective tracking and handling of software issues, enhancements and overall progress.