path_strip_level = $path_strip_level;
$this->lines = self::splitIntoLines($diff);
}
/**
* Splits a diff into separate lines while retaining the individual
* line ending character for every line
*/
private static function splitIntoLines($diff)
{
// this works because in unified diff format even empty lines are
// either prefixed with a '+', '-' or ' '
$splitted = preg_split("/\r\n|\n/", $diff, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
$last_off = -1;
$lines = array();
while (($split = array_shift($splitted)) !== null) {
if ($last_off != -1) {
$lines[] .= substr($diff, $last_off, $split[1] - $last_off);
}
$last_off = $split[1];
}
$lines[] = substr($diff, $last_off);
return $lines;
}
public function parse()
{
$current_file = '';
$current_chunk = 0;
$lline = 0;
$rline = 0;
$files = array();
$indiff = false; // Used to skip the headers in the git patches
$i = 0; // Used to skip the end of a git patch with --\nversion number
$diffsize = count($this->lines);
while ($i < $diffsize) {
// look for the potential beginning of a diff
if (substr($this->lines[$i], 0, 4) !== '--- ') {
$i++;
continue;
}
// we're inside a diff candiate
$oldfileline = $this->lines[$i++];
$newfileline = $this->lines[$i++];
if (substr($newfileline, 0, 4) !== '+++ ') {
// not a valid diff here, move on
continue;
}
// use new file name by default
preg_match("/^\+\+\+ ([^\t\n\r]+)/", $newfileline, $m);
$current_file = $m[1];
if ($current_file === '/dev/null') {
// except if it's /dev/null, use the old one instead
// eg. mtn 0.48 and newer
preg_match("/^--- ([^\t\r\n]+)/", $oldfileline, $m);
$current_file = $m[1];
}
if ($this->path_strip_level > 0) {
$fileparts = explode('/', $current_file, $this->path_strip_level+1);
$current_file = array_pop($fileparts);
}
$current_chunk = 0;
$files[$current_file] = array();
$files[$current_file]['chunks'] = array();
$files[$current_file]['chunks_def'] = array();
while ($i < $diffsize && substr($this->lines[$i], 0, 3) === '@@ ') {
$elems = preg_match('/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@.*/',
$this->lines[$i++], $results);
if ($elems != 1) {
// hunk is badly formatted
break;
}
$delstart = $results[1];
$dellines = $results[2] === '' ? 1 : $results[2];
$addstart = $results[3];
$addlines = $results[4] === '' ? 1 : $results[4];
$files[$current_file]['chunks_def'][] = array(
array($delstart, $dellines), array($addstart, $addlines)
);
$files[$current_file]['chunks'][] = array();
while ($i < $diffsize && ($addlines >= 0 || $dellines >= 0)) {
$linetype = $this->lines[$i] != '' ? $this->lines[$i][0] : false;
$content = substr($this->lines[$i], 1);
switch ($linetype) {
case ' ':
$files[$current_file]['chunks'][$current_chunk][] =
array($delstart, $addstart, $content);
$dellines--;
$addlines--;
$delstart++;
$addstart++;
break;
case '+':
$files[$current_file]['chunks'][$current_chunk][] =
array('', $addstart, $content);
$addlines--;
$addstart++;
break;
case '-':
$files[$current_file]['chunks'][$current_chunk][] =
array($delstart, '', $content);
$dellines--;
$delstart++;
break;
case '\\':
// no new line at the end of this file; remove pseudo new line from last line
$cur = count($files[$current_file]['chunks'][$current_chunk]) - 1;
$files[$current_file]['chunks'][$current_chunk][$cur][2] =
rtrim($files[$current_file]['chunks'][$current_chunk][$cur][2], "\r\n");
continue;
default:
break 2;
}
$i++;
}
$current_chunk++;
}
}
$this->files = $files;
return $files;
}
/**
* Return the html version of a parsed diff.
*/
public function as_html()
{
$out = '';
foreach ($this->files as $filename => $file) {
$pretty = '';
$fileinfo = IDF_FileUtil::getMimeType($filename);
if (IDF_FileUtil::isSupportedExtension($fileinfo[2])) {
$pretty = ' prettyprint';
}
$cc = 1;
$offsets = array();
$contents = array();
foreach ($file['chunks'] as $chunk) {
foreach ($chunk as $line) {
list($left, $right, $content) = $line;
if ($left and $right) {
$class = 'context';
} elseif ($left) {
$class = 'removed';
} else {
$class = 'added';
}
$offsets[] = sprintf('
%s | %s | ', $left, $right);
$content = Pluf_esc($content);
$content = self::makeNonPrintableCharsVisible($content);
$contents[] = sprintf('%s | ', $class, $pretty, $content);
}
if (count($file['chunks']) > $cc) {
$offsets[] = '... | ... | ';
$contents[] = ' | ';
}
$cc++;
}
list($added, $removed) = end($file['chunks_def']);
$added = $added[0] + $added[1];
$leftwidth = 1;
if ($added > 0)
$leftwidth = ((ceil(log10($added)) + 1) * 8) + 12;
$removed = $removed[0] + $removed[1];
$rightwidth = 1;
if ($removed > 0)
$rightwidth = ((ceil(log10($removed)) + 1) * 8) + 12;
$inner_linecounts =
'' ."\n".
'' ."\n".
'' .
implode('
'."\n".'', $offsets).
'
' ."\n".
'
' ."\n";
$inner_contents =
'' ."\n".
'' .
implode('
'."\n".'', $contents) .
'
' ."\n".
'
' ."\n";
$out .= '' ."\n".
'' ."\n".
''.
''.Pluf_esc($filename).' | '.
'
' ."\n".
'' .
''. $inner_linecounts .' | '. "\n".
''. $inner_contents .' | '.
'
' ."\n".
'
' ."\n";
}
return Pluf_Template::markSafe($out);
}
private static function makeNonPrintableCharsVisible($line)
{
return preg_replace('/([\x00-\x1F])/ue',
'"".bin2hex("\\1").""',
$line);
}
public static function padLine($line)
{
$line = str_replace("\t", ' ', $line);
$n = strlen($line);
for ($i=0;$i<$n;$i++) {
if (substr($line, $i, 1) != ' ') {
break;
}
}
return str_repeat(' ', $i).substr($line, $i);
}
/**
* Review patch.
*
* Given the original file as a string and the parsed
* corresponding diff chunks, generate a side by side view of the
* original file and new file with added/removed lines.
*
* Example of use:
*
* $diff = new IDF_Diff(file_get_contents($diff_file));
* $orig = file_get_contents($orig_file);
* $diff->parse();
* echo $diff->fileCompare($orig, $diff->files[$orig_file], $diff_file);
*
* @param string Original file
* @param array Chunk description of the diff corresponding to the file
* @param string Original file name
* @param int Number of lines before/after the chunk to be displayed (10)
* @return Pluf_Template_SafeString The table body
*/
public function fileCompare($orig, $chunks, $filename, $context=10)
{
$orig_lines = self::splitIntoLines($orig);
$new_chunks = $this->mergeChunks($orig_lines, $chunks, $context);
return $this->renderCompared($new_chunks, $filename);
}
public function mergeChunks($orig_lines, $chunks, $context=10)
{
$spans = array();
$new_chunks = array();
$min_line = 0;
$max_line = 0;
//if (count($chunks['chunks_def']) == 0) return '';
foreach ($chunks['chunks_def'] as $chunk) {
$start = ($chunk[0][0] > $context) ? $chunk[0][0]-$context : 0;
$end = (($chunk[0][0]+$chunk[0][1]+$context-1) < count($orig_lines)) ? $chunk[0][0]+$chunk[0][1]+$context-1 : count($orig_lines);
$spans[] = array($start, $end);
}
// merge chunks/get the chunk lines
// these are reference lines
$chunk_lines = array();
foreach ($chunks['chunks'] as $chunk) {
foreach ($chunk as $line) {
$chunk_lines[] = $line;
}
}
$i = 0;
foreach ($chunks['chunks'] as $chunk) {
$n_chunk = array();
// add lines before
if ($chunk[0][0] > $spans[$i][0]) {
for ($lc=$spans[$i][0];$lc<$chunk[0][0];$lc++) {
$exists = false;
foreach ($chunk_lines as $line) {
if ($lc == $line[0]
or ($chunk[0][1]-$chunk[0][0]+$lc) == $line[1]) {
$exists = true;
break;
}
}
if (!$exists) {
$orig = isset($orig_lines[$lc-1]) ? $orig_lines[$lc-1] : '';
$n_chunk[] = array(
$lc,
$chunk[0][1]-$chunk[0][0]+$lc,
$orig
);
}
}
}
// add chunk lines
foreach ($chunk as $line) {
$n_chunk[] = $line;
}
// add lines after
$lline = $line;
if (!empty($lline[0]) and $lline[0] < $spans[$i][1]) {
for ($lc=$lline[0];$lc<=$spans[$i][1];$lc++) {
$exists = false;
foreach ($chunk_lines as $line) {
if ($lc == $line[0] or ($lline[1]-$lline[0]+$lc) == $line[1]) {
$exists = true;
break;
}
}
if (!$exists) {
$n_chunk[] = array(
$lc,
$lline[1]-$lline[0]+$lc,
$orig_lines[$lc-1]
);
}
}
}
$new_chunks[] = $n_chunk;
$i++;
}
// Now, each chunk has the right length, we need to merge them
// when needed
$nnew_chunks = array();
$i = 0;
foreach ($new_chunks as $chunk) {
if ($i>0) {
$lline = end($nnew_chunks[$i-1]);
if ($chunk[0][0] <= $lline[0]+1) {
// need merging
foreach ($chunk as $line) {
if ($line[0] > $lline[0] or empty($line[0])) {
$nnew_chunks[$i-1][] = $line;
}
}
} else {
$nnew_chunks[] = $chunk;
$i++;
}
} else {
$nnew_chunks[] = $chunk;
$i++;
}
}
return $nnew_chunks;
}
public function renderCompared($chunks, $filename)
{
$fileinfo = IDF_FileUtil::getMimeType($filename);
$pretty = '';
if (IDF_FileUtil::isSupportedExtension($fileinfo[2])) {
$pretty = ' prettyprint';
}
$out = '';
$cc = 1;
$i = 0;
foreach ($chunks as $chunk) {
foreach ($chunk as $line) {
$line1 = ' ';
$line2 = ' ';
$line[2] = (strlen($line[2])) ? self::padLine(Pluf_esc($line[2])) : ' ';
if ($line[0] and $line[1]) {
$class = 'diff-c';
$line1 = $line2 = $line[2];
} elseif ($line[0]) {
$class = 'diff-r';
$line1 = $line[2];
} else {
$class = 'diff-a';
$line2 = $line[2];
}
$out .= sprintf('%s | %s | %s | %s |
'."\n", $line[0], $class, $pretty, $line1, $line[1], $class, $pretty, $line2);
}
if (count($chunks) > $cc)
$out .= '... | | ... | |
'."\n";
$cc++;
$i++;
}
return Pluf_Template::markSafe($out);
}
}