Path types no more!
[viewsvn.git] / includes / svnlib.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # ViewSVN [#]version[#]
5 || # Copyright ©2002-[#]year[#] Iris Studios, Inc.
6 || #
7 || # This program is free software; you can redistribute it and/or modify
8 || # it under the terms of the GNU General Public License as published by
9 || # the Free Software Foundation; version [#]gpl[#] of the License.
10 || #
11 || # This program is distributed in the hope that it will be useful, but
12 || # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 || # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 || # more details.
15 || #
16 || # You should have received a copy of the GNU General Public License along
17 || # with this program; if not, write to the Free Software Foundation, Inc.,
18 || # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 || ###################################################################
20 \*=====================================================================*/
21
22 /**
23 * Command line library and SVN-PHP bridge extension
24 *
25 * @package ViewSVN
26 */
27
28 /**
29 * This class acts as the bridge between the command line Xquery class
30 * and the various display files. It handles preparation of Xquery
31 * code.
32 *
33 * @package ViewSVN
34 * @version $Id$
35 */
36 class SVNLib
37 {
38 /**
39 * Path to the SVN binary
40 * @var string
41 * @access private
42 */
43 var $svnpath;
44
45 /**
46 * Controller
47 * @var object
48 * @access private
49 */
50 var $controller;
51
52 // ###################################################################
53 /**
54 * Constructor: validate SVN path
55 *
56 * @param object Controller
57 */
58 function SVNLib(&$controller)
59 {
60 $this->controller =& $controller;
61
62 $this->svnpath =& $this->controller->xquery->cmd($this->controller->registry->svnpath);
63
64 $access = $this->controller->xquery->exec($this->svnpath . ' --version');
65
66 if (!$access)
67 {
68 $this->controller->registry->trigger->error($this->controller->registry->lang->string('The SVN binary could not be located'));
69 }
70
71 if (!preg_match('#^svn, version (.*?)\)$#i', trim($access[0])))
72 {
73 $this->controller->registry->trigger->error($this->controller->registry->lang->string('The SVN binary does not appear to be valid (it failed our tests)'));
74 }
75 }
76
77 // ###################################################################
78 /**
79 * Executes the SVN binary
80 *
81 * @access private
82 *
83 * @param string Command
84 *
85 * @return array Output
86 */
87 function svn($command)
88 {
89 $output = $this->controller->xquery->exec($this->svnpath . ' ' . $command . ' 2>&1');
90
91 // make sure that we keep escaped chars
92 //$output = str_replace(array('\t', '\n', '\r'), array('\\\t', '\\\n', '\\\r'), $output);
93 //$output = preg_replace('#\\\(.)#', '\\\\\\\\' . '\1', $output);
94 //$output = str_replace('\\', '\\\\', $output);
95
96 $temp = implode("\n", $output);
97 if (strpos($temp, '(apr' . '_err=') !== false)
98 {
99 $this->controller->registry->trigger->error(nl2br($temp));
100 }
101 return $output;
102 }
103
104 // ###################################################################
105 /**
106 * This function is used to prepare a common command. It sends the
107 * specified command along with the repository path and the current
108 * node. The revision is either fectched from the argument list or
109 * from the node, too.
110 *
111 * @access public
112 *
113 * @param string SVN command
114 * @param bool Alternate revision
115 *
116 * @return array Lines of output
117 */
118 function command($command, $rev = false)
119 {
120 global $viewsvn;
121
122 $revision = ($rev !== false ? $rev : SVNCommon::rev($this->controller->revnum));
123
124 return $this->svn($command . ' ' . $this->controller->repospath . $this->controller->path . ($revision !== null ? '@' . $revision : ''));
125 }
126
127 // ###################################################################
128 /**
129 * A library complicator function that handles the diff command because
130 * the standard command() system does not work with more complex args.
131 *
132 * @access public
133 *
134 * @param integer Lower revision
135 * @param integer Higher revision
136 *
137 * @return array Lines of diff output
138 */
139 function diff($lorev, $hirev)
140 {
141 global $viewsvn;
142
143 $hirev = SVNCommon::rev($hirev);
144 $lorev = SVNCommon::rev($lorev);
145 if ($lorev == 'HEAD')
146 {
147 $lorev = 1;
148 }
149
150 if (is_integer($hirev) AND is_integer($lorev))
151 {
152 if ($lorev > $hirev)
153 {
154 $lorev = $hirev - 1;
155 }
156 if ($lorev == $hirev)
157 {
158 $lorev = 0;
159 }
160 }
161
162 return $this->svn('diff -r' . $lorev . ':' . $hirev . ' ' . $this->controller->repospath . $this->controller->path);
163 }
164
165 // ###################################################################
166 /**
167 * A library complicator function to create log output. This is needed
168 * because command() doesn't handle ranged revisions.
169 *
170 * @access public
171 *
172 * @param string Repository
173 * @param string Path
174 * @param integer Lower revision
175 * @param integer Higher revision
176 *
177 * @return array Lines of log output
178 */
179 function log($lorev, $hirev)
180 {
181 global $viewsvn;
182
183 $hirev = $this->rev($hirev);
184 $lorev = $this->rev($hirev);
185 if ($lorev == 'HEAD')
186 {
187 $lorev = 0;
188 }
189
190 if (is_integer($hirev) AND is_integer($lorev))
191 {
192 if ($lorev > $hirev)
193 {
194 $lorev = $hirev - 1;
195 }
196 if ($lorev == $hirev)
197 {
198 $lorev = 0;
199 }
200 }
201
202 $repospath = $viewsvn->repos->fetch_path($repos, false);
203
204 return $this->svn('log -v -r' . $hirev . ':' . $lorev . ' ' . $repospath . $path);
205 }
206 }
207
208 /**
209 * Annotation/blame system; constructs an array that is ready for output
210 *
211 * @package ViewSVN
212 * @version $Id$
213 */
214 class SVNBlame
215 {
216 /**
217 * Array of blame information
218 * @var array
219 * @access private
220 */
221 var $blame = array();
222
223 /**
224 * Raw "svn blame" output
225 * @var array
226 * @access private
227 */
228 var $rawoutput;
229
230 // ###################################################################
231 /**
232 * Constructor: create blame and store data
233 *
234 * @param object Controller
235 */
236 function SVNBlame(&$controller)
237 {
238 $this->rawoutput = $controller->library->command('blame');
239 $this->process();
240 }
241
242 // ###################################################################
243 /**
244 * Returns blame for display
245 *
246 * @access public
247 *
248 * @return array Blame data
249 */
250 function fetch()
251 {
252 return $this->blame;
253 }
254
255 // ###################################################################
256 /**
257 * Parses the blame data
258 *
259 * @access private
260 */
261 function process()
262 {
263 $lineno = 1;
264
265 foreach ($this->rawoutput AS $line)
266 {
267 if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)\s(.*)$#', $line, $matches))
268 {
269 $this->blame[] = array(
270 'rev' => $matches[1],
271 'author' => $matches[2],
272 'line' => $matches[3],
273 'lineno' => $lineno++
274 );
275 }
276 // a blank line
277 else if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)$#', $line, $matches))
278 {
279 $this->blame[] = array(
280 'rev' => $matches[1],
281 'author' => $matches[2],
282 'line' => '',
283 'lineno' => $lineno++
284 );
285 }
286 }
287 }
288 }
289
290 /**
291 * Log management system; creates a complex list of SVN log information
292 *
293 * @package ViewSVN
294 * @version $Id$
295 */
296 class SVNLog
297 {
298 /**
299 * Array of logs
300 * @var array
301 * @access private
302 */
303 var $logs = array();
304
305 /**
306 * Raw "svn log" output
307 * @var array
308 * @access private
309 */
310 var $rawoutput;
311
312 // ###################################################################
313 /**
314 * Constructor: create log store for the given file
315 *
316 * @param object Controller
317 */
318 function SVNLog(&$controller)
319 {
320 $this->rawoutput = $controller->library->command('log', null);
321 $this->process();
322 }
323
324 // ###################################################################
325 /**
326 * Returns logs for display
327 *
328 * @access public
329 *
330 * @return array Log data
331 */
332 function fetch()
333 {
334 return $this->logs;
335 }
336
337 // ###################################################################
338 /**
339 * Splits up the raw output into a usable log
340 *
341 * @access private
342 */
343 function process()
344 {
345 $lastrev = 0;
346
347 for ($i = 1; $i <= sizeof($this->rawoutput) - 1; $i++)
348 {
349 $line = $this->rawoutput["$i"];
350
351 if (preg_match('#^r([0-9]*) \| (.*?) \| (....-..-.. ..:..:..) ([0-9\-]*) \((.*?)\) \| ([0-9]*) lines?$#', $line, $matches))
352 {
353 if (isset($this->logs["$lastrev"]))
354 {
355 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
356 }
357
358 $this->logs["$matches[1]"] = array(
359 'rev' => $matches[1],
360 'author' => $matches[2],
361 'date' => $matches[3],
362 'timezone' => $matches[4],
363 'lines' => $matches[6],
364 'message' => ''
365 );
366
367 $lastrev = $matches[1];
368 }
369 else if (preg_match('#^\s+([ADMR])\s(.*)#', $line, $matches))
370 {
371 if (preg_match('#(.*) \(from (.*?)\)$#', $matches[2], $amatches))
372 {
373 $matches[2] = $amatches[1];
374 }
375
376 $this->logs["$lastrev"]['files'][] = array(
377 'action' => $matches[1],
378 'file' => trim($matches[2]),
379 'from' => (isset($amatches[2]) ? $amatches[2] : '')
380 );
381 }
382 else
383 {
384 if (trim($line) != 'Changed paths:')
385 {
386 $this->logs["$lastrev"]['message'] .= $line . "\n";
387 }
388 }
389 }
390
391 if (isset($this->logs["$lastrev"]))
392 {
393 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
394 }
395 }
396
397 // ###################################################################
398 /**
399 * Trims the last dash line off a message
400 *
401 * @access private
402 *
403 * @param string Message with annoying-ass line
404 *
405 * @return string Clean string
406 */
407 function strip_last_line($string)
408 {
409 return trim(preg_replace("#\n(.*?)\n$#", '', $string));
410 }
411 }
412
413 /**
414 * Diff system; constructs a diff array that is ready for output
415 *
416 * @package ViewSVN
417 * @version $Id$
418 */
419 class SVNDiff
420 {
421 /**
422 * Array of diff information
423 * @var array
424 * @access private
425 */
426 var $diff = array();
427
428 /**
429 * Raw "svn diff" output
430 * @var array
431 * @access private
432 */
433 var $rawoutput;
434
435 // ###################################################################
436 /**
437 * Constructor: create and store diff data
438 *
439 * @param object Controller
440 * @param integer Lower revision
441 * @param integer Higher revision
442 */
443 function SVNDiff(&$controller, $lorev, $hirev)
444 {
445 $this->rawoutput = $controller->library->diff($lorev, $hirev);
446 $this->process();
447 }
448
449 // ###################################################################
450 /**
451 * Returns diffs for display
452 *
453 * @access public
454 *
455 * @return array Diff data
456 */
457 function fetch()
458 {
459 return $this->diff;
460 }
461
462 // ###################################################################
463 /**
464 * Processes and prepares diff data
465 *
466 * @access private
467 */
468 function process()
469 {
470 global $viewsvn;
471
472 $chunk = 0;
473 $indexcounter = null;
474 $curprop = '';
475
476 $delstack = array();
477
478 foreach ($this->rawoutput AS $line)
479 {
480 if (preg_match('#^@@ \-([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@$#', $line, $bits))
481 {
482 $property = false;
483 $delstack = array();
484 $this->diff["$index"][ ++$chunk ]['hunk'] = array('old' => array('line' => $bits[1], 'count' => $bits[2]), 'new' => array('line' => $bits[3], 'count' => $bits[4]));
485 $lines['old'] = $this->diff["$index"]["$chunk"]['hunk']['old']['line'] - 1;
486 $lines['new'] = $this->diff["$index"]["$chunk"]['hunk']['new']['line'] - 1;
487 continue;
488 }
489 else if (preg_match('#^Property changes on: (.*?)$#', $line, $bits))
490 {
491 $property = true;
492 $index = $bits[1];
493 $this->diff["$index"]['props'] = array();
494 continue;
495 }
496
497 if ($indexcounter <= 3 AND $indexcounter !== null)
498 {
499 $indexcounter++;
500 continue;
501 }
502 else if ($indexcounter == 3)
503 {
504 $indexcounter = null;
505 continue;
506 }
507
508 if (preg_match('#^([\+\- ])(.*)#', $line, $matches) AND !$property)
509 {
510 $act = $matches[1];
511 $content = $matches[2];
512
513 if ($act == ' ')
514 {
515 $this->diff["$index"]["$chunk"][] = array(
516 'line' => $content,
517 'act' => '',
518 'oldlineno' => ++$lines['old'],
519 'newlineno' => ++$lines['new']
520 );
521
522 $delstack = array();
523 }
524 else if ($act == '+')
525 {
526 // potential line delta
527 if (sizeof($delstack) > 0)
528 {
529 $lastline = array_shift($delstack);
530
531 if ($delta = @$this->fetch_diff_extent($lastline['line'], $content))
532 {
533 if (strlen($lastline['line']) > ($delta['start'] - $delta['end']))
534 {
535 $end = strlen($lastline['line']) + $delta['end'];
536 $viewsvn->debug("RM delta- = " . $end);
537 $change = '{@-' . '-}' . substr($lastline['line'], $delta['start'], $end - $delta['start']) . '{/@-' . '-}';
538 $this->diff["$index"]["$chunk"]["$lastline[INDEX]"]['line'] = substr($lastline['line'], 0, $delta['start']) . $change . substr($lastline['line'], $end);
539 }
540
541 if (strlen($content) > $delta['start'] - $delta['end'])
542 {
543 $end = strlen($content) + $delta['end'];
544 $viewsvn->debug("MK delta+ = " . $end);
545 $change = '{@+' . '+}' . substr($content, $delta['start'], $end - $delta['start']) . '{/@+' . '+}';
546 $content = substr($content, 0, $delta['start']) . $change . substr($content, $end);
547 }
548 }
549 }
550
551 $this->diff["$index"]["$chunk"][] = array(
552 'line' => $content,
553 'act' => '+',
554 'oldlineno' => '',
555 'newlineno' => ++$lines['new']
556 );
557 }
558 else if ($act == '-')
559 {
560 $this->diff["$index"]["$chunk"][] = $thearray = array(
561 'line' => $content,
562 'act' => '-',
563 'oldlineno' => ++$lines['old'],
564 'newlineno' => ''
565 );
566
567 $key = sizeof($this->diff["$index"]["$chunk"]) - 2;
568 $thearray['INDEX'] = $key;
569
570 array_push($delstack, $thearray);
571 }
572 }
573 // whitespace lines
574 else
575 {
576 if (preg_match('#^Index: (.*?)$#', $line, $matches))
577 {
578 $index = $matches[1];
579 $indexcounter = 1;
580 $chunk = 0;
581 continue;
582 }
583
584 if ($property)
585 {
586 if (preg_match('#^__*_$#', trim($line)))
587 {
588 $viewsvn->debug("skipping: $line");
589 continue;
590 }
591
592 if (preg_match('#Name: (.*?)$#', $line, $matches))
593 {
594 $curprop = $matches[1];
595 $viewsvn->debug("prop: $curprop");
596 continue;
597 }
598 else
599 {
600 if (preg_match('#^\s+?\+(.*)#', $line, $matches))
601 {
602 $mode = 'add';
603 $this->diff["$index"]['props']["$curprop"]['add'] .= $matches[1];
604 }
605 else if (preg_match('#^\s+?\-(.*)#', $line, $matches))
606 {
607 $mode = 'del';
608 $this->diff["$index"]['props']["$curprop"]['del'] .= $matches[1];
609 }
610 else if (!preg_match('#^\s+[\+\- ](.*)#', $line) AND trim($line) != '')
611 {
612 $this->diff["$index"]['props']["$curprop"]["$mode"] .= "\n" . $line;
613 }
614 continue;
615 }
616 }
617
618 $this->diff["$index"]["$chunk"][] = array(
619 'line' => '',
620 'act' => '',
621 'oldlineno' => ++$lines['old'],
622 'newlineno' => ++$lines['new']
623 );
624
625 $delstack = array();
626 }
627 }
628 }
629
630 // ###################################################################
631 /**
632 * Returns the amount of change that occured between two lines
633 *
634 * @access private
635 *
636 * @param string Old line
637 * @param string New line
638 *
639 * @return array Difference of positions
640 */
641 function fetch_diff_extent($old, $new)
642 {
643 global $viewsvn;
644
645 $start = 0;
646 $min = min(strlen($old), strlen($new));
647
648 $viewsvn->debug("min1 = $min");
649
650 while ($start < $min AND $old["$start"] == $new["$start"])
651 {
652 $start++;
653 }
654
655 $end = -1;
656 $min = $min - $start;
657
658 $viewsvn->debug("min2 = $min");
659
660 $viewsvn->debug("checking: " . $old[ strlen($old) + $end ] . ' == ' . $new[ strlen($new) + $end ]);
661
662 while (-$end <= $min AND $old[ strlen($old) + $end ] == $new[ strlen($new) + $end ])
663 {
664 $end--;
665 }
666
667 return array('start' => $start, 'end' => $end + 1);
668 }
669 }
670
671 /*=====================================================================*\
672 || ###################################################################
673 || # $HeadURL$
674 || # $Id$
675 || ###################################################################
676 \*=====================================================================*/
677 ?>