Indefero

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-2011 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 $path_strip_level = 0;
31 protected $lines = array();
32
33 public $files = array();
34
35 public function __construct($diff, $path_strip_level = 0)
36 {
37 $this->path_strip_level = $path_strip_level;
38 $this->lines = IDF_FileUtil::splitIntoLines($diff, true);
39 }
40
41 public function parse()
42 {
43 $current_file = '';
44 $current_chunk = 0;
45 $lline = 0;
46 $rline = 0;
47 $files = array();
48 $indiff = false; // Used to skip the headers in the git patches
49 $i = 0; // Used to skip the end of a git patch with --\nversion number
50 $diffsize = count($this->lines);
51 while ($i < $diffsize) {
52 // look for the potential beginning of a diff
53 if (substr($this->lines[$i], 0, 4) !== '--- ') {
54 $i++;
55 continue;
56 }
57
58 // we're inside a diff candiate
59 $oldfileline = $this->lines[$i++];
60 $newfileline = $this->lines[$i++];
61 if (substr($newfileline, 0, 4) !== '+++ ') {
62 // not a valid diff here, move on
63 continue;
64 }
65
66 // use new file name by default
67 preg_match("/^\+\+\+ ([^\t\n\r]+)/", $newfileline, $m);
68 $current_file = $m[1];
69 if ($current_file === '/dev/null') {
70 // except if it's /dev/null, use the old one instead
71 // eg. mtn 0.48 and newer
72 preg_match("/^--- ([^\t\r\n]+)/", $oldfileline, $m);
73 $current_file = $m[1];
74 }
75 if ($this->path_strip_level > 0) {
76 $fileparts = explode('/', $current_file, $this->path_strip_level+1);
77 $current_file = array_pop($fileparts);
78 }
79 $current_chunk = 0;
80 $files[$current_file] = array();
81 $files[$current_file]['chunks'] = array();
82 $files[$current_file]['chunks_def'] = array();
83
84 while ($i < $diffsize && substr($this->lines[$i], 0, 3) === '@@ ') {
85 $elems = preg_match('/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@.*/',
86 $this->lines[$i++], $results);
87 if ($elems != 1) {
88 // hunk is badly formatted
89 break;
90 }
91 $delstart = $results[1];
92 $dellines = $results[2] === '' ? 1 : $results[2];
93 $addstart = $results[3];
94 $addlines = $results[4] === '' ? 1 : $results[4];
95
96 $files[$current_file]['chunks_def'][] = array(
97 array($delstart, $dellines), array($addstart, $addlines)
98 );
99 $files[$current_file]['chunks'][] = array();
100
101 while ($i < $diffsize && ($addlines >= 0 || $dellines >= 0)) {
102 $linetype = $this->lines[$i] != '' ? $this->lines[$i][0] : false;
103 $content = substr($this->lines[$i], 1);
104 switch ($linetype) {
105 case ' ':
106 $files[$current_file]['chunks'][$current_chunk][] =
107 array($delstart, $addstart, $content);
108 $dellines--;
109 $addlines--;
110 $delstart++;
111 $addstart++;
112 break;
113 case '+':
114 $files[$current_file]['chunks'][$current_chunk][] =
115 array('', $addstart, $content);
116 $addlines--;
117 $addstart++;
118 break;
119 case '-':
120 $files[$current_file]['chunks'][$current_chunk][] =
121 array($delstart, '', $content);
122 $dellines--;
123 $delstart++;
124 break;
125 case '\\':
126 // no new line at the end of this file; remove pseudo new line from last line
127 $cur = count($files[$current_file]['chunks'][$current_chunk]) - 1;
128 $files[$current_file]['chunks'][$current_chunk][$cur][2] =
129 rtrim($files[$current_file]['chunks'][$current_chunk][$cur][2], "\r\n");
130 continue;
131 default:
132 break 2;
133 }
134 $i++;
135 }
136 $current_chunk++;
137 }
138 }
139 $this->files = $files;
140 return $files;
141 }
142
143 /**
144 * Return the html version of a parsed diff.
145 */
146 public function as_html()
147 {
148 $out = '';
149 foreach ($this->files as $filename => $file) {
150 $pretty = '';
151 $fileinfo = IDF_FileUtil::getMimeType($filename);
152 if (IDF_FileUtil::isSupportedExtension($fileinfo[2])) {
153 $pretty = ' prettyprint';
154 }
155
156 $cc = 1;
157 $offsets = array();
158 $contents = array();
159
160 foreach ($file['chunks'] as $chunk) {
161 foreach ($chunk as $line) {
162 list($left, $right, $content) = $line;
163 if ($left and $right) {
164 $class = 'context';
165 } elseif ($left) {
166 $class = 'removed';
167 } else {
168 $class = 'added';
169 }
170
171 $offsets[] = sprintf('<td>%s</td><td>%s</td>', $left, $right);
172 $content = IDF_FileUtil::emphasizeControlCharacters(Pluf_esc($content));
173 $contents[] = sprintf('<td class="%s%s mono">%s</td>', $class, $pretty, $content);
174 }
175 if (count($file['chunks']) > $cc) {
176 $offsets[] = '<td class="next">...</td><td class="next">...</td>';
177 $contents[] = '<td class="next"></td>';
178 }
179 $cc++;
180 }
181
182 list($added, $removed) = end($file['chunks_def']);
183
184 $added = $added[0] + $added[1];
185 $leftwidth = 0;
186 if ($added > 0)
187 $leftwidth = ((ceil(log10($added)) + 1) * 8) + 17;
188
189 $removed = $removed[0] + $removed[1];
190 $rightwidth = 0;
191 if ($removed > 0)
192 $rightwidth = ((ceil(log10($removed)) + 1) * 8) + 17;
193
194 // we need to correct the width of a single column a little
195 // to take less space and to hide the empty one
196 $class = '';
197 if ($leftwidth == 0) {
198 $class = 'left-hidden';
199 $rightwidth -= floor(log10($removed));
200 }
201 else if ($rightwidth == 0) {
202 $class = 'right-hidden';
203 $leftwidth -= floor(log10($added));
204 }
205
206 $inner_linecounts =
207 '<table class="diff-linecounts '.$class.'">' ."\n".
208 '<colgroup><col width="'.$leftwidth.'" /><col width="'. $rightwidth.'" /></colgroup>' ."\n".
209 '<tr class="line">' .
210 implode('</tr>'."\n".'<tr class="line">', $offsets).
211 '</tr>' ."\n".
212 '</table>' ."\n";
213
214
215 $inner_contents =
216 '<table class="diff-contents">' ."\n".
217 '<tr class="line">' .
218 implode('</tr>'."\n".'<tr class="line">', $contents) .
219 '</tr>' ."\n".
220 '</table>' ."\n";
221
222 $out .= '<table class="diff unified">' ."\n".
223 '<colgroup><col width="'.($leftwidth + $rightwidth + 1).'" /><col width="*" /></colgroup>' ."\n".
224 '<tr id="diff-'.md5($filename).'">'.
225 '<th colspan="2">'.Pluf_esc($filename).'</th>'.
226 '</tr>' ."\n".
227 '<tr>' .
228 '<td>'. $inner_linecounts .'</td>'. "\n".
229 '<td><div class="scroll">'. $inner_contents .'</div></td>'.
230 '</tr>' ."\n".
231 '</table>' ."\n";
232 }
233
234 return Pluf_Template::markSafe($out);
235 }
236
237 /**
238 * Review patch.
239 *
240 * Given the original file as a string and the parsed
241 * corresponding diff chunks, generate a side by side view of the
242 * original file and new file with added/removed lines.
243 *
244 * Example of use:
245 *
246 * $diff = new IDF_Diff(file_get_contents($diff_file));
247 * $orig = file_get_contents($orig_file);
248 * $diff->parse();
249 * echo $diff->fileCompare($orig, $diff->files[$orig_file], $diff_file);
250 *
251 * @param string Original file
252 * @param array Chunk description of the diff corresponding to the file
253 * @param string Original file name
254 * @param int Number of lines before/after the chunk to be displayed (10)
255 * @return Pluf_Template_SafeString The table body
256 */
257 public function fileCompare($orig, $chunks, $filename, $context=10)
258 {
259 $orig_lines = IDF_FileUtil::splitIntoLines($orig);
260 $new_chunks = $this->mergeChunks($orig_lines, $chunks, $context);
261 return $this->renderCompared($new_chunks, $filename);
262 }
263
264 private function mergeChunks($orig_lines, $chunks, $context=10)
265 {
266 $spans = array();
267 $new_chunks = array();
268 $min_line = 0;
269 $max_line = 0;
270 //if (count($chunks['chunks_def']) == 0) return '';
271 foreach ($chunks['chunks_def'] as $chunk) {
272 $start = ($chunk[0][0] > $context) ? $chunk[0][0]-$context : 0;
273 $end = (($chunk[0][0]+$chunk[0][1]+$context-1) < count($orig_lines)) ? $chunk[0][0]+$chunk[0][1]+$context-1 : count($orig_lines);
274 $spans[] = array($start, $end);
275 }
276 // merge chunks/get the chunk lines
277 // these are reference lines
278 $chunk_lines = array();
279 foreach ($chunks['chunks'] as $chunk) {
280 foreach ($chunk as $line) {
281 $chunk_lines[] = $line;
282 }
283 }
284 $i = 0;
285 foreach ($chunks['chunks'] as $chunk) {
286 $n_chunk = array();
287 // add lines before
288 if ($chunk[0][0] > $spans[$i][0]) {
289 for ($lc=$spans[$i][0];$lc<$chunk[0][0];$lc++) {
290 $exists = false;
291 foreach ($chunk_lines as $line) {
292 if ($lc == $line[0]
293 or ($chunk[0][1]-$chunk[0][0]+$lc) == $line[1]) {
294 $exists = true;
295 break;
296 }
297 }
298 if (!$exists) {
299 $orig = isset($orig_lines[$lc-1]) ? $orig_lines[$lc-1] : '';
300 $n_chunk[] = array(
301 $lc,
302 $chunk[0][1]-$chunk[0][0]+$lc,
303 $orig
304 );
305 }
306 }
307 }
308 // add chunk lines
309 foreach ($chunk as $line) {
310 $n_chunk[] = $line;
311 }
312 // add lines after
313 $lline = $line;
314 if (!empty($lline[0]) and $lline[0] < $spans[$i][1]) {
315 for ($lc=$lline[0];$lc<=$spans[$i][1];$lc++) {
316 $exists = false;
317 foreach ($chunk_lines as $line) {
318 if ($lc == $line[0] or ($lline[1]-$lline[0]+$lc) == $line[1]) {
319 $exists = true;
320 break;
321 }
322 }
323 if (!$exists) {
324 $n_chunk[] = array(
325 $lc,
326 $lline[1]-$lline[0]+$lc,
327 $orig_lines[$lc-1]
328 );
329 }
330 }
331 }
332 $new_chunks[] = $n_chunk;
333 $i++;
334 }
335 // Now, each chunk has the right length, we need to merge them
336 // when needed
337 $nnew_chunks = array();
338 $i = 0;
339 foreach ($new_chunks as $chunk) {
340 if ($i>0) {
341 $lline = end($nnew_chunks[$i-1]);
342 if ($chunk[0][0] <= $lline[0]+1) {
343 // need merging
344 foreach ($chunk as $line) {
345 if ($line[0] > $lline[0] or empty($line[0])) {
346 $nnew_chunks[$i-1][] = $line;
347 }
348 }
349 } else {
350 $nnew_chunks[] = $chunk;
351 $i++;
352 }
353 } else {
354 $nnew_chunks[] = $chunk;
355 $i++;
356 }
357 }
358 return $nnew_chunks;
359 }
360
361 private function renderCompared($chunks, $filename)
362 {
363 $fileinfo = IDF_FileUtil::getMimeType($filename);
364 $pretty = '';
365 if (IDF_FileUtil::isSupportedExtension($fileinfo[2])) {
366 $pretty = ' prettyprint';
367 }
368
369 $cc = 1;
370 $left_offsets = array();
371 $left_contents = array();
372 $right_offsets = array();
373 $right_contents = array();
374
375 $max_lineno_left = $max_lineno_right = 0;
376
377 foreach ($chunks as $chunk) {
378 foreach ($chunk as $line) {
379 $left = '';
380 $right = '';
381 $content = IDF_FileUtil::emphasizeControlCharacters(Pluf_esc($line[2]));
382
383 if ($line[0] and $line[1]) {
384 $class = 'context';
385 $left = $right = $content;
386 } elseif ($line[0]) {
387 $class = 'removed';
388 $left = $content;
389 } else {
390 $class = 'added';
391 $right = $content;
392 }
393
394 $left_offsets[] = sprintf('<td>%s</td>', $line[0]);
395 $right_offsets[] = sprintf('<td>%s</td>', $line[1]);
396 $left_contents[] = sprintf('<td class="%s%s mono">%s</td>', $class, $pretty, $left);
397 $right_contents[] = sprintf('<td class="%s%s mono">%s</td>', $class, $pretty, $right);
398
399 $max_lineno_left = max($max_lineno_left, $line[0]);
400 $max_lineno_right = max($max_lineno_right, $line[1]);
401 }
402
403 if (count($chunks) > $cc) {
404 $left_offsets[] = '<td class="next">...</td>';
405 $right_offsets[] = '<td class="next">...</td>';
406 $left_contents[] = '<td></td>';
407 $right_contents[] = '<td></td>';
408 }
409 $cc++;
410 }
411
412 $leftwidth = 1;
413 if ($max_lineno_left > 0)
414 $leftwidth = ((ceil(log10($max_lineno_left)) + 1) * 8) + 17;
415
416 $rightwidth = 1;
417 if ($max_lineno_right > 0)
418 $rightwidth = ((ceil(log10($max_lineno_right)) + 1) * 8) + 17;
419
420 $inner_linecounts_left =
421 '<table class="diff-linecounts">' ."\n".
422 '<colgroup><col width="'.$leftwidth.'" /></colgroup>' ."\n".
423 '<tr class="line">' .
424 implode('</tr>'."\n".'<tr class="line">', $left_offsets).
425 '</tr>' ."\n".
426 '</table>' ."\n";
427
428 $inner_linecounts_right =
429 '<table class="diff-linecounts">' ."\n".
430 '<colgroup><col width="'.$rightwidth.'" /></colgroup>' ."\n".
431 '<tr class="line">' .
432 implode('</tr>'."\n".'<tr class="line">', $right_offsets).
433 '</tr>' ."\n".
434 '</table>' ."\n";
435
436 $inner_contents_left =
437 '<table class="diff-contents">' ."\n".
438 '<tr class="line">' .
439 implode('</tr>'."\n".'<tr class="line">', $left_contents) .
440 '</tr>' ."\n".
441 '</table>' ."\n";
442
443 $inner_contents_right =
444 '<table class="diff-contents">' ."\n".
445 '<tr class="line">' .
446 implode('</tr>'."\n".'<tr class="line">', $right_contents) .
447 '</tr>' ."\n".
448 '</table>' ."\n";
449
450 $out =
451 '<table class="diff context">' ."\n".
452 '<colgroup>' .
453 '<col width="'.($leftwidth + 1).'" /><col width="*" />' .
454 '<col width="'.($rightwidth + 1).'" /><col width="*" />' .
455 '</colgroup>' ."\n".
456 '<tr id="diff-'.md5($filename).'">'.
457 '<th colspan="4">'.Pluf_esc($filename).'</th>'.
458 '</tr>' ."\n".
459 '<tr>' .
460 '<th colspan="2">'.__('Old').'</th><th colspan="2">'.__('New').'</th>' .
461 '</tr>'.
462 '<tr>' .
463 '<td>'. $inner_linecounts_left .'</td>'. "\n".
464 '<td><div class="scroll">'. $inner_contents_left .'</div></td>'. "\n".
465 '<td>'. $inner_linecounts_right .'</td>'. "\n".
466 '<td><div class="scroll">'. $inner_contents_right .'</div></td>'. "\n".
467 '</tr>' ."\n".
468 '</table>' ."\n";
469
470 return Pluf_Template::markSafe($out);
471 }
472}
473

Archive Download this file