535 lines
16 KiB
PHP
535 lines
16 KiB
PHP
<?php
|
|
/** DomFramework
|
|
* @package domframework
|
|
* @author Dominique Fournier <dominique@fournier38.fr>
|
|
* @license BSD
|
|
*/
|
|
|
|
namespace Domframework;
|
|
|
|
/** Create a diff from two strings, array or files
|
|
* The output is compatible with "patch" command.
|
|
*/
|
|
class xdiff
|
|
{
|
|
/** The name of file1
|
|
*/
|
|
private $filename1 = "Original";
|
|
|
|
/** The name of file2
|
|
*/
|
|
private $filename2 = "New";
|
|
|
|
/** The timestamp for file1
|
|
*/
|
|
private $file1Time;
|
|
|
|
/** The timestamp for file2
|
|
*/
|
|
private $file2Time;
|
|
|
|
/** The output requested
|
|
*/
|
|
private $output = null;
|
|
|
|
/** Define the size of one column in side by side mode
|
|
* 62 chars by default
|
|
* The maximum width used on screen is 2*$s2sWidth+2 (134 by default)
|
|
* The value must validate $s2sWidth = 8 * X - 2 (with X is integer), like
|
|
* 6, 14, 22, 30, 38, 46, 54, 62, 70, 78, 86, 94, 102
|
|
*/
|
|
private $s2sWidth = 62;
|
|
|
|
/** The constructor allow to choose the output.
|
|
* @param string $output The output mode [Normal|Unified|SideBySide]
|
|
*/
|
|
public function __construct ($output = "normal")
|
|
// {{{
|
|
{
|
|
if (! method_exists ($this, "display".ucfirst ($output)))
|
|
throw new \Exception ("Invalid output requested to xdiff", 406);
|
|
$this->output = "display".ucfirst ($output);
|
|
$this->file1Time = date ("Y-m-d H:i:s.u000 O");
|
|
$this->file2Time = date ("Y-m-d H:i:s.u001 O");
|
|
}
|
|
// }}}
|
|
|
|
/** Allow to set the side by side width to the maximum allowed by screenWidth
|
|
* @param integer $screenWidth The maximum width of the screen
|
|
*/
|
|
public function setScreenWidth ($screenWidth)
|
|
// {{{
|
|
{
|
|
for ($x = 20 ; $x > 0 ; $x--)
|
|
{
|
|
$s2sWidth = 8 * $x - 2;
|
|
if (2*$s2sWidth+2 <= $screenWidth)
|
|
{
|
|
$this->s2sWidth = $s2sWidth;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
/** Compute the differences between two strings $string1 and $string2
|
|
* @param string $string1 The first string to compare
|
|
* @param string $string2 The second string to compare
|
|
*/
|
|
public function diff ($string1, $string2)
|
|
// {{{
|
|
{
|
|
if (! is_string ($string1))
|
|
throw new \Exception (
|
|
"Invalid string1 provided to diff method : not a string", 406);
|
|
if (! is_string ($string2))
|
|
throw new \Exception (
|
|
"Invalid string2 provided to diff method : not a string", 406);
|
|
return $this->diffArray (
|
|
preg_split ("#(.*\\R)#", $string1, -1,
|
|
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY),
|
|
preg_split ("#(.*\\R)#", $string2, -1,
|
|
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY));
|
|
}
|
|
// }}}
|
|
|
|
/** Compute the differences between two files $file1 and $file2
|
|
* @param string $file1 The first file to use to compare
|
|
* @param string $file2 The second file to use to compare
|
|
*/
|
|
public function diffFile ($file1, $file2)
|
|
// {{{
|
|
{
|
|
if (! is_string ($file1))
|
|
throw new \Exception (
|
|
"Invalid file1 provided to diff method : not a string", 406);
|
|
if (! is_string ($file2))
|
|
throw new \Exception (
|
|
"Invalid file2 provided to diff method : not a string", 406);
|
|
if (! file_exists ($file1))
|
|
throw new \Exception (
|
|
"Invalid file1 provided to diff method : file don't exists", 406);
|
|
if (! file_exists ($file2))
|
|
throw new \Exception (
|
|
"Invalid file2 provided to diff method : file don't exists", 406);
|
|
if (! is_readable ($file1))
|
|
throw new \Exception (
|
|
"Invalid file1 provided to diff method : file is not readable", 406);
|
|
if (! is_readable ($file2))
|
|
throw new \Exception (
|
|
"Invalid file2 provided to diff method : file is not readable", 406);
|
|
$this->filename1 = $file1;
|
|
$this->filename2 = $file2;
|
|
$this->file1Time = date ("Y-m-d H:i:s.u000 O",
|
|
filemtime ($this->filename1));
|
|
$this->file2Time = date ("Y-m-d H:i:s.u001 O",
|
|
filemtime ($this->filename2));
|
|
return $this->diff (file_get_contents ($file1),
|
|
file_get_contents ($file2));
|
|
}
|
|
// }}}
|
|
|
|
/** Compute the differences between two arrays $array1 and $array2
|
|
* @param array $array1 The first array to compare
|
|
* @param array $array2 The second array to compare
|
|
*/
|
|
public function diffArray ($array1, $array2)
|
|
// {{{
|
|
{
|
|
$diff = $this->computeArray ($array1, $array2);
|
|
$method = $this->output;
|
|
return $this->$method ($diff);
|
|
}
|
|
// }}}
|
|
|
|
/** Compute the differences between two arrays $array1 and $array2
|
|
* @param array $array1 The first array to compare
|
|
* @param array $array2 The second array to compare
|
|
* @return array The data in internal format
|
|
*/
|
|
final public function computeArray ($array1, $array2)
|
|
// {{{
|
|
{
|
|
if (! is_array ($array1))
|
|
throw new \Exception (
|
|
"Invalid array1 provided to diffArray method : not a array", 406);
|
|
if (! is_array ($array2))
|
|
throw new \Exception (
|
|
"Invalid array2 provided to diffArray method : not a array", 406);
|
|
$array1 = array_values ($array1);
|
|
$array2 = array_values ($array2);
|
|
$diff = array ();
|
|
$i = 0; // $i is the index for $array1
|
|
$j = 0; // $j is the index for $array2
|
|
while ($i < count ($array1) || $j < count ($array2))
|
|
{
|
|
$chunk1 = array ();
|
|
$chunk2 = array ();
|
|
if (key_exists ($i, $array1) && is_array ($array1[$i]))
|
|
throw new \Exception ("Can not diff a multidimensional array");
|
|
if (key_exists ($j, $array2) && is_array ($array2[$j]))
|
|
throw new \Exception ("Can not diff a multidimensional array");
|
|
if (key_exists ($i, $array1) && key_exists ($j, $array2) &&
|
|
$array1[$i] === $array2[$j])
|
|
{
|
|
// EQUAL
|
|
while (key_exists ($i, $array1) && key_exists ($j, $array2) &&
|
|
$array1[$i] === $array2[$j])
|
|
{
|
|
$chunk1[] = $array1[$i];
|
|
$i++;
|
|
$j++;
|
|
}
|
|
$diff[] = array ("type" => "Equal",
|
|
"startLine1" => 1 + $i - count ($chunk1),
|
|
"endLine1" => $i,
|
|
"startLine2" => 1 + $j - count ($chunk1),
|
|
"endLine2" => $j,
|
|
"length" => count ($chunk1),
|
|
"chunk" => $chunk1);
|
|
|
|
continue;
|
|
}
|
|
// Generate the chunks
|
|
$lcs = $this->lcs ($array1, $array2, $i, $j);
|
|
if ($lcs === "")
|
|
{
|
|
while ($i < count ($array1))
|
|
{
|
|
if ($array1[$i] === $lcs)
|
|
break;
|
|
$chunk1[] = $array1[$i];
|
|
$i++;
|
|
$lcs = $this->lcs ($array1, $array2, $i, $j);
|
|
}
|
|
while ($j < count ($array2))
|
|
{
|
|
if ($array2[$j] === $lcs)
|
|
break;
|
|
$chunk2[] = $array2[$j];
|
|
$j++;
|
|
$lcs = $this->lcs ($array1, $array2, $i, $j);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while ($i < count ($array1))
|
|
{
|
|
if ($array1[$i] === $lcs)
|
|
break;
|
|
$chunk1[] = $array1[$i];
|
|
$i++;
|
|
}
|
|
while ($j < count ($array2))
|
|
{
|
|
if ($array2[$j] === $lcs)
|
|
break;
|
|
$chunk2[] = $array2[$j];
|
|
$j++;
|
|
}
|
|
}
|
|
// Add the diffs by the chunks availability
|
|
if (empty ($chunk1) && ! empty ($chunk2))
|
|
{
|
|
// APPEND
|
|
$diff[] = array ("type" => "Append",
|
|
"startLine1" => $i,
|
|
"endLine1" => $i,
|
|
"startLine2" => 1 + $j - count ($chunk2),
|
|
"endLine2" => $j,
|
|
"length" => count ($chunk2),
|
|
"chunk" => $chunk2);
|
|
}
|
|
elseif (! empty ($chunk1) && empty ($chunk2))
|
|
{
|
|
// DELETE
|
|
$diff[] = array ("type" => "Delete",
|
|
"startLine1" => 1 + $i - count ($chunk1),
|
|
"endLine1" => $i,
|
|
"startLine2" => $j,
|
|
"endLine2" => $j,
|
|
"length" => count ($chunk1),
|
|
"chunk" => $chunk1);
|
|
}
|
|
else
|
|
{
|
|
// CHANGE ON BOTH ARRAY
|
|
$diff[] = array ("type" => "Change",
|
|
"startLine1" => 1 + $i - count ($chunk1),
|
|
"endLine1" => $i,
|
|
"startLine2" => 1 + $j - count ($chunk2),
|
|
"endLine2" => $j,
|
|
"length1" => count ($chunk1),
|
|
"length2" => count ($chunk2),
|
|
"chunk1" => $chunk1,
|
|
"chunk2" => $chunk2);
|
|
}
|
|
}
|
|
return $diff;
|
|
}
|
|
// }}}
|
|
|
|
/** Return a string like "diff -u"
|
|
* @param array $diffArray The diff array analyzed by diffArray method
|
|
* @return string
|
|
*/
|
|
private function displayUnified ($diffArray)
|
|
// {{{
|
|
{
|
|
$d = "";
|
|
$i = 0 ;
|
|
while ($i < count ($diffArray))
|
|
{
|
|
$diff = $diffArray[$i];
|
|
$i++;
|
|
if ($diff["type"] === "Equal")
|
|
continue;
|
|
|
|
|
|
if ($diff["type"] === "Append")
|
|
{
|
|
$info = "@@ -";
|
|
$info .= $diff["startLine1"];
|
|
$info .= ",0";
|
|
$info .= " +";
|
|
$info .= $diff["startLine2"];
|
|
if ($diff["length"] !== 1)
|
|
$info .= ",".$diff["length"];
|
|
$info .= " @@\n";
|
|
$d .= $info;
|
|
$d .= "+".implode ("+", $diff["chunk"]);
|
|
}
|
|
elseif ($diff["type"] === "Delete")
|
|
{
|
|
$info = "@@ -";
|
|
$info .= $diff["startLine1"];
|
|
if ($diff["length"] !== 1)
|
|
$info .= ",".$diff["length"];
|
|
$info .= " +";
|
|
$info .= $diff["startLine2"];
|
|
$info .= ",0";
|
|
$info .= " @@\n";
|
|
$d .= $info;
|
|
$d .= "-".implode ("-", $diff["chunk"]);
|
|
}
|
|
elseif ($diff["type"] === "Change")
|
|
{
|
|
$info = "@@ -";
|
|
$info .= $diff["startLine1"];
|
|
if ($diff["length1"] !== 1)
|
|
$info .= ",".$diff["length1"];
|
|
$info .= " +";
|
|
$info .= $diff["startLine2"];
|
|
if ($diff["length2"] !== 1)
|
|
$info .= ",".$diff["length2"];
|
|
$info .= " @@\n";
|
|
$d .= $info;
|
|
$d .= "-".implode ("-", $diff["chunk1"]);
|
|
$d .= "+".implode ("+", $diff["chunk2"]);
|
|
}
|
|
else
|
|
throw new \Exception ("Invalid Chunk Type : ".$diff["type"]);
|
|
}
|
|
if ($d === "")
|
|
return $d;
|
|
$e = "--- $this->filename1 $this->file1Time\n";
|
|
$e .= "+++ $this->filename2 $this->file2Time\n";
|
|
return $e.$d;
|
|
}
|
|
// }}}
|
|
|
|
/** Return a string like "diff" without parameter
|
|
* @param array $diffArray The diff array analyzed by diffArray method
|
|
* @return string
|
|
*/
|
|
private function displayNormal ($diffArray)
|
|
// {{{
|
|
{
|
|
$d = "";
|
|
$i = 0 ;
|
|
while ($i < count ($diffArray))
|
|
{
|
|
$diff = $diffArray[$i];
|
|
$i++;
|
|
if ($diff["type"] === "Equal")
|
|
continue;
|
|
$info = $diff["startLine1"];
|
|
if ($diff["startLine1"] !== $diff["endLine1"])
|
|
$info .= ",".$diff["endLine1"];
|
|
$info .= "%s";
|
|
$info .= $diff["startLine2"];
|
|
if ($diff["startLine2"] !== $diff["endLine2"])
|
|
$info .= ",".$diff["endLine2"];
|
|
$info .= "\n";
|
|
if ($diff["type"] === "Append")
|
|
{
|
|
$d .= sprintf ($info, "a");
|
|
$d .= "> ".implode ("> ", $diff["chunk"]);
|
|
}
|
|
elseif ($diff["type"] === "Delete")
|
|
{
|
|
$d .= sprintf ($info, "d");
|
|
$d .= "< ".implode ("< ", $diff["chunk"]);
|
|
}
|
|
elseif ($diff["type"] === "Change")
|
|
{
|
|
$d .= sprintf ($info, "c");
|
|
$d .= "< ".implode ("< ", $diff["chunk1"]);
|
|
$d .= "---\n";
|
|
$d .= "> ".implode ("> ", $diff["chunk2"]);
|
|
}
|
|
else
|
|
throw new \Exception ("Invalid Chunk Type : ".$diff["type"]);
|
|
}
|
|
return $d;
|
|
}
|
|
// }}}
|
|
|
|
/** Return a string like "diff -y" (Side by Side)
|
|
* @param array $diffArray The diff array analyzed by diffArray method
|
|
* @return string
|
|
*/
|
|
private function displaySideBySide ($diffArray)
|
|
// {{{
|
|
{
|
|
$d = "";
|
|
$i = 0 ;
|
|
while ($i < count ($diffArray))
|
|
{
|
|
$diff = $diffArray[$i];
|
|
$i++;
|
|
if ($diff["type"] === "Equal")
|
|
{
|
|
foreach ($diff["chunk"] as $line)
|
|
$d .= $this->side ($line, $line);
|
|
}
|
|
elseif ($diff["type"] === "Append")
|
|
{
|
|
foreach ($diff["chunk"] as $line)
|
|
$d .= $this->side ("", $line, ">");
|
|
}
|
|
elseif ($diff["type"] === "Delete")
|
|
{
|
|
foreach ($diff["chunk"] as $line)
|
|
$d .= $this->side ($line, "", "<");
|
|
}
|
|
elseif ($diff["type"] === "Change")
|
|
{
|
|
$x = 0;
|
|
$y = 0;
|
|
while (key_exists ($x, $diff["chunk1"]) ||
|
|
key_exists ($y, $diff["chunk2"]))
|
|
{
|
|
$side1 = (key_exists ($x, $diff["chunk1"])) ?
|
|
$diff["chunk1"][$x] : "";
|
|
$side2 = (key_exists ($y, $diff["chunk2"])) ?
|
|
$diff["chunk2"][$y] : "";
|
|
$d .= $this->side ($side1, $side2, "|");
|
|
if ($side1 !== "")
|
|
$x++;
|
|
if ($side2 !== "")
|
|
$y++;
|
|
}
|
|
}
|
|
}
|
|
return $d;
|
|
}
|
|
// }}}
|
|
|
|
/** Return a string sideBySide. Used by displaySideBySide for each line
|
|
* @param string $side1 The string to be displayed on side1
|
|
* @param string $side2 The string to be displayed on side2
|
|
* @param string|null $symbol The symbol used to present the diff (<>|)
|
|
*/
|
|
private function side ($side1, $side2, $symbol = null)
|
|
// {{{
|
|
{
|
|
$d = "";
|
|
if (! is_string ($side1))
|
|
throw new \Exception ("XDiff : side1 parameter not a string");
|
|
if (! is_string ($side2))
|
|
throw new \Exception ("XDiff : side2 parameter not a string");
|
|
if (trim ($symbol) === "")
|
|
$symbol = null;
|
|
$side1 = mb_substr (rtrim ($side1), 0, $this->s2sWidth - 1);
|
|
$side2 = mb_substr (rtrim ($side2), 0, $this->s2sWidth - 1);
|
|
$side1 = str_replace ("\t", " ", $side1);
|
|
$side2 = str_replace ("\t", " ", $side2);
|
|
$d .= $side1;
|
|
$nbtabs = floor ((1 + $this->s2sWidth - mb_strlen ($side1)) / 8);
|
|
if ($symbol !== null || $side2 !== "")
|
|
$d .= str_repeat ("\t", $nbtabs);
|
|
if ($side1 !== "" && $symbol === null)
|
|
$d .= "\t";
|
|
elseif ($symbol !== null)
|
|
{
|
|
if (mb_strlen ($side1) < $this->s2sWidth - 1)
|
|
{
|
|
if ($nbtabs > 1)
|
|
$d .= str_repeat (" ", 5);
|
|
else
|
|
$d .= str_repeat (" ", $this->s2sWidth - mb_strlen ($side1) - 1);
|
|
}
|
|
$d .= " ".$symbol;
|
|
}
|
|
if ($symbol !== null && $side2 !== "")
|
|
$d .= "\t";
|
|
$d .= $side2;
|
|
$d .= "\n";
|
|
return $d;
|
|
}
|
|
// }}}
|
|
|
|
/** This function return the next common part between both arrays starting at
|
|
* position $i for $array1 and $j for array2
|
|
* Return empty string if no common lines was found
|
|
* @param array $array1 The first array to compare
|
|
* @param array $array2 The second array to compare
|
|
* @param integer $i The position pointer on first array
|
|
* @param integer $j The position pointer on second array
|
|
* @return string
|
|
*/
|
|
private function lcs ($array1, $array2, $i, $j)
|
|
// {{{
|
|
{
|
|
$found1 = false;
|
|
$found2 = false;
|
|
while ($i < count ($array1))
|
|
{
|
|
$tmp2 = $j;
|
|
while ($tmp2 < count ($array2))
|
|
{
|
|
if ($array1[$i] === $array2[$tmp2] && trim ($array1[$i]) !== "")
|
|
{
|
|
$found1 = true;
|
|
break 2;
|
|
}
|
|
$tmp2++;
|
|
}
|
|
$i++;
|
|
}
|
|
while ($j < count ($array2))
|
|
{
|
|
$tmp1 = $i;
|
|
while ($tmp1 < count ($array1))
|
|
{
|
|
if ($array2[$j] === $array1[$tmp1] && trim ($array2[$j]) !== "")
|
|
{
|
|
$found2 = true;
|
|
break 2;
|
|
}
|
|
$tmp1++;
|
|
}
|
|
$j++;
|
|
}
|
|
if (! $found1 || ! $found2)
|
|
return "";
|
|
if ($tmp1 - $i < $tmp2 - $j)
|
|
{
|
|
return "";
|
|
}
|
|
return $array1[$i];
|
|
}
|
|
// }}}
|
|
}
|