Added multi file support in diffs
[viewsvn.git] / includes / svnlib.php
1 <?php
2 /*=====================================================================*\
3 || ################################################################### ||
4 || # ViewSVN [#]version[#]
5 || # --------------------------------------------------------------- # ||
6 || # Copyright ©2002-[#]year[#] by Iris Studios, Inc. All Rights Reserved. # ||
7 || # This file may not be reproduced in any way without permission. # ||
8 || # --------------------------------------------------------------- # ||
9 || # User License Agreement at http://www.iris-studios.com/license/ # ||
10 || ################################################################### ||
11 \*=====================================================================*/
12
13 /**
14 * Command line interface with the SVN commands
15 *
16 * @package ViewSVN
17 */
18
19 /**
20 * Interacts with the command line subsystem to
21 * access SVN information
22 *
23 * @package ViewSVN
24 * @version $Id$
25 */
26 class SVNLib
27 {
28 /**
29 * Path to the SVN binary
30 * @var string
31 */
32 var $svnpath;
33
34 /**
35 * Constructor: validate SVN path
36 *
37 * @param string Path to SVN binary
38 */
39 function SVNLib($svnpath)
40 {
41 global $viewsvn;
42
43 $this->svnpath = $viewsvn->shell->cmd($svnpath);
44
45 $access = $viewsvn->shell->exec($this->svnpath . ' --version');
46
47 if (!$access)
48 {
49 $viewsvn->trigger->error('svn binary could not be found');
50 }
51
52 if (!preg_match('#^svn, version (.*?)\)$#i', trim($access[0])))
53 {
54 $viewsvn->trigger->error('svn binary does not pass test');
55 }
56 }
57
58 /**
59 * Prepares data for output
60 *
61 * @access public
62 *
63 * @param string Standard data
64 *
65 * @return string Output-ready data
66 */
67 function format($string)
68 {
69 // convert entities
70 $string = htmlspecialchars($string);
71
72 // tabs to 5 spaces
73 $string = str_replace("\t", ' ', $string);
74
75 // spaces to nbsp
76 $string = str_replace(' ', '&nbsp;', $string);
77
78 // convert advanced diff
79 $string = str_replace(array('{@++}', '{@--}'), array('<span class="diff_add">', '<span class="diff_del">'), $string);
80 $string = str_replace(array('{/@++}', '{/@--}'), '</span>', $string);
81
82 // nl2br
83 $string = nl2br($string);
84
85 return $string;
86 }
87
88 /**
89 * Executes the SVN binary
90 *
91 * @access private
92 *
93 * @param string Command
94 *
95 * @return array Output
96 */
97 function svn($command)
98 {
99 global $viewsvn;
100
101 return $viewsvn->shell->exec($this->svnpath . ' ' . $command . ' 2>&1');
102 }
103
104 /**
105 * SVN Wrapper: standard command system
106 *
107 * @access private
108 *
109 * @param string SVN command
110 * @param string Repository
111 * @param string Path
112 * @param integer Revision
113 *
114 * @return array Lines of output
115 */
116 function std($command, $repos, $path, $revision)
117 {
118 global $viewsvn;
119
120 $revision = $this->rev($revision);
121 $repospath = $viewsvn->repos->fetch_path($repos, false);
122
123 return $this->svn($command . ' -r' . $revision . ' ' . $repospath . $path);
124 }
125
126 /**
127 * SVN Wrapper: blame
128 *
129 * @access protected
130 *
131 * @param string Repository
132 * @param string Path
133 * @param integer Revision
134 *
135 * @return array Lines of blame output
136 */
137 function blame($repos, $path, $revision)
138 {
139 return $this->std('blame', $repos, $path, $revision);
140 }
141
142 /**
143 * SVN Wrapper: cat
144 *
145 * @access protected
146 *
147 * @param string Repository
148 * @param string Path
149 * @param integer Revision
150 *
151 * @return array Lines of cat output
152 */
153 function cat($repos, $path, $revision)
154 {
155 global $viewsvn;
156
157 $repospath = $viewsvn->repos->fetch_path($repos, false);
158
159 return $this->svn('cat ' . $repospath . $path . '@' . $revision);
160 }
161
162 /**
163 * SVN Wrapper: diff
164 *
165 * @access protected
166 *
167 * @param string Repository
168 * @param string Path
169 * @param integer Lower revision
170 * @param integer Higher revision
171 *
172 * @return array Lines of diff output
173 */
174 function diff($repos, $path, $lorev, $hirev)
175 {
176 global $viewsvn;
177
178 $hirev = $this->rev($hirev);
179 $lorev = $this->rev($lorev);
180 if ($lorev == 'HEAD')
181 {
182 $lorev = 0;
183 }
184
185 if (is_integer($hirev) AND is_integer($lorev))
186 {
187 if ($lorev > $hirev)
188 {
189 $lorev = $hirev - 1;
190 }
191 if ($lorev == $hirev)
192 {
193 $lorev = 0;
194 }
195 }
196
197 $repospath = $viewsvn->repos->fetch_path($repos, false);
198
199 return $this->svn('diff -r' . $lorev . ':' . $hirev . ' ' . $repospath . $path);
200 }
201
202 /**
203 * SVN Wrapper: log
204 *
205 * @access protected
206 *
207 * @param string Repository
208 * @param string Path
209 * @param integer Lower revision
210 * @param integer Higher revision
211 *
212 * @return array Lines of log output
213 */
214 function log($repos, $path, $lorev, $hirev)
215 {
216 global $viewsvn;
217
218 $hirev = $this->rev($hirev);
219 $lorev = $this->rev($hirev);
220 if ($lorev == 'HEAD')
221 {
222 $lorev = 0;
223 }
224
225 if (is_integer($hirev) AND is_integer($lorev))
226 {
227 if ($lorev > $hirev)
228 {
229 $lorev = $hirev - 1;
230 }
231 if ($lorev == $hirev)
232 {
233 $lorev = 0;
234 }
235 }
236
237 $repospath = $viewsvn->repos->fetch_path($repos, false);
238
239 return $this->svn('log -r' . $hirev . ':' . $lorev . ' ' . $repospath . $path);
240 }
241
242 /**
243 * SVN Wrapper: ls (list)
244 *
245 * @access protected
246 *
247 * @param string Repository
248 * @param string Path
249 * @param integer Revision
250 *
251 * @return array Lines of list output
252 */
253 function ls($repos, $path, $revision)
254 {
255 return $this->std('list', $repos, $path, $revision);
256 }
257
258 /**
259 * Generates a clean revision number
260 *
261 * @access public
262 *
263 * @param integer Revision number
264 *
265 * @return mixed Cleaned revision or HEAD
266 */
267 function rev($revision)
268 {
269 if (($revision = intval($revision)) < 1)
270 {
271 $revision = 'HEAD';
272 }
273 return $revision;
274 }
275 }
276
277 /**
278 * Annotation/blame system; constructs an array
279 * that is ready for output
280 *
281 * @package ViewSVN
282 * @version $Id$
283 */
284 class SVNBlame
285 {
286 /**
287 * Array of blame information
288 * @var array
289 */
290 var $blame = array();
291
292 /**
293 * Raw "svn blame" output
294 * @var array
295 */
296 var $rawoutput;
297
298 /**
299 * Constructor: create blame and store data
300 *
301 * @param string Repository
302 * @param string Path
303 * @param integer Revision
304 */
305 function SVNBlame($repos, $path, $revision)
306 {
307 global $viewsvn;
308
309 $this->rawoutput = $viewsvn->svn->blame($repos, $path, $revision);
310 $this->process();
311 }
312
313 /**
314 * Returns blame for display
315 *
316 * @access public
317 *
318 * @return array Blame data
319 */
320 function fetch()
321 {
322 return $this->blame;
323 }
324
325 /**
326 * Parses the blame data
327 *
328 * @access private
329 */
330 function process()
331 {
332 $lineno = 1;
333
334 foreach ($this->rawoutput AS $line)
335 {
336 if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)\s(.*)$#', $line, $matches))
337 {
338 $this->blame[] = array(
339 'rev' => $matches[1],
340 'author' => $matches[2],
341 'line' => $matches[3],
342 'lineno' => $lineno++
343 );
344 }
345 // a blank line
346 else if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)$#', $line, $matches))
347 {
348 $this->blame[] = array(
349 'rev' => $matches[1],
350 'author' => $matches[2],
351 'line' => '',
352 'lineno' => $lineno++
353 );
354 }
355 }
356 }
357 }
358
359 /**
360 * Log management system; creates a complex list
361 * of SVN log information
362 *
363 * @package ViewSVN
364 * @version $Id$
365 */
366 class SVNLog
367 {
368 /**
369 * Array of logs
370 * @var array
371 */
372 var $logs = array();
373
374 /**
375 * Raw "svn log" output
376 * @var array
377 */
378 var $rawoutput;
379
380 /**
381 * Constructor: create log store for the given file
382 *
383 * @param string Repository
384 * @param string Path
385 * @param integer Lower revision
386 * @param integer Higher revision
387 */
388 function SVNLog($repos, $path, $lorev, $hirev)
389 {
390 global $viewsvn;
391
392 $this->rawoutput = $viewsvn->svn->log($repos, $path, $lorev, $hirev);
393 $this->process();
394 }
395
396 /**
397 * Returns logs for display
398 *
399 * @access public
400 *
401 * @return array Log data
402 */
403 function fetch()
404 {
405 return $this->logs;
406 }
407
408 /**
409 * Splits up the raw output into a usable log
410 *
411 * @access private
412 */
413 function process()
414 {
415 $lastrev = 0;
416
417 for ($i = 1; $i <= count($this->rawoutput) - 1; $i++)
418 {
419 $line = $this->rawoutput["$i"];
420
421 if (preg_match('#^r([0-9]*) \| (.*?) \| (....-..-.. ..:..:..) ([0-9\-]*) \((.*?)\) \| ([0-9]*) lines?$#', $line, $matches))
422 {
423 if (isset($this->logs["$lastrev"]))
424 {
425 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
426 }
427
428 $this->logs["$matches[1]"] = array(
429 'rev' => $matches[1],
430 'author' => $matches[2],
431 'date' => $matches[3],
432 'timezone' => $matches[4],
433 'lines' => $matches[6],
434 'message' => ''
435 );
436
437 $lastrev = $matches[1];
438 }
439 else
440 {
441 $this->logs["$lastrev"]['message'] .= $line . "\n";
442 }
443 }
444
445 if (isset($this->logs["$lastrev"]))
446 {
447 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
448 }
449 }
450
451 /**
452 * Trims the last dash line off a message
453 *
454 * @access private
455 *
456 * @param string Message with annoying-ass line
457 *
458 * @return string Clean string
459 */
460 function strip_last_line($string)
461 {
462 return trim(preg_replace("#\n(.*?)\n$#", '', $string));
463 }
464 }
465
466 /**
467 * Diff system; constructs a diff array that
468 * is ready for output
469 *
470 * @package ViewSVN
471 */
472 class SVNDiff
473 {
474 /**
475 * Array of diff information
476 * @var array
477 */
478 var $diff = array();
479
480 /**
481 * Raw "svn diff" output
482 * @var array
483 */
484 var $rawoutput;
485
486 /**
487 * Constructor: create and store diff data
488 *
489 * @param string Repository
490 * @param string Path
491 * @param integer Lower revision
492 * @param integer Higher revision
493 */
494 function SVNDiff($repos, $path, $lorev, $hirev)
495 {
496 global $viewsvn;
497
498 $this->rawoutput = $viewsvn->svn->diff($repos, $path, $lorev, $hirev);
499 $this->process();
500 }
501
502 /**
503 * Returns diffs for display
504 *
505 * @access public
506 *
507 * @return array Diff data
508 */
509 function fetch()
510 {
511 return $this->diff;
512 }
513
514 /**
515 * Processes and prepares diff data
516 *
517 * @access private
518 */
519 function process()
520 {
521 $chunk = 0;
522 $indexcounter = null;
523
524 $lastact = '';
525 $lastcontent = '';
526
527 foreach ($this->rawoutput AS $line)
528 {
529 if (preg_match('#^@@ \-([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@$#', $line, $bits))
530 {
531 $lastact = '';
532 $lastcontent = '';
533
534 $this->diff["$index"][ ++$chunk ]['hunk'] = array('old' => array('line' => $bits[1], 'count' => $bits[2]), 'new' => array('line' => $bits[3], 'count' => $bits[4]));
535 $lines['old'] = $this->diff["$index"]["$chunk"]['hunk']['old']['line'] - 1;
536 $lines['new'] = $this->diff["$index"]["$chunk"]['hunk']['new']['line'] - 1;
537 continue;
538 }
539
540 if ($indexcounter <= 5 AND $indexcounter !== null)
541 {
542 $indexcounter++;
543 continue;
544 }
545 else if ($indexcounter == 5)
546 {
547 $indexcounter = null;
548 continue;
549 }
550
551 if (preg_match('#^([\+\- ])(.*)#', $line, $matches))
552 {
553 $act = $matches[1];
554 $content = $matches[2];
555
556 if ($act == ' ')
557 {
558 $this->diff["$index"]["$chunk"][] = array(
559 'line' => $content,
560 'act' => '',
561 'oldlineno' => ++$lines['old'],
562 'newlineno' => ++$lines['new']
563 );
564 }
565 else if ($act == '+')
566 {
567 // potential line delta
568 if ($lastact == '-')
569 {
570 if ($delta = @$this->fetch_diff_extent($lastcontent, $content))
571 {
572 // create two sets of ends for the two contents
573 $delta['endo'] = strlen($lastcontent) - $delta['end'];
574 $delta['endn'] = strlen($content) - $delta['end'];
575
576 $diffo = $delta['endo'] - $delta['start'];
577 $diffn = $delta['endn'] - $delta['start'];
578
579 if (strlen($lastcontent) > $delta['endo'] - $diffo)
580 {
581 $removed = substr($lastcontent, $delta['start'], $diffo);
582 $this->diff["$index"]["$chunk"][ count($this->diff["$index"]["$chunk"]) - 2 ]['line'] = substr_replace($lastcontent, '{@--}' . $removed . '{/@--}', $delta['start'], $diffo);
583 }
584
585 if (strlen($content) > $delta['endn'] - $diffn)
586 {
587 $added = substr($content, $delta['start'], $diffn);
588 $content = substr_replace($content, '{@++}' . $added . '{/@++}', $delta['start'], $diffn);
589 }
590 }
591 }
592
593 $this->diff["$index"]["$chunk"][] = array(
594 'line' => $content,
595 'act' => '+',
596 'oldlineno' => '',
597 'newlineno' => ++$lines['new']
598 );
599 }
600 else if ($act == '-')
601 {
602 $lastcontent = $content;
603
604 $this->diff["$index"]["$chunk"][] = array(
605 'line' => $content,
606 'act' => '-',
607 'oldlineno' => ++$lines['old'],
608 'newlineno' => ''
609 );
610 }
611
612 $lastact = $act;
613 }
614 // whitespace lines
615 else
616 {
617 if (preg_match('#^Index: (.*?)$#', $line, $matches))
618 {
619 $index = $matches[1];
620 $indexcounter = 1;
621 $chunk = 0;
622 continue;
623 }
624
625 $lastact = '';
626
627 $this->diff["$index"]["$chunk"][] = array(
628 'line' => '',
629 'act' => '',
630 'oldlineno' => ++$lines['old'],
631 'newlineno' => ++$lines['new']
632 );
633 }
634 }
635 }
636
637 /**
638 * Returns the amount of change that occured
639 * between two lines
640 *
641 * @access private
642 *
643 * @param string Old line
644 * @param string New line
645 *
646 * @return array Difference of positions
647 */
648 function fetch_diff_extent($old, $new)
649 {
650 $start = 0;
651 $min = min(strlen($old), strlen($new));
652
653 for ($start = 0; $start < $min; $start++)
654 {
655 if ($old{"$start"} != $new{"$start"})
656 {
657 break;
658 }
659 }
660
661 $max = max(strlen($old), strlen($new));
662
663 for ($end = 0; $end < $min; $end++)
664 {
665 $oldpos = strlen($old) - $end;
666 $newpos = strlen($new) - $end;
667
668 if ($old{"$oldpos"} != $new{"$newpos"})
669 {
670 break;
671 }
672 }
673
674 $end--;
675
676 if ($start == 0 AND $end == $max)
677 {
678 return false;
679 }
680
681 return array('start' => $start, 'end' => $end);
682 }
683 }
684
685 /*=====================================================================*\
686 || ###################################################################
687 || # $HeadURL$
688 || # $Id$
689 || ###################################################################
690 \*=====================================================================*/
691 ?>