2 rcs_id('$Id: diff.php,v 1.16 2001-12-11 05:51:10 carstenklapp Exp $');
5 // A PHP diff engine for phpwiki.
7 // Copyright (C) 2000 Geoffrey T. Dairiki <dairiki@dairiki.org>
8 // You may copy this code freely under the conditions of the GPL.
11 // FIXME: possibly remove assert()'s for production version?
13 // PHP3 does not have assert()
14 define('USE_ASSERTS', function_exists('assert'));
18 * Class used internally by WikiDiff to actually compute the diffs.
20 * The algorithm used here is mostly lifted from the perl module
21 * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
22 * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
24 * More ideas are taken from:
25 * http://www.ics.uci.edu/~eppstein/161/960229.html
27 * Some ideas are (and a bit of code) are from from analyze.c, from GNU
28 * diffutils-2.7, which can be found at:
29 * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
31 * Finally, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
36 var $edits; // List of editing operation to convert XV to YV.
37 var $xv = array(), $yv = array();
39 function _WikiDiffEngine ($from_lines, $to_lines)
41 $n_from = sizeof($from_lines);
42 $n_to = sizeof($to_lines);
45 // Ignore trailing and leading matching lines.
46 while ($n_from > 0 && $n_to > 0)
48 if ($from_lines[$n_from - 1] != $to_lines[$n_to - 1])
54 for ( $skip = 0; $skip < min($n_from, $n_to); $skip++)
55 if ($from_lines[$skip] != $to_lines[$skip])
62 $this->xchanged = array();
63 $this->ychanged = array();
65 // Ignore lines which do not exist in both files.
66 for ($x = 0; $x < $n_from; $x++)
67 $xhash[$from_lines[$x + $skip]] = 1;
68 for ($y = 0; $y < $n_to; $y++)
70 $line = $to_lines[$y + $skip];
72 if ( ($this->ychanged[$y] = empty($xhash[$line])) )
78 for ($x = 0; $x < $n_from; $x++)
80 $line = $from_lines[$x + $skip];
82 if ( ($this->xchanged[$x] = empty($yhash[$line])) )
89 $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
91 // Merge edits when possible
92 $this->_shift_boundaries($xlines, $this->xchanged, $this->ychanged);
93 $this->_shift_boundaries($ylines, $this->ychanged, $this->xchanged);
95 // Compute the edit operations.
96 $this->edits = array();
98 $this->edits[] = $skip;
102 while ($x < $n_from || $y < $n_to)
104 USE_ASSERTS && assert($y < $n_to || $this->xchanged[$x]);
105 USE_ASSERTS && assert($x < $n_from || $this->ychanged[$y]);
107 // Skip matching "snake".
111 while ( $x < $n_from && $y < $n_to
112 && !$this->xchanged[$x] && !$this->ychanged[$y])
119 $this->edits[] = $x - $x0;
124 while ($x < $n_from && $this->xchanged[$x])
130 $this->edits[] = -($x - $x0);
133 if (!empty($this->ychanged[$y]))
136 while ($y < $n_to && $this->ychanged[$y])
137 $adds[] = "" . $ylines[$y++];
138 $this->edits[] = $adds;
142 $this->edits[] = $endskip;
145 /* Divide the Largest Common Subsequence (LCS) of the sequences
146 * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
149 * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
150 * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
151 * sub sequences. The first sub-sequence is contained in [X0, X1),
152 * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
153 * that (X0, Y0) == (XOFF, YOFF) and
154 * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
156 * This function assumes that the first lines of the specified portions
157 * of the two files do not match, and likewise that the last lines do not
158 * match. The caller must trim matching lines from the beginning and end
159 * of the portions it is going to specify.
161 function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks)
165 if ($xlim - $xoff > $ylim - $yoff)
167 // Things seems faster (I'm not sure I understand why)
168 // when the shortest sequence in X.
170 list ($xoff, $xlim, $yoff, $ylim)
171 = array( $yoff, $ylim, $xoff, $xlim);
175 for ($i = $ylim - 1; $i >= $yoff; $i--)
176 $ymatches[$this->xv[$i]][] = $i;
178 for ($i = $ylim - 1; $i >= $yoff; $i--)
179 $ymatches[$this->yv[$i]][] = $i;
182 $this->seq[0]= $yoff - 1;
183 $this->in_seq = array();
186 $numer = $xlim - $xoff + $nchunks - 1;
188 for ($chunk = 0; $chunk < $nchunks; $chunk++) {
190 for ($i = 0; $i <= $this->lcs; $i++)
191 $ymids[$i][$chunk-1] = $this->seq[$i];
193 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
194 for ( ; $x < $x1; $x++) {
195 $line = $flip ? $this->yv[$x] : $this->xv[$x];
196 if (empty($ymatches[$line]))
198 $matches = $ymatches[$line];
200 while (list ($junk, $y) = each($matches))
201 if (empty($this->in_seq[$y]))
203 $k = $this->_lcs_pos($y);
204 USE_ASSERTS && assert($k > 0);
205 $ymids[$k] = $ymids[$k-1];
208 while (list ($junk, $y) = each($matches))
210 if ($y > $this->seq[$k-1])
212 USE_ASSERTS && assert($y < $this->seq[$k]);
213 // Optimization: this is a common case:
214 // next match is just replacing previous match.
215 $this->in_seq[$this->seq[$k]] = false;
217 $this->in_seq[$y] = 1;
219 else if (empty($this->in_seq[$y]))
221 $k = $this->_lcs_pos($y);
222 USE_ASSERTS && assert($k > 0);
223 $ymids[$k] = $ymids[$k-1];
229 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
230 $ymid = $ymids[$this->lcs];
231 for ($n = 0; $n < $nchunks - 1; $n++)
233 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
235 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
237 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
239 return array($this->lcs, $seps);
242 function _lcs_pos ($ypos)
245 if ($end == 0 || $ypos > $this->seq[$end])
247 $this->seq[++$this->lcs] = $ypos;
248 $this->in_seq[$ypos] = 1;
255 $mid = (int)(($beg + $end) / 2);
256 if ( $ypos > $this->seq[$mid] )
262 USE_ASSERTS && assert($ypos != $this->seq[$end]);
264 $this->in_seq[$this->seq[$end]] = false;
265 $this->seq[$end] = $ypos;
266 $this->in_seq[$ypos] = 1;
270 /* Find LCS of two sequences.
272 * The results are recorded in the vectors $this->{x,y}changed[], by
273 * storing a 1 in the element for each line that is an insertion
274 * or deletion (ie. is not in the LCS).
276 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
278 * Note that XLIM, YLIM are exclusive bounds.
279 * All line numbers are origin-0 and discarded lines are not counted.
281 function _compareseq ($xoff, $xlim, $yoff, $ylim)
283 // Slide down the bottom initial diagonal.
284 while ($xoff < $xlim && $yoff < $ylim
285 && $this->xv[$xoff] == $this->yv[$yoff])
291 // Slide up the top initial diagonal.
292 while ($xlim > $xoff && $ylim > $yoff
293 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1])
299 if ($xoff == $xlim || $yoff == $ylim)
303 // This is ad hoc but seems to work well.
304 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
305 //$nchunks = max(2,min(8,(int)$nchunks));
306 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
308 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
313 // X and Y sequences have no common subsequence:
315 while ($yoff < $ylim)
316 $this->ychanged[$this->yind[$yoff++]] = 1;
317 while ($xoff < $xlim)
318 $this->xchanged[$this->xind[$xoff++]] = 1;
322 // Use the partitions to split this problem into subproblems.
325 while ($pt2 = next($seps))
327 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
333 /* Adjust inserts/deletes of identical lines to join changes
334 * as much as possible.
336 * We do something when a run of changed lines include a
337 * line at one end and has an excluded, identical line at the other.
338 * We are free to choose which identical line is included.
339 * `compareseq' usually chooses the one at the beginning,
340 * but usually it is cleaner to consider the following identical line
341 * to be the "change".
343 * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
345 function _shift_boundaries ($lines, &$changed, $other_changed)
350 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
351 $len = sizeof($lines);
352 $other_len = sizeof($other_changed);
357 * Scan forwards to find beginning of another run of changes.
358 * Also keep track of the corresponding point in the other file.
360 * Throughout this code, $i and $j are adjusted together so that
361 * the first $i elements of $changed and the first $j elements
362 * of $other_changed both contain the same number of zeros
364 * Furthermore, $j is always kept so that $j == $other_len or
365 * $other_changed[$j] == false.
367 while ($j < $other_len && $other_changed[$j])
370 while ($i < $len && ! $changed[$i])
372 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
374 while ($j < $other_len && $other_changed[$j])
383 // Find the end of this run of changes.
384 while (++$i < $len && $changed[$i])
390 * Record the length of this run of changes, so that
391 * we can later determine whether the run has grown.
393 $runlength = $i - $start;
396 * Move the changed region back, so long as the
397 * previous unchanged line matches the last changed one.
398 * This merges with previous changed regions.
400 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1])
402 $changed[--$start] = 1;
403 $changed[--$i] = false;
404 while ($start > 0 && $changed[$start - 1])
406 USE_ASSERTS && assert('$j > 0');
407 while ($other_changed[--$j])
409 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
413 * Set CORRESPONDING to the end of the changed run, at the last
414 * point where it corresponds to a changed run in the other file.
415 * CORRESPONDING == LEN means no such point has been found.
417 $corresponding = $j < $other_len ? $i : $len;
420 * Move the changed region forward, so long as the
421 * first changed line matches the following unchanged one.
422 * This merges with following changed regions.
423 * Do this second, so that if there are no merges,
424 * the changed region is moved forward as far as possible.
426 while ($i < $len && $lines[$start] == $lines[$i])
428 $changed[$start++] = false;
430 while ($i < $len && $changed[$i])
433 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
435 if ($j < $other_len && $other_changed[$j])
438 while ($j < $other_len && $other_changed[$j])
443 while ($runlength != $i - $start);
446 * If possible, move the fully-merged run of changes
447 * back to a corresponding run in the other file.
449 while ($corresponding < $i)
451 $changed[--$start] = 1;
453 USE_ASSERTS && assert('$j > 0');
454 while ($other_changed[--$j])
456 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
463 * Class representing a diff between two files.
470 * Compute diff between files (or deserialize serialized WikiDiff.)
472 function WikiDiff($from_lines = false, $to_lines = false)
474 if ($from_lines && $to_lines)
476 $compute = new _WikiDiffEngine($from_lines, $to_lines);
477 $this->edits = $compute->edits;
479 else if ($from_lines)
481 // $from_lines is not really from_lines, but rather
482 // a serialized WikiDiff.
483 $this->edits = unserialize($from_lines);
487 $this->edits = array();
492 * Compute reversed WikiDiff.
496 * $diff = new WikiDiff($lines1, $lines2);
497 * $rev = $diff->reverse($lines1);
499 * // reconstruct $lines1 from $lines2:
500 * $out = $rev->apply($lines2);
502 function reverse ($from_lines)
507 for ( reset($this->edits);
508 $edit = current($this->edits);
512 { // Was an add, turn it into a delete.
513 $nadd = sizeof($edit);
514 USE_ASSERTS && assert ($nadd > 0);
519 // Was a copy --- just pass it through. }
523 { // Was a delete, turn it into an add.
526 while ($ndelete-- > 0)
527 $edit[] = "" . $from_lines[$x++];
530 trigger_error("assertion error", E_USER_ERROR);
532 $rev->edits[] = $edit;
539 * Compose (concatenate) WikiDiffs.
543 * $diff1 = new WikiDiff($lines1, $lines2);
544 * $diff2 = new WikiDiff($lines2, $lines3);
545 * $comp = $diff1->compose($diff2);
547 * // reconstruct $lines3 from $lines1:
548 * $out = $comp->apply($lines1);
550 function compose ($that)
555 $comp = new WikiDiff;
556 $left = current($this->edits);
557 $right = current($that->edits);
559 while ($left || $right)
561 if (!is_array($left) && $left < 0)
562 { // Left op is a delete.
564 $left = next($this->edits);
566 else if (is_array($right))
567 { // Right op is an add.
569 $right = next($that->edits);
571 else if (!$left || !$right)
572 trigger_error("assertion error", E_USER_ERROR);
573 else if (!is_array($left) && $left > 0)
574 { // Left op is a copy.
575 if ($left <= abs($right))
577 $newop = $right > 0 ? $left : -$left;
580 $right = next($that->edits);
581 $left = next($this->edits);
586 $left -= abs($right);
587 $right = next($that->edits);
591 { // Left op is an add.
592 assert('is_array($left)');
593 $nleft = sizeof($left);
594 if ($nleft <= abs($right))
597 { // Right op is copy
601 else // Right op is delete
607 $right = next($that->edits);
608 $left = next($this->edits);
614 for ($i = 0; $i < $right; $i++)
615 $newop[] = $left[$i];
618 for ($i = abs($right); $i < $nleft; $i++)
621 $right = next($that->edits);
632 if (is_array($op) && is_array($newop))
634 // Both $op and $newop are adds.
635 for ($i = 0; $i < sizeof($newop); $i++)
638 else if (($op > 0 && $newop > 0) || ($op < 0 && $newop < 0))
639 { // $op and $newop are both either deletes or copies.
644 $comp->edits[] = $op;
649 $comp->edits[] = $op;
658 for (reset($this->edits);
659 $edit = current($this->edits);
666 echo "Delete " . -$edit;
667 else if (is_array($edit))
669 echo "Add " . sizeof($edit) . "<ul>";
670 for ($i = 0; $i < sizeof($edit); $i++)
671 echo "<li>" . htmlspecialchars($edit[$i]);
675 trigger_error("assertion error", E_USER_ERROR);
682 * Apply a WikiDiff to a set of lines.
686 * $diff = new WikiDiff($lines1, $lines2);
688 * // reconstruct $lines2 from $lines1:
689 * $out = $diff->apply($lines1);
691 function apply ($from_lines)
694 $xlim = sizeof($from_lines);
696 for ( reset($this->edits);
697 $edit = current($this->edits);
703 while (list ($junk, $line) = each($edit))
708 $output[] = $from_lines[$x++];
713 ExitWiki(sprintf(gettext ("WikiDiff::apply: line count mismatch: %s != %s"), $x, $xlim));
718 * Serialize a WikiDiff.
722 * $diff = new WikiDiff($lines1, $lines2);
723 * $string = $diff->serialize;
725 * // recover WikiDiff from serialized version:
726 * $diff2 = new WikiDiff($string);
728 function serialize ()
730 return serialize($this->edits);
734 * Return true if two files were equal.
738 if (sizeof($this->edits) > 1)
740 if (sizeof($this->edits) == 0)
742 // Test for: only edit is a copy.
743 return !is_array($this->edits[0]) && $this->edits[0] > 0;
747 * Compute the length of the Longest Common Subsequence (LCS).
749 * This is mostly for diagnostic purposed.
754 for (reset($this->edits);
755 $edit = current($this->edits);
758 if (!is_array($edit) && $edit > 0)
765 * Check a WikiDiff for validity.
767 * This is here only for debugging purposes.
769 function _check ($from_lines, $to_lines)
771 $test = $this->apply($from_lines);
772 if (serialize($test) != serialize($to_lines))
773 ExitWiki(gettext ("WikiDiff::_check: failed"));
776 $prev = current($this->edits);
777 $prevtype = is_array($prev) ? 'a' : ($prev > 0 ? 'c' : 'd');
779 while ($edit = next($this->edits))
781 $type = is_array($edit) ? 'a' : ($edit > 0 ? 'c' : 'd');
782 if ( $prevtype == $type )
783 ExitWiki(gettext ("WikiDiff::_check: edit sequence is non-optimal"));
787 printf ("<strong>" . gettext ("WikiDiff Okay: LCS = %s") . "</strong>\n", $lcs);
793 * A class to format a WikiDiff as HTML.
797 * $diff = new WikiDiff($lines1, $lines2); // compute diff.
799 * $fmt = new WikiDiffFormatter;
800 * echo $fmt->format($diff, $lines1); // Output HTMLified standard diff.
802 * or to output reverse diff (diff's that would take $lines2 to $lines1):
804 * $fmt = new WikiDiffFormatter('reversed');
805 * echo $fmt->format($diff, $lines1);
807 class WikiDiffFormatter
810 var $do_reverse_diff;
811 var $context_prefix, $deletes_prefix, $adds_prefix;
813 function WikiDiffFormatter ($reverse = false)
815 $this->do_reverse_diff = $reverse;
816 $this->context_lines = 0;
817 $this->context_prefix = ' ';
818 $this->deletes_prefix = '< ';
819 $this->adds_prefix = '> ';
822 function format ($diff, $from_lines)
824 return Element('table',
825 array('width' => '100%',
826 'bgcolor' => 'black',
830 $this->_format($diff->edits, $from_lines));
833 function _format ($edits, $from_lines)
837 $xlim = sizeof($from_lines);
840 while ($edit = current($edits))
842 if (!is_array($edit) && $edit >= 0)
843 { // Edit op is a copy.
851 // Start of an output hunk.
852 $xoff = max(0, $x - $this->context_lines);
853 $yoff = $xoff + $y - $x;
856 // Get leading context.
858 for ($i = $xoff; $i < $x; $i++)
859 $context[] = $from_lines[$i];
860 $hunk['c'] = $context;
864 { // Edit op is an add.
866 $hunk[$this->do_reverse_diff ? 'd' : 'a'] = $edit;
869 { // Edit op is a delete
872 $deletes[] = $from_lines[$x++];
873 $hunk[$this->do_reverse_diff ? 'a' : 'd'] = $deletes;
877 $next = next($edits);
880 if ( !$next || $ncopy > 2 * $this->context_lines)
882 // End of an output hunk.
886 $xend = min($x + $this->context_lines, $xlim);
889 // Get trailing context.
891 for ($i = $x; $i < $xend; $i++)
892 $context[] = $from_lines[$i];
893 $hunks[] = array('c' => $context);
896 $xlen = $xend - $xoff;
897 $ylen = $xend + $y - $x - $yoff;
898 $xbeg = $xlen ? $xoff + 1 : $xoff;
899 $ybeg = $ylen ? $yoff + 1 : $yoff;
901 if ($this->do_reverse_diff)
902 list ($xbeg, $xlen, $ybeg, $ylen)
903 = array($ybeg, $ylen, $xbeg, $xlen);
905 $rows .= $this->_emit_diff($xbeg,$xlen,$ybeg,$ylen, $hunks);
914 for ($i = $x; $i < $x + $ncopy; $i++)
915 $context[] = $from_lines[$i];
916 $hunk = array('c' => $context);
926 function _emit_lines($lines, $prefix, $color)
929 $prefix = Element('td', array('class' => 'diff-notation', 'width' => "1%"), $prefix);
931 while (list ($junk, $line) = each($lines))
933 $line = rtrim($line);
934 $line = empty($line) ? ' ' : htmlspecialchars($line);
935 $html .= Element('tr', array('valign' => 'top'),
936 $prefix . Element('td', array('class' => $color),
937 Element('tt', $line)));
942 function _emit_diff ($xbeg,$xlen,$ybeg,$ylen,$hunks)
944 $header = Element('tr', array('bgcolor' => '#cccccc'),
945 Element('td', array('colspan' => 2),
947 $this->_diff_header($xbeg, $xlen, $ybeg, $ylen))));
949 $prefix = array('c' => $this->context_prefix,
950 'a' => $this->adds_prefix,
951 'd' => $this->deletes_prefix);
952 $color = array('c' => 'diff-unchanged',
954 'd' => 'diff-deleted');
957 for (reset($hunks); $hunk = current($hunks); next($hunks))
959 if (!empty($hunk['c']))
960 $diff .= $this->_emit_lines($hunk['c'],
961 $this->context_prefix, 'diff-unchanged');
962 if (!empty($hunk['d']))
963 $diff .= $this->_emit_lines($hunk['d'],
964 $this->deletes_prefix, 'diff-deleted');
965 if (!empty($hunk['a']))
966 $diff .= $this->_emit_lines($hunk['a'],
967 $this->adds_prefix, 'diff-added');
971 return Element('tr', Element('td',
973 array('width' => '100%',
974 'bgcolor' => 'white',
982 function _diff_header ($xbeg,$xlen,$ybeg,$ylen)
984 $what = $xlen ? ($ylen ? 'c' : 'd') : 'a';
985 $xlen = $xlen > 1 ? "," . ($xbeg + $xlen - 1) : '';
986 $ylen = $ylen > 1 ? "," . ($ybeg + $ylen - 1) : '';
988 return "$xbeg$xlen$what$ybeg$ylen";
993 * A class to format a WikiDiff as a pretty HTML unified diff.
997 * $diff = new WikiDiff($lines1, $lines2); // compute diff.
999 * $fmt = new WikiUnifiedDiffFormatter;
1000 * echo $fmt->format($diff, $lines1); // Output HTMLified unified diff.
1002 class WikiUnifiedDiffFormatter extends WikiDiffFormatter
1004 function WikiUnifiedDiffFormatter ($reverse = false, $context_lines = 3)
1006 $this->do_reverse_diff = $reverse;
1007 $this->context_lines = $context_lines;
1008 $this->context_prefix = ' ';
1009 $this->deletes_prefix = '-';
1010 $this->adds_prefix = '+';
1013 function _diff_header ($xbeg,$xlen,$ybeg,$ylen)
1015 $xlen = $xlen == 1 ? '' : ",$xlen";
1016 $ylen = $ylen == 1 ? '' : ",$ylen";
1018 return "@@ -$xbeg$xlen +$ybeg$ylen @@";
1024 /////////////////////////////////////////////////////////////////
1026 function PageInfoRow ($pagename, $label, $rev)
1028 global $datetimeformat;
1030 $cols = QElement('td', array('align' => 'right'), $label);
1033 $url = WikiURL($pagename, array('version' => $rev->getVersion()));
1034 $linked_version = QElement('a', array('href' => $url), $rev->getVersion());
1035 $cols .= Element('td',
1036 gettext("version") . " " . $linked_version);
1038 $cols .= QElement('td',
1039 sprintf(gettext ("last modified on %s"),
1040 strftime($datetimeformat, $rev->get('mtime'))));
1041 $cols .= QElement('td',
1042 sprintf(gettext ("by %s"), $rev->get('author')));
1044 $cols .= QElement('td', array('colspan' => '3'),
1047 return Element('tr', $cols);
1050 function showDiff ($dbi, $request) {
1051 $pagename = $request->getArg('pagename');
1052 $version = $request->getArg('version');
1053 $previous = $request->getArg('previous');
1055 $page = $dbi->getPage($pagename);
1058 if (!($new = $page->getRevision($version)))
1059 NoSuchRevision($page, $version);
1060 $new_version = sprintf(gettext("version %d"), $version);
1063 $new = $page->getCurrentRevision();
1064 $new_version = gettext('current version');
1067 if (preg_match('/^\d+$/', $previous)) {
1068 if ( !($old = $page->getRevision($previous)) )
1069 NoSuchRevision($page, $previous);
1070 $old_version = sprintf(gettext("version %d"), $previous);
1071 $others = array('major', 'minor', 'author');
1074 switch ($previous) {
1077 while ($old = $page->getRevisionBefore($old)) {
1078 if (! $old->get('is_minor_edit'))
1081 $old_version = gettext("previous major revision");
1082 $others = array('minor', 'author');
1086 while ($old = $page->getRevisionBefore($old)) {
1087 if ($old->get('author') != $new->get('author'))
1090 $old_version = gettext("revision by previous author");
1091 $others = array('major', 'minor');
1096 $old = $page->getRevisionBefore($new);
1097 $old_version = gettext("previous revision");
1098 $others = array('major', 'author');
1103 $new_url = WikiURL($pagename, array('version' => $new->getVersion()));
1104 $new_link = QElement('a', array('href' => $new_url), $new_version);
1105 $old_url = WikiURL($pagename, array('version' => $old ? $old->getVersion() : 0));
1106 $old_link = QElement('a', array('href' => $old_url), $old_version);
1107 $page_link = LinkExistingWikiWord($pagename);
1109 $html = Element('p',
1110 sprintf(htmlspecialchars(gettext("Differences between %s and %s of %s.")),
1111 $new_link, $old_link, $page_link));
1114 $label = array('major' => gettext("Previous Major Revision"),
1115 'minor' => gettext("Previous Revision"),
1116 'author'=> gettext("Previous Author"));
1117 foreach ($others as $other) {
1118 $args = array('action' => 'diff', 'previous' => $other);
1120 $args['version'] = $version;
1121 $otherdiffs .= ' ' . QElement('a', array('href' => WikiURL($pagename, $args),
1122 'class' => 'wikiaction'),
1125 $html .= Element('p',
1126 htmlspecialchars(gettext("Other diffs:"))
1130 if ($old and $old->getVersion() == 0)
1133 $html .= Element('table',
1134 PageInfoRow($pagename, gettext ("Newer page:"), $new)
1135 . PageInfoRow($pagename, gettext ("Older page:"), $old));
1139 $diff = new WikiDiff($old->getContent(), $new->getContent());
1140 if ($diff->isEmpty()) {
1141 $html .= Element('hr');
1142 $html .= QElement('p', '[' . gettext ("Versions are identical") . ']');
1145 //$fmt = new WikiDiffFormatter;
1146 $fmt = new WikiUnifiedDiffFormatter;
1147 $html .= $fmt->format($diff, $old->getContent());
1151 include_once('lib/Template.php');
1152 echo GeneratePage('MESSAGE', $html,
1153 sprintf(gettext ("Diff: %s"), $pagename));
1159 // c-basic-offset: 4
1160 // c-hanging-comment-ender-p: nil
1161 // indent-tabs-mode: nil