wiki
[ class tree: wiki ] [ index: wiki ] [ all elements ]

Source for file diff.php

Documentation is available at diff.php

  1. <?php
  2. /**
  3.  * $Header: /cvsroot/bitweaver/_bit_wiki/diff.php,v 1.3 2005/08/24 21:00:26 squareing Exp $
  4.  *
  5.  * A PHP diff engine for phpwiki.
  6.  *
  7.  * Copyright (c) 2004 bitweaver.org
  8.  * Copyright (c) 2003 tikwiki.org
  9.  * Copyright (c) 2000 Geoffrey T. Dairiki <dairiki@dairiki.org>
  10.  * All Rights Reserved. See copyright.txt for details and a complete list of authors.
  11.  * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details
  12.  *
  13.  * $Id: diff.php,v 1.3 2005/08/24 21:00:26 squareing Exp $
  14.  * @package wiki
  15.  */
  16.  
  17. /**
  18.  * required setup
  19.  */
  20. define('USE_ASSERTS'function_exists('assert'));
  21. /**
  22.  * Class used internally by WikiDiff to actually compute the diffs.
  23.  *
  24.  * The algorithm used here is mostly lifted from the perl module
  25.  * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
  26.  *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
  27.  *
  28.  * More ideas are taken from:
  29.  *   http://www.ics.uci.edu/~eppstein/161/960229.html
  30.  *
  31.  * Some ideas are (and a bit of code) are from from analyze.c, from GNU
  32.  * diffutils-2.7, which can be found at:
  33.  *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
  34.  *
  35.  * Finally, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
  36.  * are my own.
  37.  * @package wiki
  38.  * @subpackage WikiDiffEngine
  39.  */
  40. {
  41.   var $edits;    // List of editing operation to convert XV to YV.
  42.   
  43.   var $xv = array()$yv = array();
  44.   var $xchanged$ychanged;
  45.   function _WikiDiffEngine ($from_lines$to_lines)
  46.       {
  47.     $n_from sizeof($from_lines);
  48.     $n_to sizeof($to_lines);
  49.     $endskip 0;
  50.         // Ignore differences in line endings
  51.         for ($i 0$i $n_from$i++)
  52.           {
  53.             $from_lines[$irtrim($from_lines[$i]);
  54.           }
  55.         for ($i 0$i $n_to$i++)
  56.           {
  57.             $to_lines[$irtrim($to_lines[$i]);
  58.           }
  59.     // Ignore trailing and leading matching lines.
  60.     while ($n_from && $n_to 0)
  61.       {
  62.         if ($from_lines[$n_from 1!= $to_lines[$n_to 1])
  63.         break;
  64.         $n_from--;
  65.         $n_to--;
  66.         $endskip++;
  67.       }
  68.     for $skip 0$skip min($n_from$n_to)$skip++)
  69.         if ($from_lines[$skip!= $to_lines[$skip])
  70.         break;
  71.     $n_from -= $skip;
  72.     $n_to -= $skip;
  73.     $xlines array();
  74.     $ylines array();
  75.     // Ignore lines which do not exist in both files.
  76.     for ($x 0$x $n_from$x++)
  77.         $xhash[$from_lines[$x $skip]] 1;
  78.     for ($y 0$y $n_to$y++)
  79.       {
  80.         $line $to_lines[$y $skip];
  81.         $ylines[$line;
  82.         if ( ($this->ychanged[$yempty($xhash[$line])) )
  83.         continue;
  84.         $yhash[$line1;
  85.         $this->yv[$line;
  86.         $this->yind[$y;
  87.       }
  88.     for ($x 0$x $n_from$x++)
  89.       {
  90.         $line $from_lines[$x $skip];
  91.         $xlines[$line;
  92.         if ( ($this->xchanged[$xempty($yhash[$line])) )
  93.         continue;
  94.         $this->xv[$line;
  95.         $this->xind[$x;
  96.       }
  97.     // Find the LCS.
  98.     $this->_compareseq(0sizeof($this->xv)0sizeof($this->yv));
  99.     // Merge edits when possible
  100.     $this->_shift_boundaries($xlines$this->xchanged$this->ychanged);
  101.     $this->_shift_boundaries($ylines$this->ychanged$this->xchanged);
  102.     // Compute the edit operations.
  103.     $this->edits = array();
  104.     if ($skip)
  105.         $this->edits[$skip;
  106.     $x 0;
  107.     $y 0;
  108.     while ($x $n_from || $y $n_to)
  109.       {
  110.         USE_ASSERTS && assert($y $n_to || $this->xchanged[$x]);
  111.         USE_ASSERTS && assert($x $n_from || $this->ychanged[$y]);
  112.         // Skip matching "snake".
  113.         $x0 $x;
  114.         $ncopy 0;
  115.         while $x $n_from && $y $n_to
  116.                 && !$this->xchanged[$x&& !$this->ychanged[$y])
  117.           {
  118.         ++$x;
  119.         ++$y;
  120.         ++$ncopy;
  121.           }
  122.         if ($x $x0)
  123.         $this->edits[$x $x0;
  124.         // Find deletes.
  125.         $x0 $x;
  126.         $ndelete 0;
  127.         while ($x $n_from && $this->xchanged[$x])
  128.           {
  129.         ++$x;
  130.         ++$ndelete;
  131.           }
  132.         if ($x $x0)
  133.         $this->edits[= -($x $x0);
  134.         // Find adds.
  135.         if (isset($this->ychanged[$y]&& $this->ychanged[$y])
  136.           {
  137.         $adds array();
  138.         while ($y $n_to && $this->ychanged[$y])
  139.             $adds["" $ylines[$y++];
  140.         $this->edits[$adds;
  141.           }
  142.       }
  143.     if ($endskip != 0)
  144.         $this->edits[$endskip;
  145.       }
  146.   /* Divide the Largest Common Subsequence (LCS) of the sequences
  147.    * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
  148.    * sized segments.
  149.    *
  150.    * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
  151.    * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
  152.    * sub sequences.  The first sub-sequence is contained in [X0, X1),
  153.    * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
  154.    * that (X0, Y0) == (XOFF, YOFF) and
  155.    * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
  156.    *
  157.    * This function assumes that the first lines of the specified portions
  158.    * of the two files do not match, and likewise that the last lines do not
  159.    * match.  The caller must trim matching lines from the beginning and end
  160.    * of the portions it is going to specify.
  161.    */
  162.   function _diag ($xoff$xlim$yoff$ylim$nchunks)
  163.       {
  164.     $flip false;
  165.     if ($xlim $xoff $ylim $yoff)
  166.       {
  167.         // Things seems faster (I'm not sure I understand why)
  168.         // when the shortest sequence in X.
  169.         $flip true;
  170.         list ($xoff$xlim$yoff$ylim)
  171.         = array$yoff$ylim$xoff$xlim);
  172.       }
  173.     if ($flip)
  174.         for ($i $ylim 1$i >= $yoff$i--)
  175.         $ymatches[$this->xv[$i]][$i;
  176.     else
  177.         for ($i $ylim 1$i >= $yoff$i--)
  178.         $ymatches[$this->yv[$i]][$i;
  179.     $this->lcs 0;
  180.     $this->seq[0]$yoff 1;
  181.     $this->in_seq array();
  182.     $ymids[0array();
  183.     $numer $xlim $xoff $nchunks 1;
  184.     $x $xoff;
  185.     for ($chunk 0$chunk $nchunks$chunk++)
  186.       {
  187.         if ($chunk 0)
  188.         for ($i 0$i <= $this->lcs$i++)
  189.             $ymids[$i][$chunk-1$this->seq[$i];
  190.         $x1 $xoff + (int)(($numer ($xlim-$xoff)*$chunk$nchunks);
  191.         for $x $x1$x++)
  192.           {
  193.         $matches $ymatches[$flip $this->yv[$x$this->xv[$x]];
  194.         if (!$matches)
  195.             continue;
  196.         reset($matches);
  197.         while (list ($junk$yeach($matches))
  198.             if (empty($this->in_seq[$y]))
  199.               {
  200.             $k $this->_lcs_pos($y);
  201.             USE_ASSERTS && assert($k 0);
  202.             $ymids[$k$ymids[$k-1];
  203.             break;
  204.               }
  205.         while (list ($junk$yeach($matches))
  206.           {
  207.             if ($y $this->seq[$k-1])
  208.               {
  209.             USE_ASSERTS && assert($y $this->seq[$k]);
  210.             // Optimization: this is a common case:
  211.             //  next match is just replacing previous match.
  212.             $this->in_seq[$this->seq[$k]] false;
  213.             $this->seq[$k$y;
  214.             $this->in_seq[$y1;
  215.               }
  216.             else if (empty($this->in_seq[$y]))
  217.               {
  218.             $k $this->_lcs_pos($y);
  219.             USE_ASSERTS && assert($k 0);
  220.             $ymids[$k$ymids[$k-1];
  221.               }
  222.           }
  223.           }
  224.       }
  225.     $seps[$flip array($yoff$xoffarray($xoff$yoff);
  226.     $ymid $ymids[$this->lcs];
  227.     for ($n 0$n $nchunks 1$n++)
  228.       {
  229.         $x1 $xoff + (int)(($numer ($xlim $xoff$n$nchunks);
  230.         $y1 $ymid[$n1;
  231.         $seps[$flip array($y1$x1array($x1$y1);
  232.       }
  233.     $seps[$flip array($ylim$xlimarray($xlim$ylim);
  234.     return array($this->lcs$seps);
  235.       }
  236.   function _lcs_pos ($ypos)
  237.       {
  238.     $end $this->lcs;
  239.     if ($end == || $ypos $this->seq[$end])
  240.       {
  241.         $this->seq[++$this->lcs$ypos;
  242.         $this->in_seq[$ypos1;
  243.         return $this->lcs;
  244.       }
  245.     $beg 1;
  246.     while ($beg $end)
  247.       {
  248.         $mid = (int)(($beg $end2);
  249.         if $ypos $this->seq[$mid)
  250.         $beg $mid 1;
  251.         else
  252.         $end $mid;
  253.       }
  254.     USE_ASSERTS && assert($ypos != $this->seq[$end]);
  255.     $this->in_seq[$this->seq[$end]] false;
  256.     $this->seq[$end$ypos;
  257.     $this->in_seq[$ypos1;
  258.     return $end;
  259.       }
  260.   /* Find LCS of two sequences.
  261.    *
  262.    * The results are recorded in the vectors $this->{x,y}changed[], by
  263.    * storing a 1 in the element for each line that is an insertion
  264.    * or deletion (ie. is not in the LCS).
  265.    *
  266.    * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
  267.    *
  268.    * Note that XLIM, YLIM are exclusive bounds.
  269.    * All line numbers are origin-0 and discarded lines are not counted.
  270.    */
  271.   function _compareseq ($xoff$xlim$yoff$ylim)
  272.       {
  273.     // Slide down the bottom initial diagonal.
  274.     while ($xoff $xlim && $yoff $ylim
  275.     && $this->xv[$xoff== $this->yv[$yoff])
  276.       {
  277.         ++$xoff;
  278.         ++$yoff;
  279.       }
  280.     // Slide up the top initial diagonal.
  281.     while ($xlim $xoff && $ylim $yoff
  282.     && $this->xv[$xlim 1== $this->yv[$ylim 1])
  283.       {
  284.         --$xlim;
  285.         --$ylim;
  286.       }
  287.     if ($xoff == $xlim || $yoff == $ylim)
  288.         $lcs 0;
  289.     else
  290.       {
  291.         // This is ad hoc but seems to work well.
  292.         //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
  293.         //$nchunks = max(2,min(8,(int)$nchunks));
  294.         $nchunks min(7$xlim $xoff$ylim $yoff1;
  295.         list ($lcs$seps)
  296.         = $this->_diag($xoff,$xlim,$yoff$ylim,$nchunks);
  297.       }
  298.     if ($lcs == 0)
  299.       {
  300.         // X and Y sequences have no common subsequence:
  301.         // mark all changed.
  302.         while ($yoff $ylim)
  303.         $this->ychanged[$this->yind[$yoff++]] 1;
  304.         while ($xoff $xlim)
  305.         $this->xchanged[$this->xind[$xoff++]] 1;
  306.       }
  307.     else
  308.       {
  309.         // Use the partitions to split this problem into subproblems.
  310.         reset($seps);
  311.         $pt1 $seps[0];
  312.         while ($pt2 next($seps))
  313.           {
  314.         $this->_compareseq ($pt1[0]$pt2[0]$pt1[1]$pt2[1]);
  315.         $pt1 $pt2;
  316.           }
  317.       }
  318.       }
  319.   /* Adjust inserts/deletes of identical lines to join changes
  320.    * as much as possible.
  321.    *
  322.    * We do something when a run of changed lines include a
  323.    * line at one end and has an excluded, identical line at the other.
  324.    * We are free to choose which identical line is included.
  325.    * `compareseq' usually chooses the one at the beginning,
  326.    * but usually it is cleaner to consider the following identical line
  327.    * to be the "change".
  328.    *
  329.    * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
  330.    */
  331.   function _shift_boundaries ($lines&$changed$other_changed)
  332.       {
  333.     $i 0;
  334.     $j 0;
  335.     USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
  336.     $len sizeof($lines);
  337.     $other_len sizeof($other_changed);
  338.     while (1)
  339.       {
  340.         /*
  341.          * Scan forwards to find beginning of another run of changes.
  342.          * Also keep track of the corresponding point in the other file.
  343.          *
  344.          * Throughout this code, $i and $j are adjusted together so that
  345.          * the first $i elements of $changed and the first $j elements
  346.          * of $other_changed both contain the same number of zeros
  347.          * (unchanged lines).
  348.          * Furthermore, $j is always kept so that $j == $other_len or
  349.          * $other_changed[$j] == false.
  350.          */
  351.         while ($j $other_len && $other_changed[$j])
  352.         $j++;
  353.         while ($i $len && $changed[$i])
  354.           {
  355.         USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
  356.         $i++$j++;
  357.         while ($j $other_len && $other_changed[$j])
  358.             $j++;
  359.           }
  360.         if ($i == $len)
  361.         break;
  362.         $start $i;
  363.         // Find the end of this run of changes.
  364.         while (++$i $len && $changed[$i])
  365.         continue;
  366.         do
  367.           {
  368.         /*
  369.          * Record the length of this run of changes, so that
  370.          * we can later determine whether the run has grown.
  371.          */
  372.         $runlength $i $start;
  373.         /*
  374.          * Move the changed region back, so long as the
  375.          * previous unchanged line matches the last changed one.
  376.          * This merges with previous changed regions.
  377.          */
  378.         while ($start && $lines[$start 1== $lines[$i 1])
  379.           {
  380.             $changed[--$start1;
  381.             $changed[--$ifalse;
  382.             while ($start && $changed[$start 1])
  383.             $start--;
  384.             USE_ASSERTS && assert('$j > 0');
  385.             while ($other_changed[--$j])
  386.             continue;
  387.             USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
  388.           }
  389.         /*
  390.          * Set CORRESPONDING to the end of the changed run, at the last
  391.          * point where it corresponds to a changed run in the other file.
  392.          * CORRESPONDING == LEN means no such point has been found.
  393.          */
  394.         $corresponding $j $other_len $i $len;
  395.         /*
  396.          * Move the changed region forward, so long as the
  397.          * first changed line matches the following unchanged one.
  398.          * This merges with following changed regions.
  399.          * Do this second, so that if there are no merges,
  400.          * the changed region is moved forward as far as possible.
  401.          */
  402.         while ($i $len && $lines[$start== $lines[$i])
  403.           {
  404.             $changed[$start++false;
  405.             $changed[$i++1;
  406.             while ($i $len && $changed[$i])
  407.             $i++;
  408.             USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
  409.             $j++;
  410.             if ($j $other_len && $other_changed[$j])
  411.               {
  412.             $corresponding $i;
  413.             while ($j $other_len && $other_changed[$j])
  414.                 $j++;
  415.               }
  416.           }
  417.           }
  418.         while ($runlength != $i $start);
  419.         /*
  420.          * If possible, move the fully-merged run of changes
  421.          * back to a corresponding run in the other file.
  422.          */
  423.         while ($corresponding $i)
  424.           {
  425.         $changed[--$start1;
  426.         $changed[--$i0;
  427.         USE_ASSERTS && assert('$j > 0');
  428.         while ($other_changed[--$j])
  429.             continue;
  430.         USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
  431.           }
  432.       }
  433.       }
  434. }
  435. /**
  436.  * Class representing a diff between two files.
  437.  *
  438.  * @package wiki
  439.  * @subpackage WikiDiff
  440.  */
  441. class WikiDiff
  442. {
  443.   var $edits;
  444.   /**
  445.    * Compute diff between files (or deserialize serialized WikiDiff.)
  446.    */
  447.   function WikiDiff($from_lines false$to_lines false)
  448.       {
  449.     if ($from_lines && $to_lines)
  450.       {
  451.         $compute new _WikiDiffEngine($from_lines$to_lines);
  452.         $this->edits = $compute->edits;
  453.       }
  454.     else if ($from_lines)
  455.       {
  456.         // $from_lines is not really from_lines, but rather
  457.         // a serialized WikiDiff.
  458.         $this->edits = unserialize($from_lines);
  459.       }
  460.     else
  461.       {
  462.         $this->edits = array();
  463.       }
  464.       }
  465.   /**
  466.    * Compute reversed WikiDiff.
  467.    *
  468.    * SYNOPSIS:
  469.    *
  470.    *  $diff = new WikiDiff($lines1, $lines2);
  471.    *  $rev = $diff->reverse($lines1);
  472.    *
  473.    *  // reconstruct $lines1 from $lines2:
  474.    *  $out = $rev->apply($lines2);
  475.    */
  476.   function reverse ($from_lines)
  477.       {
  478.     $x 0;
  479.     $rev new WikiDiff;
  480.     for reset($this->edits);
  481.           $edit current($this->edits);
  482.           next($this->edits<