473 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			473 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 | 
						|
/*
 | 
						|
# ***** BEGIN LICENSE BLOCK *****
 | 
						|
# This file is part of InDefero, an open source project management application.
 | 
						|
# Copyright (C) 2008-2011 Céondo Ltd and contributors.
 | 
						|
#
 | 
						|
# InDefero is free software; you can redistribute it and/or modify
 | 
						|
# it under the terms of the GNU General Public License as published by
 | 
						|
# the Free Software Foundation; either version 2 of the License, or
 | 
						|
# (at your option) any later version.
 | 
						|
#
 | 
						|
# InDefero is distributed in the hope that it will be useful,
 | 
						|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						|
# GNU General Public License for more details.
 | 
						|
#
 | 
						|
# You should have received a copy of the GNU General Public License
 | 
						|
# along with this program; if not, write to the Free Software
 | 
						|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 | 
						|
#
 | 
						|
# ***** END LICENSE BLOCK ***** */
 | 
						|
 | 
						|
/**
 | 
						|
 * Diff parser.
 | 
						|
 *
 | 
						|
 */
 | 
						|
class IDF_Diff
 | 
						|
{
 | 
						|
    public $path_strip_level = 0;
 | 
						|
    protected $lines = array();
 | 
						|
 | 
						|
    public $files = array();
 | 
						|
 | 
						|
    public function __construct($diff, $path_strip_level = 0)
 | 
						|
    {
 | 
						|
        $this->path_strip_level = $path_strip_level;
 | 
						|
        $this->lines = IDF_FileUtil::splitIntoLines($diff, true);
 | 
						|
    }
 | 
						|
 | 
						|
    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('<td>%s</td><td>%s</td>', $left, $right);
 | 
						|
                    $content = IDF_FileUtil::emphasizeControlCharacters(Pluf_esc($content));
 | 
						|
                    $contents[] = sprintf('<td class="%s%s mono">%s</td>', $class, $pretty, $content);
 | 
						|
                }
 | 
						|
                if (count($file['chunks']) > $cc) {
 | 
						|
                    $offsets[]  = '<td class="next">...</td><td class="next">...</td>';
 | 
						|
                    $contents[] = '<td class="next"></td>';
 | 
						|
                }
 | 
						|
                $cc++;
 | 
						|
            }
 | 
						|
 | 
						|
            list($added, $removed) = end($file['chunks_def']);
 | 
						|
 | 
						|
            $added = $added[0] + $added[1];
 | 
						|
            $leftwidth = 0;
 | 
						|
            if ($added > 0)
 | 
						|
                $leftwidth = ((ceil(log10($added)) + 1) * 8) + 17;
 | 
						|
 | 
						|
            $removed = $removed[0] + $removed[1];
 | 
						|
            $rightwidth = 0;
 | 
						|
            if ($removed > 0)
 | 
						|
                $rightwidth = ((ceil(log10($removed)) + 1) * 8) + 17;
 | 
						|
 | 
						|
            // we need to correct the width of a single column a little
 | 
						|
            // to take less space and to hide the empty one
 | 
						|
            $class = '';
 | 
						|
            if ($leftwidth == 0) {
 | 
						|
                $class = 'left-hidden';
 | 
						|
                $rightwidth -= floor(log10($removed));
 | 
						|
            }
 | 
						|
            else if ($rightwidth == 0) {
 | 
						|
                $class = 'right-hidden';
 | 
						|
                $leftwidth -= floor(log10($added));
 | 
						|
            }
 | 
						|
 | 
						|
            $inner_linecounts =
 | 
						|
              '<table class="diff-linecounts '.$class.'">' ."\n".
 | 
						|
                '<colgroup><col width="'.$leftwidth.'" /><col width="'. $rightwidth.'" /></colgroup>' ."\n".
 | 
						|
                '<tr class="line">' .
 | 
						|
                  implode('</tr>'."\n".'<tr class="line">', $offsets).
 | 
						|
                '</tr>' ."\n".
 | 
						|
              '</table>' ."\n";
 | 
						|
 | 
						|
 | 
						|
            $inner_contents =
 | 
						|
              '<table class="diff-contents">' ."\n".
 | 
						|
                '<tr class="line">' .
 | 
						|
                  implode('</tr>'."\n".'<tr class="line">', $contents) .
 | 
						|
                '</tr>' ."\n".
 | 
						|
              '</table>' ."\n";
 | 
						|
 | 
						|
            $out .= '<table class="diff unified">' ."\n".
 | 
						|
                      '<colgroup><col width="'.($leftwidth + $rightwidth + 1).'" /><col width="*" /></colgroup>' ."\n".
 | 
						|
                      '<tr id="diff-'.md5($filename).'">'.
 | 
						|
                        '<th colspan="2">'.Pluf_esc($filename).'</th>'.
 | 
						|
                      '</tr>' ."\n".
 | 
						|
                      '<tr>' .
 | 
						|
                        '<td>'. $inner_linecounts .'</td>'. "\n".
 | 
						|
                        '<td><div class="scroll">'. $inner_contents .'</div></td>'.
 | 
						|
                      '</tr>' ."\n".
 | 
						|
                    '</table>' ."\n";
 | 
						|
        }
 | 
						|
 | 
						|
        return Pluf_Template::markSafe($out);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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 = IDF_FileUtil::splitIntoLines($orig);
 | 
						|
        $new_chunks = $this->mergeChunks($orig_lines, $chunks, $context);
 | 
						|
        return $this->renderCompared($new_chunks, $filename);
 | 
						|
    }
 | 
						|
 | 
						|
    private 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;
 | 
						|
    }
 | 
						|
 | 
						|
    private function renderCompared($chunks, $filename)
 | 
						|
    {
 | 
						|
        $fileinfo = IDF_FileUtil::getMimeType($filename);
 | 
						|
        $pretty = '';
 | 
						|
        if (IDF_FileUtil::isSupportedExtension($fileinfo[2])) {
 | 
						|
            $pretty = ' prettyprint';
 | 
						|
        }
 | 
						|
 | 
						|
        $cc = 1;
 | 
						|
        $left_offsets   = array();
 | 
						|
        $left_contents  = array();
 | 
						|
        $right_offsets  = array();
 | 
						|
        $right_contents = array();
 | 
						|
 | 
						|
        $max_lineno_left = $max_lineno_right = 0;
 | 
						|
 | 
						|
        foreach ($chunks as $chunk) {
 | 
						|
            foreach ($chunk as $line) {
 | 
						|
                $left    = '';
 | 
						|
                $right   = '';
 | 
						|
                $content = IDF_FileUtil::emphasizeControlCharacters(Pluf_esc($line[2]));
 | 
						|
 | 
						|
                if ($line[0] and $line[1]) {
 | 
						|
                    $class = 'context';
 | 
						|
                    $left = $right = $content;
 | 
						|
                } elseif ($line[0]) {
 | 
						|
                    $class = 'removed';
 | 
						|
                    $left = $content;
 | 
						|
                } else {
 | 
						|
                    $class = 'added';
 | 
						|
                    $right = $content;
 | 
						|
                }
 | 
						|
 | 
						|
                $left_offsets[]   = sprintf('<td>%s</td>', $line[0]);
 | 
						|
                $right_offsets[]  = sprintf('<td>%s</td>', $line[1]);
 | 
						|
                $left_contents[]  = sprintf('<td class="%s%s mono">%s</td>', $class, $pretty, $left);
 | 
						|
                $right_contents[] = sprintf('<td class="%s%s mono">%s</td>', $class, $pretty, $right);
 | 
						|
 | 
						|
                $max_lineno_left  = max($max_lineno_left, $line[0]);
 | 
						|
                $max_lineno_right = max($max_lineno_right, $line[1]);
 | 
						|
            }
 | 
						|
 | 
						|
            if (count($chunks) > $cc) {
 | 
						|
                $left_offsets[]   = '<td class="next">...</td>';
 | 
						|
                $right_offsets[]  = '<td class="next">...</td>';
 | 
						|
                $left_contents[]  = '<td></td>';
 | 
						|
                $right_contents[] = '<td></td>';
 | 
						|
            }
 | 
						|
            $cc++;
 | 
						|
        }
 | 
						|
 | 
						|
        $leftwidth = 1;
 | 
						|
        if ($max_lineno_left > 0)
 | 
						|
            $leftwidth = ((ceil(log10($max_lineno_left)) + 1) * 8) + 17;
 | 
						|
 | 
						|
        $rightwidth = 1;
 | 
						|
        if ($max_lineno_right > 0)
 | 
						|
            $rightwidth = ((ceil(log10($max_lineno_right)) + 1) * 8) + 17;
 | 
						|
 | 
						|
        $inner_linecounts_left =
 | 
						|
          '<table class="diff-linecounts">' ."\n".
 | 
						|
            '<colgroup><col width="'.$leftwidth.'" /></colgroup>' ."\n".
 | 
						|
            '<tr class="line">' .
 | 
						|
              implode('</tr>'."\n".'<tr class="line">', $left_offsets).
 | 
						|
            '</tr>' ."\n".
 | 
						|
          '</table>' ."\n";
 | 
						|
 | 
						|
        $inner_linecounts_right =
 | 
						|
          '<table class="diff-linecounts">' ."\n".
 | 
						|
            '<colgroup><col width="'.$rightwidth.'" /></colgroup>' ."\n".
 | 
						|
            '<tr class="line">' .
 | 
						|
              implode('</tr>'."\n".'<tr class="line">', $right_offsets).
 | 
						|
            '</tr>' ."\n".
 | 
						|
          '</table>' ."\n";
 | 
						|
 | 
						|
        $inner_contents_left =
 | 
						|
          '<table class="diff-contents">' ."\n".
 | 
						|
            '<tr class="line">' .
 | 
						|
              implode('</tr>'."\n".'<tr class="line">', $left_contents) .
 | 
						|
            '</tr>' ."\n".
 | 
						|
          '</table>' ."\n";
 | 
						|
 | 
						|
        $inner_contents_right =
 | 
						|
          '<table class="diff-contents">' ."\n".
 | 
						|
            '<tr class="line">' .
 | 
						|
              implode('</tr>'."\n".'<tr class="line">', $right_contents) .
 | 
						|
            '</tr>' ."\n".
 | 
						|
          '</table>' ."\n";
 | 
						|
 | 
						|
        $out =
 | 
						|
          '<table class="diff context">' ."\n".
 | 
						|
            '<colgroup>' .
 | 
						|
              '<col width="'.($leftwidth + 1).'" /><col width="*" />' .
 | 
						|
              '<col width="'.($rightwidth + 1).'" /><col width="*" />' .
 | 
						|
            '</colgroup>' ."\n".
 | 
						|
            '<tr id="diff-'.md5($filename).'">'.
 | 
						|
              '<th colspan="4">'.Pluf_esc($filename).'</th>'.
 | 
						|
            '</tr>' ."\n".
 | 
						|
            '<tr>' .
 | 
						|
              '<th colspan="2">'.__('Old').'</th><th colspan="2">'.__('New').'</th>' .
 | 
						|
            '</tr>'.
 | 
						|
            '<tr>' .
 | 
						|
              '<td>'. $inner_linecounts_left .'</td>'. "\n".
 | 
						|
              '<td><div class="scroll">'. $inner_contents_left .'</div></td>'. "\n".
 | 
						|
              '<td>'. $inner_linecounts_right .'</td>'. "\n".
 | 
						|
              '<td><div class="scroll">'. $inner_contents_right .'</div></td>'. "\n".
 | 
						|
            '</tr>' ."\n".
 | 
						|
            '</table>' ."\n";
 | 
						|
 | 
						|
        return Pluf_Template::markSafe($out);
 | 
						|
    }
 | 
						|
}
 |