2 rcs_id('$Id: diff.php,v 1.13 2001-09-18 19:16:23 dairiki 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++)
191 for ($i = 0; $i <= $this->lcs; $i++)
192 $ymids[$i][$chunk-1] = $this->seq[$i];
194 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
195 for ( ; $x < $x1; $x++)
197 $matches = $ymatches[$flip ? $this->yv[$x] : $this->xv[$x]];
201 while (list ($junk, $y) = each($matches))
202 if (empty($this->in_seq[$y]))
204 $k = $this->_lcs_pos($y);
205 USE_ASSERTS && assert($k > 0);
206 $ymids[$k] = $ymids[$k-1];
209 while (list ($junk, $y) = each($matches))
211 if ($y > $this->seq[$k-1])
213 USE_ASSERTS && assert($y < $this->seq[$k]);
214 // Optimization: this is a common case:
215 // next match is just replacing previous match.
216 $this->in_seq[$this->seq[$k]] = false;
218 $this->in_seq[$y] = 1;
220 else if (empty($this->in_seq[$y]))
222 $k = $this->_lcs_pos($y);
223 USE_ASSERTS && assert($k > 0);
224 $ymids[$k] = $ymids[$k-1];
230 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
231 $ymid = $ymids[$this->lcs];
232 for ($n = 0; $n < $nchunks - 1; $n++)
234 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
236 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
238 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
240 return array($this->lcs, $seps);
243 function _lcs_pos ($ypos)
246 if ($end == 0 || $ypos > $this->seq[$end])
248 $this->seq[++$this->lcs] = $ypos;
249 $this->in_seq[$ypos] = 1;
256 $mid = (int)(($beg + $end) / 2);
257 if ( $ypos > $this->seq[$mid] )
263 USE_ASSERTS && assert($ypos != $this->seq[$end]);
265 $this->in_seq[$this->seq[$end]] = false;
266 $this->seq[$end] = $ypos;
267 $this->in_seq[$ypos] = 1;
271 /* Find LCS of two sequences.
273 * The results are recorded in the vectors $this->{x,y}changed[], by
274 * storing a 1 in the element for each line that is an insertion
275 * or deletion (ie. is not in the LCS).
277 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
279 * Note that XLIM, YLIM are exclusive bounds.
280 * All line numbers are origin-0 and discarded lines are not counted.
282 function _compareseq ($xoff, $xlim, $yoff, $ylim)
284 // Slide down the bottom initial diagonal.
285 while ($xoff < $xlim && $yoff < $ylim
286 && $this->xv[$xoff] == $this->yv[$yoff])
292 // Slide up the top initial diagonal.
293 while ($xlim > $xoff && $ylim > $yoff
294 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1])
300 if ($xoff == $xlim || $yoff == $ylim)
304 // This is ad hoc but seems to work well.
305 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
306 //$nchunks = max(2,min(8,(int)$nchunks));
307 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
309 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
314 // X and Y sequences have no common subsequence:
316 while ($yoff < $ylim)
317 $this->ychanged[$this->yind[$yoff++]] = 1;
318 while ($xoff < $xlim)
319 $this->xchanged[$this->xind[$xoff++]] = 1;
323 // Use the partitions to split this problem into subproblems.
326 while ($pt2 = next($seps))
328 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
334 /* Adjust inserts/deletes of identical lines to join changes
335 * as much as possible.
337 * We do something when a run of changed lines include a
338 * line at one end and has an excluded, identical line at the other.
339 * We are free to choose which identical line is included.
340 * `compareseq' usually chooses the one at the beginning,
341 * but usually it is cleaner to consider the following identical line
342 * to be the "change".
344 * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
346 function _shift_boundaries ($lines, &$changed, $other_changed)
351 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
352 $len = sizeof($lines);
353 $other_len = sizeof($other_changed);
358 * Scan forwards to find beginning of another run of changes.
359 * Also keep track of the corresponding point in the other file.
361 * Throughout this code, $i and $j are adjusted together so that
362 * the first $i elements of $changed and the first $j elements
363 * of $other_changed both contain the same number of zeros
365 * Furthermore, $j is always kept so that $j == $other_len or
366 * $other_changed[$j] == false.
368 while ($j < $other_len && $other_changed[$j])
371 while ($i < $len && ! $changed[$i])
373 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
375 while ($j < $other_len && $other_changed[$j])
384 // Find the end of this run of changes.
385 while (++$i < $len && $changed[$i])
391 * Record the length of this run of changes, so that
392 * we can later determine whether the run has grown.
394 $runlength = $i - $start;
397 * Move the changed region back, so long as the
398 * previous unchanged line matches the last changed one.
399 * This merges with previous changed regions.
401 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1])
403 $changed[--$start] = 1;
404 $changed[--$i] = false;
405 while ($start > 0 && $changed[$start - 1])
407 USE_ASSERTS && assert('$j > 0');
408 while ($other_changed[--$j])
410 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
414 * Set CORRESPONDING to the end of the changed run, at the last
415 * point where it corresponds to a changed run in the other file.
416 * CORRESPONDING == LEN means no such point has been found.
418 $corresponding = $j < $other_len ? $i : $len;
421 * Move the changed region forward, so long as the
422 * first changed line matches the following unchanged one.
423 * This merges with following changed regions.
424 * Do this second, so that if there are no merges,
425 * the changed region is moved forward as far as possible.
427 while ($i < $len && $lines[$start] == $lines[$i])
429 $changed[$start++] = false;
431 while ($i < $len && $changed[$i])
434 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
436 if ($j < $other_len && $other_changed[$j])
439 while ($j < $other_len && $other_changed[$j])
444 while ($runlength != $i - $start);
447 * If possible, move the fully-merged run of changes
448 * back to a corresponding run in the other file.
450 while ($corresponding < $i)
452 $changed[--$start] = 1;
454 USE_ASSERTS && assert('$j > 0');
455 while ($other_changed[--$j])
457 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
464 * Class representing a diff between two files.
471 * Compute diff between files (or deserialize serialized WikiDiff.)
473 function WikiDiff($from_lines = false, $to_lines = false)
475 if ($from_lines && $to_lines)
477 $compute = new _WikiDiffEngine($from_lines, $to_lines);
478 $this->edits = $compute->edits;
480 else if ($from_lines)
482 // $from_lines is not really from_lines, but rather
483 // a serialized WikiDiff.
484 $this->edits = unserialize($from_lines);
488 $this->edits = array();
493 * Compute reversed WikiDiff.
497 * $diff = new WikiDiff($lines1, $lines2);
498 * $rev = $diff->reverse($lines1);
500 * // reconstruct $lines1 from $lines2:
501 * $out = $rev->apply($lines2);
503 function reverse ($from_lines)
508 for ( reset($this->edits);
509 $edit = current($this->edits);
513 { // Was an add, turn it into a delete.
514 $nadd = sizeof($edit);
515 USE_ASSERTS && assert ($nadd > 0);
520 // Was a copy --- just pass it through. }
524 { // Was a delete, turn it into an add.
527 while ($ndelete-- > 0)
528 $edit[] = "" . $from_lines[$x++];
531 trigger_error("assertion error", E_USER_ERROR);
533 $rev->edits[] = $edit;
540 * Compose (concatenate) WikiDiffs.
544 * $diff1 = new WikiDiff($lines1, $lines2);
545 * $diff2 = new WikiDiff($lines2, $lines3);
546 * $comp = $diff1->compose($diff2);
548 * // reconstruct $lines3 from $lines1:
549 * $out = $comp->apply($lines1);
551 function compose ($that)
556 $comp = new WikiDiff;
557 $left = current($this->edits);
558 $right = current($that->edits);
560 while ($left || $right)
562 if (!is_array($left) && $left < 0)
563 { // Left op is a delete.
565 $left = next($this->edits);
567 else if (is_array($right))
568 { // Right op is an add.
570 $right = next($that->edits);
572 else if (!$left || !$right)
573 trigger_error("assertion error", E_USER_ERROR);
574 else if (!is_array($left) && $left > 0)
575 { // Left op is a copy.
576 if ($left <= abs($right))
578 $newop = $right > 0 ? $left : -$left;
581 $right = next($that->edits);
582 $left = next($this->edits);
587 $left -= abs($right);
588 $right = next($that->edits);
592 { // Left op is an add.
593 assert('is_array($left)');
594 $nleft = sizeof($left);
595 if ($nleft <= abs($right))
598 { // Right op is copy
602 else // Right op is delete
608 $right = next($that->edits);
609 $left = next($this->edits);
615 for ($i = 0; $i < $right; $i++)
616 $newop[] = $left[$i];
619 for ($i = abs($right); $i < $nleft; $i++)
622 $right = next($that->edits);
633 if (is_array($op) && is_array($newop))
635 // Both $op and $newop are adds.
636 for ($i = 0; $i < sizeof($newop); $i++)
639 else if (($op > 0 && $newop > 0) || ($op < 0 && $newop < 0))
640 { // $op and $newop are both either deletes or copies.
645 $comp->edits[] = $op;
650 $comp->edits[] = $op;
659 for (reset($this->edits);
660 $edit = current($this->edits);
667 echo "Delete " . -$edit;
668 else if (is_array($edit))
670 echo "Add " . sizeof($edit) . "<ul>";
671 for ($i = 0; $i < sizeof($edit); $i++)
672 echo "<li>" . htmlspecialchars($edit[$i]);
676 trigger_error("assertion error", E_USER_ERROR);
683 * Apply a WikiDiff to a set of lines.
687 * $diff = new WikiDiff($lines1, $lines2);
689 * // reconstruct $lines2 from $lines1:
690 * $out = $diff->apply($lines1);
692 function apply ($from_lines)
695 $xlim = sizeof($from_lines);
697 for ( reset($this->edits);
698 $edit = current($this->edits);
704 while (list ($junk, $line) = each($edit))
709 $output[] = $from_lines[$x++];
714 ExitWiki(sprintf(gettext ("WikiDiff::apply: line count mismatch: %s != %s"), $x, $xlim));
719 * Serialize a WikiDiff.
723 * $diff = new WikiDiff($lines1, $lines2);
724 * $string = $diff->serialize;
726 * // recover WikiDiff from serialized version:
727 * $diff2 = new WikiDiff($string);
729 function serialize ()
731 return serialize($this->edits);
735 * Return true if two files were equal.
739 if (sizeof($this->edits) > 1)
741 if (sizeof($this->edits) == 0)
743 // Test for: only edit is a copy.
744 return !is_array($this->edits[0]) && $this->edits[0] > 0;
748 * Compute the length of the Longest Common Subsequence (LCS).
750 * This is mostly for diagnostic purposed.
755 for (reset($this->edits);
756 $edit = current($this->edits);
759 if (!is_array($edit) && $edit > 0)
766 * Check a WikiDiff for validity.
768 * This is here only for debugging purposes.
770 function _check ($from_lines, $to_lines)
772 $test = $this->apply($from_lines);
773 if (serialize($test) != serialize($to_lines))
774 ExitWiki(gettext ("WikiDiff::_check: failed"));
777 $prev = current($this->edits);
778 $prevtype = is_array($prev) ? 'a' : ($prev > 0 ? 'c' : 'd');
780 while ($edit = next($this->edits))
782 $type = is_array($edit) ? 'a' : ($edit > 0 ? 'c' : 'd');
783 if ( $prevtype == $type )
784 ExitWiki(gettext ("WikiDiff::_check: edit sequence is non-optimal"));
788 printf ("<strong>" . gettext ("WikiDiff Okay: LCS = %s") . "</strong>\n", $lcs);
794 * A class to format a WikiDiff as HTML.
798 * $diff = new WikiDiff($lines1, $lines2); // compute diff.
800 * $fmt = new WikiDiffFormatter;
801 * echo $fmt->format($diff, $lines1); // Output HTMLified standard diff.
803 * or to output reverse diff (diff's that would take $lines2 to $lines1):
805 * $fmt = new WikiDiffFormatter('reversed');
806 * echo $fmt->format($diff, $lines1);
808 class WikiDiffFormatter
811 var $do_reverse_diff;
812 var $context_prefix, $deletes_prefix, $adds_prefix;
814 function WikiDiffFormatter ($reverse = false)
816 $this->do_reverse_diff = $reverse;
817 $this->context_lines = 0;
818 $this->context_prefix = ' ';
819 $this->deletes_prefix = '< ';
820 $this->adds_prefix = '> ';
823 function format ($diff, $from_lines)
825 return Element('table',
826 array('width' => '100%',
827 'bgcolor' => 'black',
831 $this->_format($diff->edits, $from_lines));
834 function _format ($edits, $from_lines)
838 $xlim = sizeof($from_lines);
841 while ($edit = current($edits))
843 if (!is_array($edit) && $edit >= 0)
844 { // Edit op is a copy.
852 // Start of an output hunk.
853 $xoff = max(0, $x - $this->context_lines);
854 $yoff = $xoff + $y - $x;
857 // Get leading context.
859 for ($i = $xoff; $i < $x; $i++)
860 $context[] = $from_lines[$i];
861 $hunk['c'] = $context;
865 { // Edit op is an add.
867 $hunk[$this->do_reverse_diff ? 'd' : 'a'] = $edit;
870 { // Edit op is a delete
873 $deletes[] = $from_lines[$x++];
874 $hunk[$this->do_reverse_diff ? 'a' : 'd'] = $deletes;
878 $next = next($edits);
881 if ( !$next || $ncopy > 2 * $this->context_lines)
883 // End of an output hunk.
887 $xend = min($x + $this->context_lines, $xlim);
890 // Get trailing context.
892 for ($i = $x; $i < $xend; $i++)
893 $context[] = $from_lines[$i];
894 $hunks[] = array('c' => $context);
897 $xlen = $xend - $xoff;
898 $ylen = $xend + $y - $x - $yoff;
899 $xbeg = $xlen ? $xoff + 1 : $xoff;
900 $ybeg = $ylen ? $yoff + 1 : $yoff;
902 if ($this->do_reverse_diff)
903 list ($xbeg, $xlen, $ybeg, $ylen)
904 = array($ybeg, $ylen, $xbeg, $xlen);
906 $rows .= $this->_emit_diff($xbeg,$xlen,$ybeg,$ylen, $hunks);
915 for ($i = $x; $i < $x + $ncopy; $i++)
916 $context[] = $from_lines[$i];
917 $hunk = array('c' => $context);
927 function _emit_lines($lines, $prefix, $color)
930 $prefix = Element('td', array('bgcolor' => '#cccccc', 'width' => "1%"), $prefix);
932 while (list ($junk, $line) = each($lines))
934 $line = rtrim($line);
935 $line = empty($line) ? ' ' : htmlspecialchars($line);
936 $html .= Element('tr', array('valign' => 'top'),
937 $prefix . Element('td', array('bgcolor' => $color),
938 Element('tt', $line)));
943 function _emit_diff ($xbeg,$xlen,$ybeg,$ylen,$hunks)
945 $header = Element('tr', array('bgcolor' => '#cccccc'),
946 Element('td', array('colspan' => 2),
948 $this->_diff_header($xbeg, $xlen, $ybeg, $ylen))));
950 $prefix = array('c' => $this->context_prefix,
951 'a' => $this->adds_prefix,
952 'd' => $this->deletes_prefix);
953 $color = array('c' => '#ffffff',
958 for (reset($hunks); $hunk = current($hunks); next($hunks))
960 if (!empty($hunk['c']))
961 $diff .= $this->_emit_lines($hunk['c'],
962 $this->context_prefix, '#ffffff');
963 if (!empty($hunk['d']))
964 $diff .= $this->_emit_lines($hunk['d'],
965 $this->deletes_prefix, '#ccffcc');
966 if (!empty($hunk['a']))
967 $diff .= $this->_emit_lines($hunk['a'],
968 $this->adds_prefix, '#ffcccc');
972 return Element('tr', Element('td',
974 array('width' => '100%',
975 'bgcolor' => 'white',
983 function _diff_header ($xbeg,$xlen,$ybeg,$ylen)
985 $what = $xlen ? ($ylen ? 'c' : 'd') : 'a';
986 $xlen = $xlen > 1 ? "," . ($xbeg + $xlen - 1) : '';
987 $ylen = $ylen > 1 ? "," . ($ybeg + $ylen - 1) : '';
989 return "$xbeg$xlen$what$ybeg$ylen";
994 * A class to format a WikiDiff as a pretty HTML unified diff.
998 * $diff = new WikiDiff($lines1, $lines2); // compute diff.
1000 * $fmt = new WikiUnifiedDiffFormatter;
1001 * echo $fmt->format($diff, $lines1); // Output HTMLified unified diff.
1003 class WikiUnifiedDiffFormatter extends WikiDiffFormatter
1005 function WikiUnifiedDiffFormatter ($reverse = false, $context_lines = 3)
1007 $this->do_reverse_diff = $reverse;
1008 $this->context_lines = $context_lines;
1009 $this->context_prefix = ' ';
1010 $this->deletes_prefix = '-';
1011 $this->adds_prefix = '+';
1014 function _diff_header ($xbeg,$xlen,$ybeg,$ylen)
1016 $xlen = $xlen == 1 ? '' : ",$xlen";
1017 $ylen = $ylen == 1 ? '' : ",$ylen";
1019 return "@@ -$xbeg$xlen +$ybeg$ylen @@";
1025 /////////////////////////////////////////////////////////////////
1027 function PageInfoRow ($pagename, $label, $rev)
1029 global $datetimeformat;
1031 $cols = QElement('td', array('align' => 'right'), $label);
1034 $url = WikiURL($pagename, array('version' => $rev->getVersion()));
1035 $linked_version = QElement('a', array('href' => $url), $rev->getVersion());
1036 $cols .= Element('td',
1037 gettext("version") . " " . $linked_version);
1039 $cols .= QElement('td',
1040 sprintf(gettext ("last modified on %s"),
1041 strftime($datetimeformat, $rev->get('mtime'))));
1042 $cols .= QElement('td',
1043 sprintf(gettext ("by %s"), $rev->get('author')));
1045 $cols .= QElement('td', array('colspan' => '3'),
1048 return Element('tr', $cols);
1051 function showDiff ($dbi, $request) {
1052 $pagename = $request->getArg('pagename');
1053 $version = $request->getArg('version');
1054 $previous = $request->getArg('previous');
1056 $page = $dbi->getPage($pagename);
1059 if (!($new = $page->getRevision($version)))
1060 NoSuchRevision($page, $version);
1061 $new_version = sprintf(gettext("version %d"), $version);
1064 $new = $page->getCurrentRevision();
1065 $new_version = gettext('current version');
1068 if (preg_match('/^\d+$/', $previous)) {
1069 if ( !($old = $page->getRevision($previous)) )
1070 NoSuchRevision($page, $previous);
1071 $old_version = sprintf(gettext("version %d"), $previous);
1072 $others = array('major', 'minor', 'author');
1075 switch ($previous) {
1078 while ($old = $page->getRevisionBefore($old)) {
1079 if (! $old->get('is_minor_edit'))
1082 $old_version = gettext("previous major revision");
1083 $others = array('minor', 'author');
1087 while ($old = $page->getRevisionBefore($old)) {
1088 if ($old->get('author') != $new->get('author'))
1091 $old_version = gettext("revision by previous author");
1092 $others = array('major', 'minor');
1097 $old = $page->getRevisionBefore($new);
1098 $old_version = gettext("previous revision");
1099 $others = array('major', 'author');
1104 $new_url = WikiURL($pagename, array('version' => $new->getVersion()));
1105 $new_link = QElement('a', array('href' => $new_url), $new_version);
1106 $old_url = WikiURL($pagename, array('version' => $old ? $old->getVersion() : 0));
1107 $old_link = QElement('a', array('href' => $old_url), $old_version);
1108 $page_link = LinkExistingWikiWord($pagename);
1110 $html = Element('p',
1111 sprintf(htmlspecialchars(gettext("Differences between %s and %s of %s.")),
1112 $new_link, $old_link, $page_link));
1115 $label = array('major' => gettext("Previous Major Revision"),
1116 'minor' => gettext("Previous Revision"),
1117 'author'=> gettext("Previous Author"));
1118 foreach ($others as $other) {
1119 $args = array('action' => 'diff', 'previous' => $other);
1121 $args['version'] = $version;
1122 $otherdiffs .= ' ' . QElement('a', array('href' => WikiURL($pagename, $args),
1123 'class' => 'wikiaction'),
1126 $html .= Element('p',
1127 htmlspecialchars(gettext("Other diffs:"))
1131 if ($old and $old->getVersion() == 0)
1134 $html .= Element('table',
1135 PageInfoRow($pagename, gettext ("Newer page:"), $new)
1136 . PageInfoRow($pagename, gettext ("Older page:"), $old));
1141 $diff = new WikiDiff($old->getContent(), $new->getContent());
1142 if ($diff->isEmpty()) {
1143 $html .= '<hr>[' . gettext ("Versions are identical") . ']';
1146 //$fmt = new WikiDiffFormatter;
1147 $fmt = new WikiUnifiedDiffFormatter;
1148 $html .= $fmt->format($diff, $old->getContent());
1152 include_once('lib/Template.php');
1153 echo GeneratePage('MESSAGE', $html,
1154 sprintf(gettext ("Diff: %s"), $pagename));
1160 // c-basic-offset: 4
1161 // c-hanging-comment-ender-p: nil
1162 // indent-tabs-mode: nil