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 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