InDefero

Sign in or create your account | Project List | Help

InDefero Git Source Tree

Root/src/IDF/Diff.php

1<?php
2/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3/*
4# ***** BEGIN LICENSE BLOCK *****
5# This file is part of InDefero, an open source project management application.
6# Copyright (C) 2008 Céondo Ltd and contributors.
7#
8# InDefero is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# InDefero is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21#
22# ***** END LICENSE BLOCK ***** */
23
24/**
25 * Diff parser.
26 *
27 */
28class IDF_Diff
29{
30    public $repo = '';
31    public $diff = '';
32    protected $lines = array();
33
34    public $files = array();
35
36    public function __construct($diff, $repo='')
37    {
38        $this->repo = $repo;
39        $this->diff = $diff;
40        $this->lines = preg_split("/\015\012|\015|\012/", $diff);
41    }
42
43    public function parse()
44    {
45        $current_file = '';
46        $current_chunk = 0;
47        $lline = 0;
48        $rline = 0;
49        $files = array();
50        $indiff = false; // Used to skip the headers in the git patches
51        $i = 0; // Used to skip the end of a git patch with --\nversion number
52        foreach ($this->lines as $line) {
53            $i++;
54            if (0 === strpos($line, '--') and isset($this->lines[$i])
55                and preg_match('/^\d+\.\d+\.\d+\.\d+$/', $this->lines[$i])) {
56                break;
57            }
58            if (0 === strpos($line, 'diff --git a')) {
59                $current_file = self::getFile($line);
60                $files[$current_file] = array();
61                $files[$current_file]['chunks'] = array();
62                $files[$current_file]['chunks_def'] = array();
63                $current_chunk = 0;
64                $indiff = true;
65                continue;
66            } else if (preg_match('#^diff -r [^\s]+ -r [^\s]+ (.+)$#', $line, $matches)) {
67                $current_file = $matches[1];
68                $files[$current_file] = array();
69                $files[$current_file]['chunks'] = array();
70                $files[$current_file]['chunks_def'] = array();
71                $current_chunk = 0;
72                $indiff = true;
73                continue;
74            } else if (0 === strpos($line, '=========')) {
75                // by default always use the new name of a possibly renamed file
76                $current_file = self::getMtnFile($this->lines[$i+1]);
77                // mtn 0.48 and newer set /dev/null as file path for dropped files
78                // so we display the old name here
79                if ($current_file == "/dev/null") {
80                    $current_file = self::getMtnFile($this->lines[$i]);
81                }
82                if ($current_file == "/dev/null") {
83                    throw new Exception(
84                        "could not determine path from diff"
85                    );
86                }
87                $files[$current_file] = array();
88                $files[$current_file]['chunks'] = array();
89                $files[$current_file]['chunks_def'] = array();
90                $current_chunk = 0;
91                $indiff = true;
92                continue;
93            } else if (0 === strpos($line, 'Index: ')) {
94                $current_file = self::getSvnFile($line);
95                $files[$current_file] = array();
96                $files[$current_file]['chunks'] = array();
97                $files[$current_file]['chunks_def'] = array();
98                $current_chunk = 0;
99                $indiff = true;
100                continue;
101            }
102            if (!$indiff) {
103                continue;
104            }
105            if (0 === strpos($line, '@@ ')) {
106                $files[$current_file]['chunks_def'][] = self::getChunk($line);
107                $files[$current_file]['chunks'][] = array();
108                $current_chunk++;
109                $lline = $files[$current_file]['chunks_def'][$current_chunk-1][0][0];
110                $rline = $files[$current_file]['chunks_def'][$current_chunk-1][1][0];
111                continue;
112            }
113            if (0 === strpos($line, '---') or 0 === strpos($line, '+++')) {
114                continue;
115            }
116            if (0 === strpos($line, '-')) {
117                $files[$current_file]['chunks'][$current_chunk-1][] = array($lline, '', substr($line, 1));
118                $lline++;
119                continue;
120            }
121            if (0 === strpos($line, '+')) {
122                $files[$current_file]['chunks'][$current_chunk-1][] = array('', $rline, substr($line, 1));
123                $rline++;
124                continue;
125            }
126            if (0 === strpos($line, ' ')) {
127                $files[$current_file]['chunks'][$current_chunk-1][] = array($lline, $rline, substr($line, 1));
128                $rline++;
129                $lline++;
130                continue;
131            }
132            if ($line == '') {
133                $files[$current_file]['chunks'][$current_chunk-1][] = array($lline, $rline, $line);
134                $rline++;
135                $lline++;
136                continue;
137            }
138        }
139        $this->files = $files;
140        return $files;
141    }
142
143    public static function getFile($line)
144    {
145        $line = substr(trim($line), 10);
146        $n = (int) strlen($line)/2;
147        return trim(substr($line, 3, $n-3));
148    }
149
150    public static function getSvnFile($line)
151    {
152        return substr(trim($line), 7);
153    }
154
155    public static function getMtnFile($line)
156    {
157        preg_match("/^[+-]{3} ([^\t]+)/", $line, $m);
158        return $m[1];
159    }
160
161    /**
162     * Return the html version of a parsed diff.
163     */
164    public function as_html()
165    {
166        $out = '';
167        foreach ($this->files as $filename=>$file) {
168            $pretty = '';
169            $fileinfo = IDF_Views_Source::getMimeType($filename);
170            if (IDF_Views_Source::isSupportedExtension($fileinfo[2])) {
171                $pretty = ' prettyprint';
172            }
173            $out .= "\n".'<table class="diff" summary="">'."\n";
174            $out .= '<tr id="diff-'.md5($filename).'"><th colspan="3">'.Pluf_esc($filename).'</th></tr>'."\n";
175            $cc = 1;
176            foreach ($file['chunks'] as $chunk) {
177                foreach ($chunk as $line) {
178                    if ($line[0] and $line[1]) {
179                        $class = 'diff-c';
180                    } elseif ($line[0]) {
181                        $class = 'diff-r';
182                    } else {
183                        $class = 'diff-a';
184                    }
185                    $line_content = self::padLine(Pluf_esc($line[2]));
186                    $out .= sprintf('<tr class="diff-line"><td class="diff-lc">%s</td><td class="diff-lc">%s</td><td class="%s%s mono">%s</td></tr>'."\n", $line[0], $line[1], $class, $pretty, $line_content);
187                }
188                if (count($file['chunks']) > $cc)
189                $out .= '<tr class="diff-next"><td>...</td><td>...</td><td>&nbsp;</td></tr>'."\n";
190                $cc++;
191            }
192            $out .= '</table>';
193        }
194        return Pluf_Template::markSafe($out);
195    }
196
197
198    public static function padLine($line)
199    {
200        $line = str_replace("\t", ' ', $line);
201        $n = strlen($line);
202        for ($i=0;$i<$n;$i++) {
203            if (substr($line, $i, 1) != ' ') {
204                break;
205            }
206        }
207        return str_repeat('&nbsp;', $i).substr($line, $i);
208    }
209
210    /**
211     * @return array array(array(start, n), array(start, n))
212     */
213    public static function getChunk($line)
214    {
215        $elts = explode(' ', $line);
216        $res = array();
217        for ($i=1;$i<3;$i++) {
218            $res[] = explode(',', trim(substr($elts[$i], 1)));
219        }
220        return $res;
221    }
222
223    /**
224     * Review patch.
225     *
226     * Given the original file as a string and the parsed
227     * corresponding diff chunks, generate a side by side view of the
228     * original file and new file with added/removed lines.
229     *
230     * Example of use:
231     *
232     * $diff = new IDF_Diff(file_get_contents($diff_file));
233     * $orig = file_get_contents($orig_file);
234     * $diff->parse();
235     * echo $diff->fileCompare($orig, $diff->files[$orig_file], $diff_file);
236     *
237     * @param string Original file
238     * @param array Chunk description of the diff corresponding to the file
239     * @param string Original file name
240     * @param int Number of lines before/after the chunk to be displayed (10)
241     * @return Pluf_Template_SafeString The table body
242     */
243    public function fileCompare($orig, $chunks, $filename, $context=10)
244    {
245        $orig_lines = preg_split("/\015\012|\015|\012/", $orig);
246        $new_chunks = $this->mergeChunks($orig_lines, $chunks, $context);
247        return $this->renderCompared($new_chunks, $filename);
248    }
249
250    public function mergeChunks($orig_lines, $chunks, $context=10)
251    {
252        $spans = array();
253        $new_chunks = array();
254        $min_line = 0;
255        $max_line = 0;
256        //if (count($chunks['chunks_def']) == 0) return '';
257        foreach ($chunks['chunks_def'] as $chunk) {
258            $start = ($chunk[0][0] > $context) ? $chunk[0][0]-$context : 0;
259            $end = (($chunk[0][0]+$chunk[0][1]+$context-1) < count($orig_lines)) ? $chunk[0][0]+$chunk[0][1]+$context-1 : count($orig_lines);
260            $spans[] = array($start, $end);
261        }
262        // merge chunks/get the chunk lines
263        // these are reference lines
264        $chunk_lines = array();
265        foreach ($chunks['chunks'] as $chunk) {
266            foreach ($chunk as $line) {
267                $chunk_lines[] = $line;
268            }
269        }
270        $i = 0;
271        foreach ($chunks['chunks'] as $chunk) {
272            $n_chunk = array();
273            // add lines before
274            if ($chunk[0][0] > $spans[$i][0]) {
275                for ($lc=$spans[$i][0];$lc<$chunk[0][0];$lc++) {
276                    $exists = false;
277                    foreach ($chunk_lines as $line) {
278                        if ($lc == $line[0]
279                            or ($chunk[0][1]-$chunk[0][0]+$lc) == $line[1]) {
280                            $exists = true;
281                            break;
282                        }
283                    }
284                    if (!$exists) {
285                        $orig = isset($orig_lines[$lc-1]) ? $orig_lines[$lc-1] : '';
286                        $n_chunk[] = array(
287                                           $lc,
288                                           $chunk[0][1]-$chunk[0][0]+$lc,
289                                           $orig
290                                           );
291                    }
292                }
293            }
294            // add chunk lines
295            foreach ($chunk as $line) {
296                $n_chunk[] = $line;
297            }
298            // add lines after
299            $lline = $line;
300            if (!empty($lline[0]) and $lline[0] < $spans[$i][1]) {
301                for ($lc=$lline[0];$lc<=$spans[$i][1];$lc++) {
302                    $exists = false;
303                    foreach ($chunk_lines as $line) {
304                        if ($lc == $line[0] or ($lline[1]-$lline[0]+$lc) == $line[1]) {
305                            $exists = true;
306                            break;
307                        }
308                    }
309                    if (!$exists) {
310                        $n_chunk[] = array(
311                                           $lc,
312                                           $lline[1]-$lline[0]+$lc,
313                                           $orig_lines[$lc-1]
314                                           );
315                    }
316                }
317            }
318            $new_chunks[] = $n_chunk;
319            $i++;
320        }
321        // Now, each chunk has the right length, we need to merge them
322        // when needed
323        $nnew_chunks = array();
324        $i = 0;
325        foreach ($new_chunks as $chunk) {
326            if ($i>0) {
327                $lline = end($nnew_chunks[$i-1]);
328                if ($chunk[0][0] <= $lline[0]+1) {
329                    // need merging
330                    foreach ($chunk as $line) {
331                        if ($line[0] > $lline[0] or empty($line[0])) {
332                            $nnew_chunks[$i-1][] = $line;
333                        }
334                    }
335                } else {
336                    $nnew_chunks[] = $chunk;
337                    $i++;
338                }
339            } else {
340                $nnew_chunks[] = $chunk;
341                $i++;
342            }
343        }
344        return $nnew_chunks;
345    }
346
347
348    public function renderCompared($chunks, $filename)
349    {
350        $fileinfo = IDF_Views_Source::getMimeType($filename);
351        $pretty = '';
352        if (IDF_Views_Source::isSupportedExtension($fileinfo[2])) {
353            $pretty = ' prettyprint';
354        }
355        $out = '';
356        $cc = 1;
357        $i = 0;
358        foreach ($chunks as $chunk) {
359            foreach ($chunk as $line) {
360                $line1 = '&nbsp;';
361                $line2 = '&nbsp;';
362                $line[2] = (strlen($line[2])) ? self::padLine(Pluf_esc($line[2])) : '&nbsp;';
363                if ($line[0] and $line[1]) {
364                    $class = 'diff-c';
365                    $line1 = $line2 = $line[2];
366                } elseif ($line[0]) {
367                    $class = 'diff-r';
368                    $line1 = $line[2];
369                } else {
370                    $class = 'diff-a';
371                    $line2 = $line[2];
372                }
373                $out .= sprintf('<tr class="diff-line"><td class="diff-lc">%s</td><td class="%s mono%s"><code>%s</code></td><td class="diff-lc">%s</td><td class="%s mono%s"><code>%s</code></td></tr>'."\n", $line[0], $class, $pretty, $line1, $line[1], $class, $pretty, $line2);
374            }
375            if (count($chunks) > $cc)
376                $out .= '<tr class="diff-next"><td>...</td><td>&nbsp;</td><td>...</td><td>&nbsp;</td></tr>'."\n";
377            $cc++;
378            $i++;
379        }
380        return Pluf_Template::markSafe($out);
381
382    }
383}
384

Archive Download this file

Branches:
dev
develop
master
newdiff
svn

Tags:
v1.0