Edgewall Software

Ticket #2041: trac-0.9b2-intertrac-r2308.patch

File trac-0.9b2-intertrac-r2308.patch, 132.6 KB (added by cboos, 6 years ago)

Patch adding TracDiff, InterTrac and InterWiki features to trac-0.9b2

  • htdocs/css/browser.css

    diff -ruN -x .svn trac-0.9b2/htdocs/css/browser.css intertrac-branch/htdocs/css/browser.css
    old new  
    4545#dirlist td.name a, #dirlist td.rev a { border-bottom: none; display: block } 
    4646#dirlist td.change * { font-size: 9px } 
    4747 
     48/* Log */ 
     49tr.diff input {  
     50 padding: 0 1em 0 1em; 
     51 margin: 0;  
     52} 
     53 
     54div.buttons { 
     55 clear: left; 
     56} 
     57 
     58#anydiff { 
     59 margin: 0 0 1em; 
     60 float: left; 
     61} 
     62#anydiff form, #anydiff div, #anydiff h2 { 
     63 display: inline; 
     64} 
     65#anydiff input {  
     66 vertical-align: baseline; 
     67 margin: 0 -0.5em 0 1em; 
     68} 
     69 
     70 
    4871/* Styles for the revision log table 
    4972   (extends the styles for "table.listing") */ 
    5073#chglist { margin-top: 0 } 
  • htdocs/css/changeset.css

    diff -ruN -x .svn trac-0.9b2/htdocs/css/changeset.css intertrac-branch/htdocs/css/changeset.css
    old new  
    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 -ruN -x .svn trac-0.9b2/templates/anydiff.cs intertrac-branch/templates/anydiff.cs
    old new  
     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 -ruN -x .svn trac-0.9b2/templates/browser.cs intertrac-branch/templates/browser.cs
    old new  
    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 -ruN -x .svn trac-0.9b2/templates/changeset.cs intertrac-branch/templates/changeset.cs
    old new  
    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      <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> 
    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="2" 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 var:changeset.message ?></dd> 
    110  <dt class="files">Files:</dt> 
    111  <dd class="files"> 
    112   <ul><?cs each:item = changeset.changes ?> 
    113    <li><?cs 
    114     if:item.change == 'add' ?><?cs 
    115      call:node_change(item, 'add', 'added') ?><?cs 
    116     elif:item.change == 'delete' ?><?cs 
    117      call:node_change(item, 'rem', 'deleted') ?><?cs 
    118     elif:item.change == 'copy' ?><?cs 
    119      call:node_change(item, 'cp', 'copied') ?><?cs 
    120     elif:item.change == 'move' ?><?cs 
    121      call:node_change(item, 'mv', 'moved') ?><?cs 
    122     elif:item.change == 'edit' ?><?cs 
    123      call:node_change(item, 'mod', 'modified') ?><?cs 
    124     /if ?> 
    125    </li> 
    126   <?cs /each ?></ul> 
    127  </dd> 
    128 </dl> 
    129  
    130 <div class="diff"> 
    131  <div id="legend"> 
    132   <h3>Legend:</h3> 
    133   <dl> 
    134    <dt class="unmod"></dt><dd>Unmodified</dd> 
    135    <dt class="add"></dt><dd>Added</dd> 
    136    <dt class="rem"></dt><dd>Removed</dd> 
    137    <dt class="mod"></dt><dd>Modified</dd> 
    138    <dt class="cp"></dt><dd>Copied</dd> 
    139    <dt class="mv"></dt><dd>Moved</dd> 
    140   </dl> 
    141  </div> 
    142  <ul class="entries"><?cs 
    143  each:item = changeset.changes ?><?cs 
    144   if:len(item.diff) || len(item.props) ?><li class="entry" id="file<?cs 
    145    var:name(item) ?>"><h2><a href="<?cs 
    146    var:item.browser_href.new ?>" title="Show new revision <?cs 
    147    var:item.rev.new ?> of this file in browser"><?cs 
    148    var:item.path.new ?></a></h2><?cs 
    149    if:len(item.props) ?><ul class="props"><?cs 
    150     each:prop = item.props ?><li>Property <strong><?cs 
    151      var:name(prop) ?></strong> <?cs 
    152      if:prop.old && prop.new ?>changed from <?cs 
    153      elif:!prop.old ?>set<?cs 
    154      else ?>deleted<?cs 
    155      /if ?><?cs 
    156      if:prop.old && prop.new ?><em><tt><?cs var:prop.old ?></tt></em><?cs /if ?><?cs 
    157      if:prop.new ?> to <em><tt><?cs var:prop.new ?></tt></em><?cs /if ?></li><?cs 
    158     /each ?></ul><?cs 
    159    /if ?><?cs 
    160    if:len(item.diff) ?><table class="<?cs 
    161     var:diff.style ?>" summary="Differences" cellspacing="0"><?cs 
    162     if:diff.style == 'sidebyside' ?> 
    163      <colgroup class="l"><col class="lineno" /><col class="content" /></colgroup> 
    164      <colgroup class="r"><col class="lineno" /><col class="content" /></colgroup> 
    165      <thead><tr> 
    166       <th colspan="2"><a href="<?cs 
    167        var:item.browser_href.old ?>" title="Show old rev. <?cs 
    168        var:item.rev.old ?> of <?cs var:item.path.old ?>">Revision <?cs 
    169        var:item.rev.old ?></a></th> 
    170       <th colspan="2"><a href="<?cs 
    171        var:item.browser_href.new ?>" title="Show new rev. <?cs 
    172        var:item.rev.new ?> of <?cs var:item.path.new ?>">Revision <?cs 
    173        var:item.rev.new ?></a></th> 
    174       </tr> 
    175      </thead><?cs 
    176      each:change = item.diff ?><tbody><?cs 
    177       call:diff_display(change, diff.style) ?></tbody><?cs 
    178       if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr> 
    179        <th>&hellip;</th><td>&nbsp;</td><th>&hellip;</th><td>&nbsp;</td> 
    180       </tr></tbody><?cs /if ?><?cs 
    181      /each ?><?cs 
    182     else ?> 
    183      <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup> 
    184      <thead><tr> 
    185       <th title="Revision <?cs var:item.rev.old ?>"><a href="<?cs 
    186        var:item.browser_href.old ?>" title="Show old version of <?cs 
    187        var:item.path.old ?>">r<?cs var:item.rev.old ?></a></th> 
    188       <th title="Revision <?cs var:item.rev.new ?>"><a href="<?cs 
    189        var:item.browser_href.new ?>" title="Show new version of <?cs 
    190        var:item.path.new ?>">r<?cs var:item.rev.new ?></a></th> 
    191       <th>&nbsp;</th></tr> 
    192      </thead><?cs 
    193      each:change = item.diff ?><?cs 
    194       call:diff_display(change, diff.style) ?><?cs 
    195       if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr> 
    196        <th>&hellip;</th><th>&hellip;</th><td>&nbsp;</td> 
    197       </tr></tbody><?cs /if ?><?cs 
    198      /each ?><?cs 
    199     /if ?></table><?cs 
    200    /if ?></li><?cs 
    201   /if ?><?cs 
    202  /each ?></ul> 
    203 </div> 
    204  
    205 </div> 
    206 <?cs include "footer.cs"?> 
  • templates/diff.cs

    diff -ruN -x .svn trac-0.9b2/templates/diff.cs intertrac-branch/templates/diff.cs
    old new  
     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      <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> 
     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="2" 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 var:changeset.message ?></dd><?cs 
     155 /if ?> 
     156 <dt class="files"><?cs  
     157  if:len(diff.changes) > #0 ?> 
     158   Files:<?cs 
     159  else ?> 
     160   (None)<?cs 
     161  /if ?> 
     162 </dt> 
     163 <dd class="files"> 
     164  <ul><?cs each:item = diff.changes ?> 
     165   <li><?cs 
     166    if:item.change == 'add' ?><?cs 
     167     call:node_change(item, 'add', 'added') ?><?cs 
     168    elif:item.change == 'delete' ?><?cs 
     169     call:node_change(item, 'rem', 'deleted') ?><?cs 
     170    elif:item.change == 'copy' ?><?cs 
     171     call:node_change(item, 'cp', 'copied') ?><?cs 
     172    elif:item.change == 'move' ?><?cs 
     173     call:node_change(item, 'mv', 'moved') ?><?cs 
     174    elif:item.change == 'edit' ?><?cs 
     175     call:node_change(item, 'mod', 'modified') ?><?cs 
     176    /if ?> 
     177   </li> 
     178  <?cs /each ?></ul> 
     179 </dd> 
     180</dl> 
     181 
     182<div class="diff"> 
     183 <div id="legend"> 
     184  <h3>Legend:</h3> 
     185  <dl> 
     186   <dt class="unmod"></dt><dd>Unmodified</dd> 
     187   <dt class="add"></dt><dd>Added</dd> 
     188   <dt class="rem"></dt><dd>Removed</dd> 
     189   <dt class="mod"></dt><dd>Modified</dd> 
     190   <dt class="cp"></dt><dd>Copied</dd> 
     191   <dt class="mv"></dt><dd>Moved</dd> 
     192  </dl> 
     193 </div> 
     194 <ul class="entries"><?cs 
     195 each:item = diff.changes ?><?cs 
     196  if:len(item.diff) || len(item.props) ?><li class="entry" id="file<?cs 
     197   var:name(item) ?>"><h2><a href="<?cs 
     198   var:item.browser_href.new ?>" title="Show new revision <?cs 
     199   var:item.rev.new ?> of this file in browser"><?cs 
     200   var:item.path.new ?></a></h2><?cs 
     201   if:len(item.props) ?><ul class="props"><?cs 
     202    each:prop = item.props ?><li>Property <strong><?cs 
     203     var:name(prop) ?></strong> <?cs 
     204     if:prop.old && prop.new ?>changed from <?cs 
     205     elif:!prop.old ?>set<?cs 
     206     else ?>deleted<?cs 
     207     /if ?><?cs 
     208     if:prop.old && prop.new ?><em><tt><?cs var:prop.old ?></tt></em><?cs /if ?><?cs 
     209     if:prop.new ?> to <em><tt><?cs var:prop.new ?></tt></em><?cs /if ?></li><?cs 
     210    /each ?></ul><?cs 
     211   /if ?><?cs 
     212   if:len(item.diff) ?><table class="<?cs 
     213    var:diff.style ?>" summary="Differences" cellspacing="0"><?cs 
     214    if:diff.style == 'sidebyside' ?> 
     215     <colgroup class="l"><col class="lineno" /><col class="content" /></colgroup> 
     216     <colgroup class="r"><col class="lineno" /><col class="content" /></colgroup> 
     217     <thead><tr> 
     218      <th colspan="2"><a href="<?cs 
     219       var:item.browser_href.old ?>" title="Show old rev. <?cs 
     220       var:item.rev.old ?> of <?cs var:item.path.old ?>">Revision <?cs 
     221       var:item.rev.old ?></a></th> 
     222      <th colspan="2"><a href="<?cs 
     223       var:item.browser_href.new ?>" title="Show new rev. <?cs 
     224       var:item.rev.new ?> of <?cs var:item.path.new ?>">Revision <?cs 
     225       var:item.rev.new ?></a></th> 
     226      </tr> 
     227     </thead><?cs 
     228     each:change = item.diff ?><tbody><?cs 
     229      call:diff_display(change, diff.style) ?></tbody><?cs 
     230      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr> 
     231       <th>&hellip;</th><td>&nbsp;</td><th>&hellip;</th><td>&nbsp;</td> 
     232      </tr></tbody><?cs /if ?><?cs 
     233     /each ?><?cs 
     234    else ?> 
     235     <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup> 
     236     <thead><tr> 
     237      <th title="Revision <?cs var:item.rev.old ?>"><a href="<?cs 
     238       var:item.browser_href.old ?>" title="Show old version of <?cs 
     239       var:item.path.old ?>">r<?cs var:item.rev.old ?></a></th> 
     240      <th title="Revision <?cs var:item.rev.new ?>"><a href="<?cs 
     241       var:item.browser_href.new ?>" title="Show new version of <?cs 
     242       var:item.path.new ?>">r<?cs var:item.rev.new ?></a></th> 
     243      <th>&nbsp;</th></tr> 
     244     </thead><?cs 
     245     each:change = item.diff ?><?cs 
     246      call:diff_display(change, diff.style) ?><?cs 
     247      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr> 
     248       <th>&hellip;</th><th>&hellip;</th><td>&nbsp;</td> 
     249      </tr></tbody><?cs /if ?><?cs 
     250     /each ?><?cs 
     251    /if ?></table><?cs 
     252   /if ?></li><?cs 
     253  /if ?><?cs 
     254 /each ?></ul> 
     255</div> 
     256 
     257</div> 
     258<?cs include "footer.cs"?> 
  • templates/log.cs

    diff -ruN -x .svn trac-0.9b2/templates/log.cs intertrac-branch/templates/log.cs
    old new  
    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 -ruN -x .svn trac-0.9b2/templates/wiki.cs intertrac-branch/templates/wiki.cs
    old new  
    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/db_default.py

    diff -ruN -x .svn trac-0.9b2/trac/db_default.py intertrac-branch/trac/db_default.py
    old new  
    432432  ('timeline', 'default_daysback', '30'), 
    433433  ('browser', 'hide_properties', 'svk:merge'), 
    434434  ('wiki', 'ignore_missing_pages', 'false'), 
     435  ('disabled_components', 'trac.wiki.api.StandardWikiPageNames', 'no'), 
     436  ('disabled_components', 'trac.wiki.api.FlexibleWikiPageNames', 'yes'), 
     437  ('disabled_components', 'trac.wiki.api.SubWikiPageNames', 'yes'), 
    435438) 
    436439 
    437440default_components = ('trac.About', 'trac.attachment',  
  • trac/env.py

    diff -ruN -x .svn trac-0.9b2/trac/env.py intertrac-branch/trac/env.py
    old new  
    6666        ComponentManager.__init__(self) 
    6767 
    6868        self.path = path 
     69        self.siblings = {} 
    6970        self.__cnx_pool = None 
    7071        if create: 
    7172            self.create(db_str) 
  • trac/__init__.py

    diff -ruN -x .svn trac-0.9b2/trac/__init__.py intertrac-branch/trac/__init__.py
    old new  
    1010""" 
    1111__docformat__ = 'epytext en' 
    1212 
    13 __version__ = '0.9b2' 
     13__version__ = '0.9b2-intertrac' 
    1414__url__ = 'http://trac.edgewall.com/' 
    1515__copyright__ = '(C) 2003-2005 Edgewall Software' 
    1616__license__ = 'BSD' 
  • trac/siteconfig.py

    diff -ruN -x .svn trac-0.9b2/trac/siteconfig.py intertrac-branch/trac/siteconfig.py
    old new  
     1 
     2# PLEASE DO NOT EDIT THIS FILE! 
     3# This file was autogenerated when installing Trac 0.9b1-intertrac. 
     4# 
     5__default_templates_dir__ = '/opt/trac/stable-bct-trac/share/trac/templates' 
     6__default_htdocs_dir__ = '/opt/trac/stable-bct-trac/share/trac/htdocs' 
     7__default_wiki_dir__ = '/opt/trac/stable-bct-trac/share/trac/wiki-default' 
     8__default_macros_dir__ = '/opt/trac/stable-bct-trac/share/trac/wiki-macros' 
     9 
  • trac/ticket/api.py

    diff -ruN -x .svn trac-0.9b2/trac/ticket/api.py intertrac-branch/trac/ticket/api.py
    old new  
    1919from trac import util 
    2020from trac.core import * 
    2121from trac.perm import IPermissionRequestor 
    22 from trac.wiki import IWikiSyntaxProvider 
     22from trac.wiki import IWikiSyntaxProvider, INTERTRAC_SCHEME 
    2323from trac.Search import ISearchSource, query_to_sql, shorten_result 
    2424 
    2525 
    … …  
    149149                ('ticket', self._format_link)] 
    150150 
    151151    def get_wiki_syntax(self): 
    152         yield (r"!?#\d+", 
    153                lambda x, y, z: self._format_link(x, 'ticket', y[1:], y)) 
     152        yield (r"!?#(?P<it_ticket>%s)?\d+" % INTERTRAC_SCHEME, 
     153               lambda x, y, z: self._format_link(x, 'ticket', y[1:], y, z)) 
    154154 
    155     def _format_link(self, formatter, ns, target, label): 
     155    def _format_link(self, formatter, ns, target, label, fullmatch=None): 
     156        intertrac = formatter.shorthand_intertrac_helper(ns, target, label, 
     157                                                         fullmatch) 
     158        if intertrac: 
     159            return intertrac 
    156160        cursor = formatter.db.cursor() 
    157161        cursor.execute("SELECT summary,status FROM ticket WHERE id=%s", 
    158162                       (target,)) 
  • trac/ticket/report.py

    diff -ruN -x .svn trac-0.9b2/trac/ticket/report.py intertrac-branch/trac/ticket/report.py
    old new  
    2424from trac.perm import IPermissionRequestor 
    2525from trac.web import IRequestHandler 
    2626from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 
    27 from trac.wiki import wiki_to_html, IWikiSyntaxProvider 
     27from trac.wiki import wiki_to_html, IWikiSyntaxProvider, INTERTRAC_SCHEME 
    2828 
    2929 
    3030dynvars_re = re.compile('\$([A-Z]+)') 
    … …  
    510510        yield ('report', self._format_link) 
    511511 
    512512    def get_wiki_syntax(self): 
    513         yield (r"!?\{\d+\}", lambda x, y, z: self._format_link(x, 'report', y[1:-1], y)) 
     513        yield (r"!?\{(?P<it_report>%s\s*)?\d+\}" % INTERTRAC_SCHEME, 
     514               lambda x, y, z: self._format_link(x, 'report', y[1:-1], y, z)) 
    514515 
    515     def _format_link(self, formatter, ns, target, label): 
     516    def _format_link(self, formatter, ns, target, label, fullmatch=None): 
     517        intertrac = formatter.shorthand_intertrac_helper(ns, target, label, 
     518                                                         fullmatch) 
     519        if intertrac: 
     520            return intertrac 
    516521        return '<a class="report" href="%s">%s</a>' % (formatter.href.report(target), label) 
    517522 
  • trac/versioncontrol/api.py

    diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/api.py intertrac-branch/trac/versioncontrol/api.py
    old new  
    3939        """ 
    4040        raise NotImplementedError 
    4141 
     42    def has_node(self, path, rev): 
     43        """ 
     44        Tell if there's a node at the specified (path,rev) combination. 
     45        """ 
     46        raise NotImplementedError 
     47     
    4248    def get_node(self, path, rev=None): 
    4349        """ 
    4450        Retrieve a Node (directory or file) from the repository at the 
    … …  
    111117        'None' is a valid revision value and represents the youngest revision. 
    112118        """ 
    113119        return NotImplementedError 
    114          
     120 
     121    def get_deltas(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 
     122        """ 
     123        Generator that yields change tuples (old_node, new_node, kind, change) 
     124        for each node change between the two arbitrary (path,rev) pairs. 
     125 
     126        The old_node is assumed to be None when the change is an ADD, 
     127        the new_node is assumed to be None when the change is a DELETE. 
     128        """ 
     129        raise NotImplementedError 
     130 
    115131 
    116132class Node(object): 
    117133    """ 
    … …  
    149165        node (if the underlying version control system supports that), which 
    150166        will be indicated by the first element of the tuple (i.e. the path) 
    151167        changing. 
     168        Starts with an entry for the current revision. 
    152169        """ 
    153170        raise NotImplementedError 
    154171 
     172    def get_previous(self): 
     173        """ 
     174        Return the (path, rev, chg) tuple corresponding to the previous 
     175        revision for that node. 
     176        """ 
     177        skip = True 
     178        for p in self.get_history(2): 
     179            if skip: 
     180                skip = False 
     181            else: 
     182                return p 
     183 
    155184    def get_properties(self): 
    156185        """ 
    157186        Returns a dictionary containing the properties (meta-data) of the node. 
  • trac/versioncontrol/cache.py

    diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/cache.py intertrac-branch/trac/versioncontrol/cache.py
    old new  
    8484    def get_node(self, path, rev=None): 
    8585        return self.repos.get_node(path, rev) 
    8686 
     87    def has_node(self, path, rev): 
     88        return self.repos.has_node(path, rev) 
     89 
    8790    def get_oldest_rev(self): 
    8891        return self.repos.oldest_rev 
    8992 
    … …  
    108111    def normalize_rev(self, rev): 
    109112        return self.repos.normalize_rev(rev) 
    110113 
     114    def get_deltas(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 
     115        return self.repos.get_deltas(old_path, old_rev, new_path, new_rev, ignore_ancestry) 
     116 
    111117 
    112118class CachedChangeset(Changeset): 
    113119 
  • trac/versioncontrol/diff.py

    diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/diff.py intertrac-branch/trac/versioncontrol/diff.py
    old new  
    218218                           ignore_space_changes) 
    219219    for group in _group_opcodes(opcodes, context): 
    220220        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] 
     221        if i1 == 0 and i2 == 0: 
     222            i1, i2 = -1, -1 # support for 'A'dd changes 
    221223        yield '@@ -%d,%d +%d,%d @@' % (i1 + 1, i2 - i1, j1 + 1, j2 - j1) 
    222224        for tag, i1, i2, j1, j2 in group: 
    223225            if tag == 'equal': 
  • trac/versioncontrol/svn_fs.py

    diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/svn_fs.py intertrac-branch/trac/versioncontrol/svn_fs.py
    old new  
    207207    def __del__(self): 
    208208        self.close() 
    209209 
     210    def has_node(self, path, rev): 
     211        rev_root = fs.revision_root(self.fs_ptr, rev, self.pool()) 
     212        node_type = fs.check_path(rev_root, path, self.pool()) 
     213        return node_type in _kindmap 
     214 
    210215    def normalize_path(self, path): 
    211216        return (not path or path == '/') and '/' or path.strip('/') 
    212217 
    … …  
    298303        subpool = Pool(self.pool) 
    299304        while rev: 
    300305            subpool.clear() 
    301             rev_root = fs.revision_root(self.fs_ptr, rev, subpool()) 
    302             node_type = fs.check_path(rev_root, path, subpool()) 
    303             if node_type in _kindmap: # then path exists at that rev 
     306            if self.has_node(path, rev): 
    304307                if expect_deletion: 
    305308                    # it was missing, now it's there again: 
    306309                    #  rev+1 must be a delete 
    … …  
    330333                expect_deletion = True 
    331334                rev = self.previous_rev(rev) 
    332335 
     336    def get_deltas(self, old_path, old_rev, new_path, new_rev, 
     337                   ignore_ancestry=0): 
     338        old_node = new_node = None 
     339        old_rev = self.normalize_rev(old_rev) 
     340        new_rev = self.normalize_rev(new_rev) 
     341        if self.has_node(old_path, old_rev): 
     342            old_node = self.get_node(old_path, old_rev) 
     343        else: 
     344            raise TracError, ('The Base for Diff is invalid: path %s' 
     345                              ' doesn\'t exist in revision %s' \ 
     346                              % (old_path, old_rev)) 
     347        if self.has_node(new_path, new_rev): 
     348            new_node = self.get_node(new_path, new_rev) 
     349        else: 
     350            raise TracError, ('The Target for Diff is invalid: path %s' 
     351                              ' doesn\'t exist in revision %s' \ 
     352                              % (new_path, new_rev)) 
     353        if new_node.kind != old_node.kind: 
     354            raise TracError, ('Diff mismatch: Base is a %s (%s in revision %s) ' 
     355                              'and Target is a %s (%s in revision %s).' \ 
     356                              % (old_node.kind, old_path, old_rev, 
     357                                 new_node.kind, new_path, new_rev)) 
     358        subpool = Pool(self.pool) 
     359        if new_node.isdir: 
     360            editor = DiffChangeEditor() 
     361            e_ptr, e_baton = delta.make_editor(editor, subpool()) 
     362            old_root = fs.revision_root(self.fs_ptr, old_rev, subpool()) 
     363            new_root = fs.revision_root(self.fs_ptr, new_rev, subpool()) 
     364            def authz_cb(root, path, pool): return 1 
     365            text_deltas = 0 # as this is anyway re-done in Diff.py... 
     366            entry_props = 0 # "... typically used only for working copy updates" 
     367            repos.svn_repos_dir_delta(old_root, old_path, '', 
     368                                      new_root, new_path, 
     369                                      e_ptr, e_baton, authz_cb, 
     370                                      text_deltas, 
     371                                      1, # directory 
     372                                      entry_props, 
     373                                      ignore_ancestry, 
     374                                      subpool()) 
     375            for path, kind, change in editor.deltas: 
     376                old_node = new_node = None 
     377                if change != Changeset.ADD: 
     378                    old_node = self.get_node(posixpath.join(old_path, path), 
     379                                             old_rev) 
     380                if change != Changeset.DELETE: 
     381                    new_node = self.get_node(posixpath.join(new_path, path), 
     382                                             new_rev) 
     383                else: 
     384                    kind = _kindmap[fs.check_path(old_root, old_node.path, 
     385                                                  subpool())] 
     386                yield  (old_node, new_node, kind, change) 
     387        else: 
     388            old_root = fs.revision_root(self.fs_ptr, old_rev, subpool()) 
     389            new_root = fs.revision_root(self.fs_ptr, new_rev, subpool()) 
     390            if fs.contents_changed(old_root, old_path, new_root, new_path, 
     391                                   subpool()): 
     392                yield (old_node, new_node, Node.FILE, Changeset.EDIT) 
     393 
    333394 
    334395class SubversionNode(Node): 
    335396 
    … …  
    352413                                               self.pool()) 
    353414        self.created_path = fs.node_created_path(self.root, self.scoped_path, 
    354415                                                 self.pool()) 
    355         # 'created_path' differs from 'path' if the last operation is a copy, 
    356         # and furthermore, 'path' might not exist at 'create_rev' 
     416        # Note: 'created_path' differs from 'path' if the last change was a copy, 
     417        #        and furthermore, 'path' might not exist at 'create_rev'. 
     418        #        The only guarantees are: 
     419        #          * this node exists at (path,rev) 
     420        #          * the node existed at (created_path,created_rev) 
     421        # TODO: check node id 
    357422        self.rev = self.created_rev 
    358423         
    359424        Node.__init__(self, path, self.rev, _kindmap[node_type]) 
    … …  
    393458        if newer: 
    394459            yield newer 
    395460 
     461#    def get_previous(self): 
     462#        # FIXME: redo it with fs.node_history 
     463 
    396464    def get_properties(self): 
    397465        props = fs.node_proplist(self.root, self.scoped_path, self.pool()) 
    398466        for name,value in props.items(): 
    … …  
    487555 
    488556    def _get_prop(self, name): 
    489557        return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool()) 
     558 
     559 
     560# 
     561# Delta editor for diffs between arbitrary nodes 
     562# 
     563# Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used 
     564#         because 'repos.svn_repos_dir_delta' *doesn't* provide it. 
     565# 
     566# Note 2: the 'dir_baton' is the path of the parent directory 
     567# 
     568 
     569class DiffChangeEditor(delta.Editor):  
     570 
     571    def __init__(self): 
     572        self.deltas = [] 
     573     
     574    # -- svn.delta.Editor callbacks 
     575 
     576    def open_root(self, base_revision, dir_pool): 
     577        return ('/', Changeset.EDIT) 
     578 
     579    def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, 
     580                      dir_pool): 
     581        self.deltas.append((path, Node.DIRECTORY, Changeset.ADD)) 
     582        return (path, Changeset.ADD) 
     583 
     584    def open_directory(self, path, dir_baton, base_revision, dir_pool): 
     585        return (path, dir_baton[1]) 
     586 
     587    def change_dir_prop(self, dir_baton, name, value, pool): 
     588        path, change = dir_baton 
     589        if change != Changeset.ADD: 
     590            self.deltas.append((path, Node.DIRECTORY, change)) 
     591 
     592    def delete_entry(self, path, revision, dir_baton, pool): 
     593        self.deltas.append((path, None, Changeset.DELETE)) 
     594 
     595    def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, 
     596                 dir_pool): 
     597        self.deltas.append((path, Node.FILE, Changeset.ADD)) 
     598 
     599    def open_file(self, path, dir_baton, dummy_rev, file_pool): 
     600        self.deltas.append((path, Node.FILE, Changeset.EDIT)) 
     601 
  • trac/versioncontrol/tests/svn_fs.py

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

    diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/browser.py intertrac-branch/trac/versioncontrol/web_ui/browser.py
    old new  
    8888 
    8989        repos = self.env.get_repository(req.authname) 
    9090        node = repos.get_node(path, rev) 
     91        rev = repos.normalize_rev(rev) 
    9192 
    9293        hidden_properties = [p.strip() for p 
    9394                             in self.config.get('browser', 'hide_properties', 
    9495                                                'svk:merge').split(',')] 
     96 
    9597        req.hdf['title'] = path 
    96         req.hdf['browser'] = { 
     98        browser_hdf = { 
    9799            'path': path, 
    98             'revision': rev or repos.youngest_rev, 
     100            'revision': rev, 
    99101            'props': dict([(util.escape(name), util.escape(value)) 
    100102                           for name, value in node.get_properties().items() 
    101                            if not name in hidden_properties]), 
    102             'href': util.escape(self.env.href.browser(path, rev=rev or 
    103                                                       repos.youngest_rev)), 
    104             'log_href': util.escape(self.env.href.log(path)) 
    105         } 
     103                           if not name in hidden_properties]) 
     104            } 
     105        browser_hrefs = { 
     106            'href': self.env.href.browser(path,rev=rev), 
     107            'restr_changeset_href': self.env.href.changeset(node.rev, path), 
     108            'anydiff_href': self.env.href.anydiff(), 
     109            'log_href': self.env.href.log(path) 
     110            } 
     111        browser_hdf.update(dict([(key, util.escape(href)) for key, href in 
     112                                 browser_hrefs.items()])) 
     113        req.hdf['browser'] = browser_hdf 
    106114 
    107115        path_links = get_path_links(self.env.href, path, rev) 
    108116        if len(path_links) > 1: 
    … …  
    162170 
    163171        req.hdf['browser.items'] = info 
    164172        req.hdf['browser.changes'] = changes 
    165  
     173        if node.path != '': 
     174            zip_href = self.env.href.diff(node.path, new=rev, old=rev, 
     175                                          old_path='/', # special case (#238) 
     176                                          format='zip') 
     177            add_link(req, 'alternate', zip_href, 'Zip Archive', 
     178                     'application/zip', 'zip') 
     179         
     180         
    166181    def _render_file(self, req, repos, node, rev=None): 
    167182        req.perm.assert_permission('FILE_VIEW') 
    168183 
  • trac/versioncontrol/web_ui/changeset.py

    diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/changeset.py intertrac-branch/trac/versioncontrol/web_ui/changeset.py
    old new  
    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 
    33 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider 
     30from trac.web.chrome import INavigationContributor 
     31from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider, \ 
     32                      INTERTRAC_SCHEME 
     33from trac.versioncontrol.web_ui.diff import AbstractDiffModule 
    3434 
     35class ChangesetModule(AbstractDiffModule): 
    3536 
    36 class ChangesetModule(Component): 
    37  
    38     implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
     37    implements(INavigationContributor,  
    3938               ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource) 
    4039 
    4140    # INavigationContributor methods 
    … …  
    4645    def get_navigation_items(self, req): 
    4746        return [] 
    4847 
    49     # IPermissionRequestor methods 
    50  
    51     def get_permission_actions(self): 
    52         return ['CHANGESET_VIEW'] 
    53  
    54     # IRequestHandler methods 
     48    # (reimplemented) IRequestHandler methods 
    5549 
    5650    def match_request(self, req): 
    57         match = re.match(r'/changeset/([0-9]+)$', req.path_info) 
     51        match = re.match(r'/changeset/([0-9]+)(/.*)?$', req.path_info) 
    5852        if match: 
    5953            req.args['rev'] = match.group(1) 
     54            path = match.group(2) 
     55            if path: 
     56                req.args['path'] = path 
    6057            return 1 
    6158 
    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  
    9659    # ITimelineEventProvider methods 
    9760 
    9861    def get_timeline_filters(self, req): 
    … …  
    143106                          message 
    144107                rev = repos.previous_rev(rev) 
    145108 
    146     # Internal methods 
    147  
    148     def _render_html(self, req, repos, chgset, diff_options): 
    149         """HTML version""" 
    150         req.hdf['title'] = '[%s]' % chgset.rev 
    151         req.hdf['changeset'] = { 
    152             'revision': chgset.rev, 
    153             'time': util.format_datetime(chgset.date), 
    154             'author': util.escape(chgset.author or 'anonymous'), 
    155             'message': wiki_to_html(chgset.message or '--', self.env, req, 
    156                                     escape_newlines=True) 
    157         } 
    158  
    159         oldest_rev = repos.oldest_rev 
    160         if chgset.rev != oldest_rev: 
    161             add_link(req, 'first', self.env.href.changeset(oldest_rev), 
    162                      'Changeset %s' % oldest_rev) 
    163             previous_rev = repos.previous_rev(chgset.rev) 
    164             add_link(req, 'prev', self.env.href.changeset(previous_rev), 
    165                      'Changeset %s' % previous_rev) 
    166         youngest_rev = repos.youngest_rev 
    167         if str(chgset.rev) != str(youngest_rev): 
    168             next_rev = repos.next_rev(chgset.rev) 
    169             add_link(req, 'next', self.env.href.changeset(next_rev), 
    170                      'Changeset %s' % next_rev) 
    171             add_link(req, 'last', self.env.href.changeset(youngest_rev), 
    172                      'Changeset %s' % youngest_rev) 
    173  
    174         edits = [] 
    175         idx = 0 
    176         for path, kind, change, base_path, base_rev in chgset.get_changes(): 
    177             info = {'change': change} 
    178             if base_path: 
    179                 info['path.old'] = base_path 
    180                 info['rev.old'] = base_rev 
    181                 info['browser_href.old'] = self.env.href.browser(base_path, 
    182                                                                  rev=base_rev) 
    183             if path: 
    184                 info['path.new'] = path 
    185                 info['rev.new'] = chgset.rev 
    186                 info['browser_href.new'] = self.env.href.browser(path, 
    187                                                                  rev=chgset.rev) 
    188             if change in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE): 
    189                 edits.append((idx, path, kind, base_path, base_rev)) 
    190             req.hdf['changeset.changes.%d' % idx] = info 
    191             idx += 1 
    192  
    193         hidden_properties = [p.strip() for p 
    194                              in self.config.get('browser', 'hide_properties', 
    195                                                 'svk:merge').split(',')] 
    196  
    197         for idx, path, kind, base_path, base_rev in edits: 
    198             old_node = repos.get_node(base_path or path, base_rev) 
    199             new_node = repos.get_node(path, chgset.rev) 
    200  
    201             # Property changes 
    202             old_props = old_node.get_properties() 
    203             new_props = new_node.get_properties() 
    204             changed_props = {} 
    205             if old_props != new_props: 
    206                 for k,v in old_props.items(): 
    207                     if not k in new_props: 
    208                         changed_props[k] = {'old': v} 
    209                     elif v != new_props[k]: 
    210                         changed_props[k] = {'old': v, 'new': new_props[k]} 
    211                 for k,v in new_props.items(): 
    212                     if not k in old_props: 
    213                         changed_props[k] = {'new': v} 
    214                 for k in hidden_properties: 
    215                     if k in changed_props: 
    216                         del changed_props[k] 
    217                 req.hdf['changeset.changes.%d.props' % idx] = changed_props 
    218  
    219             if kind == Node.DIRECTORY: 
    220                 continue 
    221  
    222             # Content changes 
    223             default_charset = self.config.get('trac', 'default_charset') 
    224             old_content = old_node.get_content().read() 
    225             if mimeview.is_binary(old_content): 
    226                 continue 
    227             charset = mimeview.get_charset(old_node.content_type) or \ 
    228                       default_charset 
    229             old_content = util.to_utf8(old_content, charset) 
    230  
    231             new_content = new_node.get_content().read() 
    232             if mimeview.is_binary(new_content): 
    233                 continue 
    234             charset = mimeview.get_charset(new_node.content_type) or \ 
    235                       default_charset 
    236             new_content = util.to_utf8(new_content, charset) 
    237  
    238             if old_content != new_content: 
    239                 context = 3 
    240                 for option in diff_options[1]: 
    241                     if option.startswith('-U'): 
    242                         context = int(option[2:]) 
    243                         break 
    244                 tabwidth = int(self.config.get('diff', 'tab_width', 
    245                                                self.config.get('mimeviewer', 
    246                                                                'tab_width'))) 
    247                 changes = hdf_diff(old_content.splitlines(), 
    248                                    new_content.splitlines(), 
    249                                    context, tabwidth, 
    250                                    ignore_blank_lines='-B' in diff_options[1], 
    251                                    ignore_case='-i' in diff_options[1], 
    252                                    ignore_space_changes='-b' in diff_options[1]) 
    253                 req.hdf['changeset.changes.%d.diff' % idx] = changes 
    254  
    255     def _render_diff(self, req, repos, chgset, diff_options): 
    256         """Raw Unified Diff version""" 
    257         req.send_response(200) 
    258         req.send_header('Content-Type', 'text/plain;charset=utf-8') 
    259         req.send_header('Content-Disposition', 
    260                         'filename=Changeset%s.diff' % req.args.get('rev')) 
    261         req.end_headers() 
    262  
    263         for path, kind, change, base_path, base_rev in chgset.get_changes(): 
    264             if change == Changeset.ADD: 
    265                 old_node = None 
    266             else: 
    267                 old_node = repos.get_node(base_path or path, base_rev) 
    268             if change == Changeset.DELETE: 
    269                 new_node = None 
    270             else: 
    271                 new_node = repos.get_node(path, chgset.rev) 
    272  
    273             # TODO: Property changes 
    274  
    275             # Content changes 
    276             if kind == 'dir': 
    277                 continue 
    278  
    279             default_charset = self.config.get('trac', 'default_charset') 
    280             new_content = old_content = '' 
    281             new_node_info = old_node_info = ('','') 
    282  
    283             if old_node: 
    284                 charset = mimeview.get_charset(old_node.content_type) or \ 
    285                           default_charset 
    286                 old_content = util.to_utf8(old_node.get_content().read(), 
    287                                            charset) 
    288                 old_node_info = (old_node.path, old_node.rev) 
    289             if mimeview.is_binary(old_content): 
    290                 continue 
    291  
    292             if new_node: 
    293                 charset = mimeview.get_charset(new_node.content_type) or \ 
    294                           default_charset 
    295                 new_content = util.to_utf8(new_node.get_content().read(), 
    296                                            charset) 
    297                 new_node_info = (new_node.path, new_node.rev) 
    298             if mimeview.is_binary(new_content): 
    299                 continue 
    300  
    301             if old_content != new_content: 
    302                 context = 3 
    303                 for option in diff_options[1]: 
    304                     if option.startswith('-U'): 
    305                         context = int(option[2:]) 
    306                         break 
    307                 req.write('Index: ' + path + util.CRLF) 
    308                 req.write('=' * 67 + util.CRLF) 
    309                 req.write('--- %s (revision %s)' % old_node_info + 
    310                           util.CRLF) 
    311                 req.write('+++ %s (revision %s)' % new_node_info + 
    312                           util.CRLF) 
    313                 for line in unified_diff(old_content.splitlines(), 
    314                                          new_content.splitlines(), context, 
    315                                          ignore_blank_lines='-B' in diff_options[1], 
    316                                          ignore_case='-i' in diff_options[1], 
    317                                          ignore_space_changes='-b' in diff_options[1]): 
    318                     req.write(line + util.CRLF) 
    319  
    320     def _render_zip(self, req, repos, chgset): 
    321         """ZIP archive with all the added and/or modified files.""" 
    322         req.send_response(200) 
    323         req.send_header('Content-Type', 'application/zip') 
    324         req.send_header('Content-Disposition', 
    325                         'filename=Changeset%s.zip' % chgset.rev) 
    326         req.end_headers() 
    327  
    328         try: 
    329             from cStringIO import StringIO 
    330         except ImportError: 
    331             from StringIO import StringIO 
    332         from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 
    333  
    334         buf = StringIO() 
    335         zipfile = ZipFile(buf, 'w', ZIP_DEFLATED) 
    336         for path, kind, change, base_path, base_rev in chgset.get_changes(): 
    337             if kind == Node.FILE and change != Changeset.DELETE: 
    338                 node = repos.get_node(path, chgset.rev) 
    339                 zipinfo = ZipInfo() 
    340                 zipinfo.filename = node.path 
    341                 zipinfo.date_time = time.gmtime(node.last_modified)[:6] 
    342                 zipinfo.compress_type = ZIP_DEFLATED 
    343                 zipfile.writestr(zipinfo, node.get_content().read()) 
    344         zipfile.close() 
    345         req.write(buf.getvalue()) 
    346109 
    347110    # IWikiSyntaxProvider methods 
    348111     
    349112    def get_wiki_syntax(self): 
    350         yield (r"!?\[\d+\]|(?:\b|!)r\d+\b(?!:\d)", 
     113        yield (r"!?\[(?P<it_changeset>%s\s*)?\d+(?:/[^\]]*)?\]|" \ 
     114               % INTERTRAC_SCHEME +                     # [1], [T1] or [trac 1] 
     115               r"(?:\b|!)r\d+\b(?!:\d)",                # r1 but not r1:2 
    351116               lambda x, y, z: self._format_link(x, 'changeset', 
    352117                                                 y[0] == 'r' and y[1:] 
    353                                                  or y[1:-1], y)) 
     118                                                 or y[1:-1], y, z)) 
    354119 
    355120    def get_link_resolvers(self): 
    356121        yield ('changeset', self._format_link) 
    357122 
    358     def _format_link(self, formatter, ns, rev, label): 
     123    def _format_link(self, formatter, ns, chgset, label, fullmatch=None): 
     124        intertrac = formatter.shorthand_intertrac_helper(ns, chgset, label, 
     125                                                         fullmatch) 
     126        if intertrac: 
     127            return intertrac 
     128        sep = chgset.find('/') 
     129        if sep > 0: 
     130            rev, path = chgset[:sep], chgset[sep:] 
     131        else: 
     132            rev, path = chgset, None 
    359133        cursor = formatter.db.cursor() 
    360134        cursor.execute('SELECT message FROM revision WHERE rev=%s', (rev,)) 
    361135        row = cursor.fetchone() 
    362136        if row: 
    363137            return '<a class="changeset" title="%s" href="%s">%s</a>' \ 
    364138                   % (util.escape(util.shorten_line(row[0])), 
    365                       formatter.href.changeset(rev), label) 
     139                      formatter.href.changeset(rev, path), label) 
    366140        else: 
    367             return '<a class="missing changeset" href="%s" rel="nofollow">%s</a>' \ 
    368                    % (formatter.href.changeset(rev), label) 
     141            return '<a class="missing changeset" href="%s"' \ 
     142                   ' rel="nofollow">%s</a>' \ 
     143                   % (formatter.href.changeset(rev, path), label) 
    369144 
    370145    # ISearchProvider methods 
    371146 
  • trac/versioncontrol/web_ui/diff.py

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

    diff -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/__init__.py intertrac-branch/trac/versioncontrol/web_ui/__init__.py
    old new  
    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 -ruN -x .svn trac-0.9b2/trac/versioncontrol/web_ui/log.py intertrac-branch/trac/versioncontrol/web_ui/log.py
    old new  
    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'] = { 
    … …  
    8397        if path_links: 
    8498            add_link(req, 'up', path_links[-1]['href'], 'Parent directory') 
    8599 
    86         repos = self.env.get_repository(req.authname) 
    87         normpath = repos.normalize_path(path) 
    88         rev = str(repos.normalize_rev(rev)) 
    89100 
    90101        # ''Node'' history uses `get_node()`, 
    91102        # ''Path'' history uses `get_path_history()` 
  • trac/web/standalone.py

    diff -ruN -x .svn trac-0.9b2/trac/web/standalone.py intertrac-branch/trac/web/standalone.py
    old new  
    139139        self.auths = auths 
    140140 
    141141        self.projects = {} 
     142        siblings = {} 
    142143        for env_path in env_paths: 
    143144            # Remove trailing slashes 
    144145            while env_path and not os.path.split(env_path)[1]: 
    145146                env_path = os.path.split(env_path)[0] 
    146147            project = os.path.split(env_path)[1] 
    147148            self.projects[project] = env_path 
     149            env = get_environment(None, self.get_env_opts(project)) 
     150            siblings[project] = env 
     151        for p in siblings.values(): 
     152            p.siblings = siblings 
    148153 
    149154    def get_env_opts(self, project=None): 
    150155        if self.env_parent_dir: 
    151156            opts = self.env_parent_dir.items() 
    152157        else: 
    153158            opts = [('TRAC_ENV', self.projects[project])] 
    154         return dict(opts + os.environ.items()) 
     159        return dict(os.environ.items() + opts) # TODO: backport 
    155160 
    156161    def send_project_index(self, req): 
    157162        if self.env_parent_dir: 
  • trac/wiki/api.py

    diff -ruN -x .svn trac-0.9b2/trac/wiki/api.py intertrac-branch/trac/wiki/api.py
    old new  
    2323    import dummy_threading as threading 
    2424import time 
    2525import urllib 
     26import re 
    2627 
    2728from trac.core import * 
    2829from trac.util import to_utf8, TRUE 
    … …  
    6566    def get_link_resolvers(): 
    6667        """Return an iterable over (namespace, formatter) tuples.""" 
    6768 Â 
     69class IWikiPageNameSyntaxProvider(Interface): 
     70 Â 
     71    def get_wiki_page_names_syntax(): 
     72        """ 
     73        Return an iterable that provides a regular expression for 
     74        matching wiki page names (see WikiPageNames) 
     75 
     76        Be careful to only allow __one__ implementation 
     77        (others should be listed in the ![disabled_components] 
     78        section of the TracIni) 
     79        """ 
     80 Â 
    6881 
    6982class WikiSystem(Component): 
    7083    """Represents the wiki system.""" 
    … …  
    7487    change_listeners = ExtensionPoint(IWikiChangeListener) 
    7588    macro_providers = ExtensionPoint(IWikiMacroProvider) 
    7689    syntax_providers = ExtensionPoint(IWikiSyntaxProvider) 
     90    wikipagenames_providers = ExtensionPoint(IWikiPageNameSyntaxProvider) 
    7791 
    7892    INDEX_UPDATE_INTERVAL = 5 # seconds 
    7993 
    … …  
    8195        self._index = None 
    8296        self._last_index_update = 0 
    8397        self._index_lock = threading.RLock() 
     98        self._compiled_rules = None 
     99        self._link_resolvers = None 
     100        self._helper_patterns = None 
     101        self._external_handlers = None 
    84102 
    85103    def _update_index(self): 
    86104        self._index_lock.acquire() 
    … …  
    116134        self._update_index() 
    117135        return self._index.has_key(pagename) 
    118136 
     137    def _get_rules(self): 
     138        self._prepare_rules() 
     139        return self._compiled_rules 
     140    rules = property(_get_rules) 
     141 
     142    def _get_helper_patterns(self): 
     143        self._prepare_rules() 
     144        return self._helper_patterns 
     145    helper_patterns = property(_get_helper_patterns) 
     146 
     147    def _get_external_handlers(self): 
     148        self._prepare_rules() 
     149        return self._external_handlers 
     150    external_handlers = property(_get_external_handlers) 
     151     
     152    def _prepare_rules(self): 
     153        from trac.wiki.formatter import Formatter 
     154        if not self._compiled_rules: 
     155            helpers = [] 
     156            handlers = {} 
     157            syntax = Formatter._pre_rules[:] 
     158            i = 0 
     159            for resolver in self.syntax_providers: 
     160                for regexp, handler in resolver.get_wiki_syntax(): 
     161                    handlers['i'+str(i)] = handler 
     162                    syntax.append('(?P<i%d>%s)' % (i, regexp)) 
     163                    i += 1 
     164            syntax += Formatter._post_rules[:] 
     165            helper_re = re.compile(r'\?P<([a-z\d_]+)>') 
     166            for rule in syntax: 
     167                helpers += helper_re.findall(rule)[1:] 
     168            rules = re.compile('(?:' + '|'.join(syntax) + ')') 
     169            self._external_handlers = handlers 
     170            self._helper_patterns = helpers 
     171            self._compiled_rules = rules 
     172 
     173    def _get_link_resolvers(self): 
     174        if not self._link_resolvers: 
     175            resolvers = {} 
     176            for resolver in self.syntax_providers: 
     177                for namespace, handler in resolver.get_link_resolvers(): 
     178                    resolvers[namespace] = handler 
     179            self._link_resolvers = resolvers 
     180        return self._link_resolvers 
     181    link_resolvers = property(_get_link_resolvers) 
     182 
    119183    # IWikiChangeListener methods 
    120184 
    121185    def wiki_page_added(self, page): 
    … …  
    136200    def get_wiki_syntax(self): 
    137201        ignore_missing = self.config.get('wiki', 'ignore_missing_pages') 
    138202        ignore_missing = ignore_missing in TRUE 
    139         yield (r"!?(?<!/)\b[A-Z][a-z]+(?:[A-Z][a-z]*[a-z/])+" 
    140                 "(?:#[A-Za-z0-9]+)?(?=\Z|\s|[.,;:!?\)}\]])", 
    141                lambda x, y, z: self._format_link(x, 'wiki', y, y, 
    142                                                  ignore_missing)) 
     203        providers = [] 
     204        for p in self.wikipagenames_providers: 
     205            if not providers: 
     206                yield (p.get_wiki_page_names_syntax(), 
     207                       lambda x, y, z: self._format_link(x, 'wiki', y, y, 
     208                                                         ignore_missing)) 
     209            pc = p.__class__ 
     210            providers.append('# %s\n%s.%s = yes' % (pc.__doc__.split('\n')[0], 
     211                                                    pc.__module__, pc.__name__) 
     212                                                    ) 
     213        if len(providers) > 1: 
     214            self.log.warning('More than one IWikiPageNameSyntaxProvider ' 
     215                             'implementation available:\n' 
     216                             'You should set one of the following to "no" ' 
     217                             'in your trac.ini:\n\n' 
     218                             '[disabled_components]\n' + 
     219                             '\n'.join(providers) +'\n') 
    143220 
    144221    def get_link_resolvers(self): 
    145222        yield ('wiki', self._format_fancy_link) 
    … …  
    163240        else: 
    164241            return '<a class="wiki" href="%s">%s</a>' \ 
    165242                   % (formatter.href.wiki(page) + anchor, label) 
     243 
     244 
     245WIKI_START = r"!?(?<!/)\b" 
     246WIKI_TARGET = r"(?:#[A-Za-z0-9]+)?" 
     247WIKI_END = r"(?=\Z|\s|[.,;:!?\)}\]])" 
     248WIKI_INTERWIKI = r"(?!:\S)" 
     249 
     250class StandardWikiPageNames(Component): 
     251    """Standard Trac WikiPageNames rule""" 
     252 
     253    implements(IWikiPageNameSyntaxProvider) 
     254 
     255    def get_wiki_page_names_syntax(self): 
     256        return (WIKI_START +                # where to start 
     257                r"[A-Z][a-z]+"              # initial WikiPageNames word 
     258                r"(?:[A-Z][a-z]*[a-z/])+" + # additional WikiPageNames word 
     259                WIKI_TARGET +               # optional trailing section link 
     260                WIKI_END +                  # where to end 
     261                WIKI_INTERWIKI)             # InterWiki support 
     262     
     263class FlexibleWikiPageNames(Component): 
     264    """Standard Trac WikiPageNames rule, plus digits 
     265    and consecutive upper-case characters allowed. 
     266 
     267    More precisely, WikiPageNames are: 
     268     * either 2 or more starting upper case letter or digits, 
     269       followed by lower case letters 
     270     * either 1 or more starting upper case letter or digits, 
     271       followed by lower case letters, repeated at least 2 times 
     272       (with optionally '/' between repetitions) 
     273    """ 
     274 
     275    implements(IWikiPageNameSyntaxProvider) 
     276 
     277    def get_wiki_page_names_syntax(self): 
     278        return (WIKI_START + 
     279                r"(?:[A-Z\d]{2,}[a-z]+"                    # 1st way 
     280                r"|[A-Z\d]+[a-z]+(?:/?[A-Z\d]+[a-z]*)+)" + # 2nd way 
     281                WIKI_TARGET + WIKI_END + WIKI_INTERWIKI) 
     282 
     283class SubWikiPageNames(Component): 
     284    """SubWiki-like rules for WikiPageNames. 
     285     
     286    See http://www.webdav.org/wiki/projects/TextFormattingRules 
     287 
     288    Note that '/' in this style of WikiPageNames are not supported. 
     289    """ 
     290 
     291    implements(IWikiPageNameSyntaxProvider) 
     292 
     293    def get_wiki_page_names_syntax(self): 
     294        return (WIKI_START + 
     295                r"(?:[A-Z][A-Z]+[a-z\d]+[A-Z]*"    # 1st and 3rd way 
     296                r"|[A-Z][a-z]+(?:[A-Z][a-z]+)+)" + # 2nd way 
     297                WIKI_TARGET + WIKI_END + WIKI_INTERWIKI) 
     298 
  • trac/wiki/formatter.py

    diff -ruN -x .svn trac-0.9b2/trac/wiki/formatter.py intertrac-branch/trac/wiki/formatter.py
    old new  
    1919from __future__ import generators 
    2020import re 
    2121import os 
    22 import string 
    2322import urllib 
    2423 
    2524try: 
    … …  
    2827    from StringIO import StringIO 
    2928 
    3029from trac import util 
     30from trac.core import * 
    3131from trac.mimeview import * 
    32 from trac.wiki.api import WikiSystem 
     32from trac.wiki.api import WikiSystem, IWikiChangeListener, IWikiMacroProvider 
    3333 
    34 __all__ = ['wiki_to_html', 'wiki_to_oneliner', 'wiki_to_outline'] 
     34__all__ = ['wiki_to_html', 'wiki_to_oneliner', 'wiki_to_outline', 
     35           'INTERTRAC_SCHEME' ] 
    3536 
    3637# 
    3738# Customization of the Wiki syntax  ***use with care*** 
    … …  
    4546SUPERSCRIPT_TOKEN = r"\^" 
    4647INLINE_TOKEN = "`" 
    4748 
    48 LINK_SCHEME = r"[\w.+-]+" # as per RFC 2396 
     49LINK_SCHEME = r"[\w.+-]+?" # as per RFC 2396 
     50INTERTRAC_SCHEME = r"[a-zA-Z.+-]+?" # no digits (support for shorthand links) 
    4951 
    5052def system_message(msg, text): 
    5153    return """<div class="system-message"> 
    … …  
    136138class Formatter(object): 
    137139    flavor = 'default' 
    138140 
    139     _link_resolvers = None 
    140141    # Rules provided by IWikiSyntaxProviders are inserted between pre_rules and post_rules 
    141142    _pre_rules = [r"(?P<bolditalic>%s)" % BOLDITALIC_TOKEN, 
    142143                  r"(?P<bold>%s)" % BOLD_TOKEN, 
    … …  
    165166                   r"(?P<last_table_cell>\|\|\s*$)", 
    166167                   r"(?P<table_cell>\|\|)"] 
    167168 
    168     _compiled_rules = None 
    169     _helper_patterns = None 
    170     _external_handlers = None 
    171169    _processor_re = re.compile('#\!([\w+-][\w+-/]*)') 
    172170    _anchor_re = re.compile('[^\w\d\.-:]+', re.UNICODE) 
    173171     
    … …  
    194192    db = property(fget=_get_db) 
    195193 
    196194    def _get_rules(self): 
    197         if not Formatter._compiled_rules: 
    198             helpers = [] 
    199             handlers = {} 
    200             syntax = Formatter._pre_rules[:] 
    201             wiki = WikiSystem(self.env) 
    202             i = 0 
    203             for resolver in wiki.syntax_providers: 
    204                 for regexp, handler in resolver.get_wiki_syntax(): 
    205                     handlers['i'+str(i)] = handler 
    206                     syntax.append('(?P<i%d>%s)' % (i, regexp)) 
    207                     i += 1 
    208             syntax += Formatter._post_rules[:] 
    209             helper_re = re.compile(r'\?P<([a-z\d]+)>') 
    210             for rule in syntax: 
    211                 helpers += helper_re.findall(rule)[1:] 
    212             rules = re.compile('(?:' + string.join(syntax, '|') + ')') 
    213             Formatter._external_handlers = handlers 
    214             Formatter._helper_patterns = helpers 
    215             Formatter._compiled_rules = rules 
    216         return Formatter._compiled_rules 
     195        return WikiSystem(self.env).rules 
    217196    rules = property(_get_rules) 
    218197 
    219198    def _get_link_resolvers(self): 
    220         if not self._link_resolvers: 
    221             resolvers = {} 
    222             wiki = WikiSystem(self.env) 
    223             for resolver in wiki.syntax_providers: 
    224                 for namespace, handler in resolver.get_link_resolvers(): 
    225                     resolvers[namespace] = handler 
    226             self._link_resolvers = resolvers 
    227         return self._link_resolvers 
     199        return WikiSystem(self.env).link_resolvers 
    228200    link_resolvers = property(_get_link_resolvers) 
    229201 
    230202    def replace(self, fullmatch): 
     203        wiki = WikiSystem(self.env)         
    231204        for itype, match in fullmatch.groupdict().items(): 
    232             if match and not itype in Formatter._helper_patterns: 
     205            if match and not itype in wiki.helper_patterns: 
    233206                # Check for preceding escape character '!' 
    234207                if match[0] == '!': 
    235208                    return match[1:] 
    236                 if itype in self._external_handlers: 
    237                     return self._external_handlers[itype](self, match, fullmatch) 
     209                if itype in wiki.external_handlers: 
     210                    return wiki.external_handlers[itype](self, match, fullmatch) 
    238211                else: 
    239212                    return getattr(self, '_' + itype + '_formatter')(match, fullmatch) 
    240213 
    … …  
    307280            return self._make_link(ns, target, match, label) 
    308281 
    309282    def _make_link(self, ns, target, match, label): 
     283        # check first for an alias defined in trac.ini 
     284        ns = self.env.config.get('intertrac', ns.upper(), ns) 
    310285        if ns in self.link_resolvers: 
    311             return self._link_resolvers[ns](self, ns, target, label) 
     286            return self.link_resolvers[ns](self, ns, target, label) 
    312287        elif target.startswith('//') or ns == "mailto": 
    313288            return self._make_ext_link(ns+':'+target, label) 
    314289        else: 
    315             return match 
     290            intertrac = self._make_intertrac_link(ns, target, label) 
     291            if intertrac: 
     292                return intertrac 
     293            else: 
     294                interwiki = self._make_interwiki_link(ns, target, label) 
     295                if interwiki: 
     296                    return interwiki 
     297                else: 
     298                    return match 
     299 
     300    def _make_intertrac_link(self, ns, target, label): 
     301        if self.env.siblings.has_key(ns): 
     302            sibling = self.env.siblings[ns] 
     303            if not hasattr(sibling, 'href'): 
     304                from trac.web.href import Href 
     305                def xchg_base(base): 
     306                    return '/'.join(base.split('/')[:-1] + [ns]) 
     307                sibling.href = Href(xchg_base(self.env.href.base)) 
     308                sibling.abs_href = Href(xchg_base(self.env.abs_href.base)) 
     309            ref = wiki_to_oneliner(target, sibling) 
     310            return ref.replace('>%s' % target, '>%s' % label) 
     311        url = self.env.config.get('intertrac', ns.upper()+'.url') 
     312        if url: 
     313            name = self.env.config.get('intertrac', ns.upper()+'.title', 
     314                                       'Trac project %s' % ns) 
     315            sep = target.find(':') 
     316            if sep != -1: 
     317                url = '%s/%s/%s' % (url, target[:sep], target[sep+1:]) 
     318            else:  
     319                url = '%s/search?q=%s' % (url, urllib.quote_plus(target)) 
     320            return self._make_ext_link(url, label, '%s in %s' % (target, name)) 
     321        else: 
     322            return None 
     323 
     324    def shorthand_intertrac_helper(self, ns, target, label, fullmatch): 
     325        if fullmatch: # short form 
     326            it_grp = fullmatch.group('it_%s' % ns) 
     327            if it_grp: 
     328                alias = it_grp.strip() 
     329                intertrac = self.env.config.get('intertrac', alias.upper(), 
     330                                                alias) 
     331                target = '%s:%s' % (ns, target[len(it_grp):]) 
     332                it = self._make_intertrac_link(intertrac, target, label) 
     333                return it or label 
     334        return None 
     335 
     336    def _make_interwiki_link(self, ns, target, label): 
     337        interwiki = InterWikiMap(self.env) 
     338        if interwiki.has_key(ns): 
     339            url, title = interwiki.url(ns, target) 
     340            return self._make_ext_link(url, label, '%s in %s' % (target, title)) 
     341        else: 
     342            return None 
    316343 
    317344    def _make_ext_link(self, url, text, title=''): 
    318345        title_attr = title and ' title="%s"' % title or '' 
    … …  
    342369        if match[0] == '!': 
    343370            return match[1:] 
    344371        else: 
    345             return self.simple_tag_handler('<span class="underline">', '</span>') 
     372            return self.simple_tag_handler('<span class="underline">', 
     373                                           '</span>') 
    346374 
    347375    def _strike_formatter(self, match, fullmatch): 
    348376        if match[0] == '!': 
    … …  
    730758    OutlineFormatter(env, absurls, db).format(wikitext, out, max_depth, 
    731759                                              min_depth) 
    732760    return out.getvalue() 
     761 
     762 
     763# -- InterWiki support 
     764 
     765class InterWikiMap(Component): 
     766 
     767    implements(IWikiChangeListener, IWikiMacroProvider) 
     768 
     769    _page_name = 'InterMapTxt' 
     770    _interwiki_re = re.compile(r"(\w+)[ \t]+([^ \t]+)(?:[ \t]+#(.*))?", 
     771                               re.UNICODE) 
     772    _argspec_re = re.compile(r"\$\d") 
     773 
     774    def __init__(self): 
     775        self._interwiki_map = None 
     776        # This dictionary maps upper-cased namespaces 
     777        # to (namespace, prefix, title) values 
     778 
     779    def has_key(self, ns): 
     780        if not self._interwiki_map: 
     781            self._update() 
     782        return self._interwiki_map.has_key(ns.upper()) 
     783 
     784    def url(self, ns, target): 
     785        ns, url, title = self._interwiki_map[ns.upper()] 
     786        args = target.split(':') 
     787        def setarg(match): 
     788            num = int(match.group()[1:]) 
     789            return 0 < num <= len(args) and args[num-1] or '' 
     790        url_with_args = re.sub(InterWikiMap._argspec_re, setarg, url) 
     791        if url_with_args == url:  
     792            return url + target, title 
     793        else: 
     794            return url_with_args, title 
     795 
     796    # IWikiChangeListener methods 
     797 
     798    def wiki_page_added(self, page): 
     799        if page.name == InterWikiMap._page_name: 
     800            self._update() 
     801 
     802    def wiki_page_changed(self, page, version, t, comment, author, ipnr): 
     803        if page.name == InterWikiMap._page_name: 
     804            self._update() 
     805 
     806    def wiki_page_deleted(self, page): 
     807        if page.name == InterWikiMap._page_name: 
     808            self._interwiki_map.clear() 
     809 
     810    def _update(self): 
     811        from trac.wiki.model import WikiPage 
     812        self._interwiki_map = {} 
     813        content = WikiPage(self.env, InterWikiMap._page_name).text 
     814        in_map = False 
     815        for line in content.split('\n'): 
     816            if in_map: 
     817                if line.startswith('----'): 
     818                    in_map = False 
     819                else: 
     820                    m = re.match(InterWikiMap._interwiki_re, line) 
     821                    if m: 
     822                        prefix, url, title = m.groups() 
     823                        url = url.strip() 
     824                        title = title and title.strip() or prefix 
     825                        self._interwiki_map[prefix.upper()] = (prefix, url, 
     826                                                               title) 
     827            elif line.startswith('----'): 
     828                in_map = True 
     829 
     830    # IWikiMacroProvider 
     831 
     832    def get_macros(self): 
     833        yield 'InterWiki' 
     834 
     835    def get_macro_description(self, name):  
     836        return "Provide a description list for the known InterWiki prefixes." 
     837 
     838    def render_macro(self, req, name, content): 
     839        if not self._interwiki_map: 
     840            self._update() 
     841        keys = self._interwiki_map.keys() 
     842        keys.sort() 
     843        buf = StringIO() 
     844        buf.write('<table><tr><th>Prefix</th><td>Site</td></tr>\n') 
     845        for k in keys: 
     846            prefix, url, title = self._interwiki_map[k] 
     847            shortened_url = url and url[:-1] 
     848            description = title == prefix and shortened_url or title 
     849            buf.write('<tr>\n' + 
     850                      ('<td><a href="%sRecentChanges">%s</a></td>' 
     851                       '<td><a href="%s">%s</a></td>\n') \ 
     852                      % (url, prefix, shortened_url, description) + 
     853                      '</tr>\n') 
     854        buf.write('</table>\n') 
     855        return buf.getvalue() 
  • trac/wiki/tests/formatter.py

    diff -ruN -x .svn trac-0.9b2/trac/wiki/tests/formatter.py intertrac-branch/trac/wiki/tests/formatter.py
    old new  
    5353                self.config = Configuration(None) 
    5454                self.href = Href('/') 
    5555                self.abs_href = Href('http://www.example.com/') 
    56                 self._wiki_pages = {} 
    5756                self.path = '' 
     57                # -- intertrac support 
     58                self.siblings = {} 
     59                self.config.set('intertrac', 'trac.title', "Trac's Trac") 
     60                self.config.set('intertrac', 'trac.url', 
     61                                "http://projects.edgewall.com/trac") 
     62                self.config.set('intertrac', 't', 'trac') 
    5863            def component_activated(self, component): 
    5964                component.env = self 
    6065                component.config = self.config 
  • trac/wiki/tests/wiki-tests.txt

    diff -ruN -x .svn trac-0.9b2/trac/wiki/tests/wiki-tests.txt intertrac-branch/trac/wiki/tests/wiki-tests.txt
    old new  
    3636<a class="ext-link" href="http://www.edgewall.com/"><span class="icon"></span>http://www.edgewall.com/</a> 
    3737</p> 
    3838============================== 
    39 #1, [1], r1, {1} 
     39#1, {1} 
     40[1], r1 
     41[1/README.txt] 
     42#12, [12], r12, {12} 
    4043------------------------------ 
    4144<p> 
    42 <a class="missing ticket" href="/ticket/1" rel="nofollow">#1</a>, <a class="missing changeset" href="/changeset/1" rel="nofollow">[1]</a>, <a class="missing changeset" href="/changeset/1" rel="nofollow">r1</a>, <a class="report" href="/report/1">{1}</a> 
     45<a class="missing ticket" href="/ticket/1" rel="nofollow">#1</a>, <a class="report" href="/report/1">{1}</a> 
     46<a class="missing changeset" href="/changeset/1" rel="nofollow">[1]</a>, <a class="missing changeset" href="/changeset/1" rel="nofollow">r1</a> 
     47<a class="missing changeset" href="/changeset/1/README.txt" rel="nofollow">[1/README.txt]</a> 
     48<a class="missing ticket" href="/ticket/12" rel="nofollow">#12</a>, <a class="missing changeset" href="/changeset/12" rel="nofollow">[12]</a>, <a class="missing changeset" href="/changeset/12" rel="nofollow">r12</a>, <a class="report" href="/report/12">{12}</a> 
    4349</p> 
    4450============================== 
    4551!#1, ![1], !r1, !{1} 
    … …  
    6066[1:2], r1:2, [12:23], r12:23 
    6167</p> 
    6268============================== 
    63 ticket:1, changeset:1, report:1, source:foo/bar 
     69ticket:1, report:1, source:foo/bar 
     70changeset:1, changeset:1/README.txt 
    6471 
    6572Issue [ticket:1], CS[changeset:1], Listing [report:1], File [source:foo/bar] 
    6673------------------------------ 
    6774<p> 
    68 <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> 
     75<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> 
     76<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> 
    6977</p> 
    7078<p> 
    7179Issue <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> 
    … …  
    7987<a class="source" href="/browser/foo/bar">source foo/bar</a>, <a class="ext-link" href="http://www.edgewall.com/"><span class="icon"></span>edgewall</a> 
    8088</p> 
    8189============================== 
     90diff:trunk//branch 
     91diff:trunk@12//branch@23 
     92diff:trunk@12:23 
     93diff:@12:23 
     94------------------------------ 
     95<p> 
     96<a class="changeset" title="Diff from trunk@latest to branch@latest" href="/diff/branch?old_path=trunk">diff:trunk//branch</a> 
     97<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> 
     98<a class="changeset" title="Diff r12:23 for trunk" href="/diff/trunk?new=23&old=12&old_path=trunk">diff:trunk@12:23</a> 
     99<a class="changeset" title="Diff r12:23 for /" href="/diff/?new=23&old=12&old_path=">diff:@12:23</a> 
     100</p> 
     101============================== 
    82102CamelCase AlabamA ABc AlaBamA FooBar 
    83103------------------------------ 
    84104<p> 
    85105<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a> AlabamA ABc AlaBamA <a class="missing wiki" href="/wiki/FooBar" rel="nofollow">FooBar?</a> 
    86106</p> 
    87107============================== 
    88 CamelCase,CamelCase.CamelCase:CamelCase 
     108CamelCase,CamelCase.CamelCase: CamelCase 
    89109------------------------------ 
    90110<p> 
    91 <a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>,<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>.CamelCase:CamelCase 
     111<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>,<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>.<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>: <a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a> 
    92112</p> 
    93113============================== 
    94114!CamelCase 
    … …  
    605625<tr><td> a  
    606626</td></tr><tr><td> b  
    607627</td></tr></table> 
     628============================== 
     629t:wiki:InterTrac 
     630trac:wiki:InterTrac 
     631[t:wiki:InterTrac intertrac] 
     632[trac:wiki:InterTrac intertrac] 
     633------------------------------ 
     634<p> 
    Â