Make sure we keep our escaped chars intact
[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 interface with the SVN commands
24 *
25 * @package ViewSVN
26 */
27
28 /**
29 * Interacts with the command line subsystem to
30 * access SVN information
31 *
32 * @package ViewSVN
33 * @version $Id$
34 */
35 class SVNLib
36 {
37 /**
38 * Path to the SVN binary
39 * @var string
40 */
41 var $svnpath;
42
43 /**
44 * Common command system
45 * @var object
46 */
47 var $common;
48
49 /**
50 * Constructor: validate SVN path
51 *
52 * @param string Path to SVN binary
53 */
54 function SVNLib($svnpath)
55 {
56 global $viewsvn;
57
58 $this->svnpath = $viewsvn->shell->cmd($svnpath);
59
60 $this->common =& new SVNCommon();
61
62 $access = $viewsvn->shell->exec($this->svnpath . ' --version');
63
64 if (!$access)
65 {
66 $viewsvn->trigger->error('svn binary could not be found');
67 }
68
69 if (!preg_match('#^svn, version (.*?)\)$#i', trim($access[0])))
70 {
71 $viewsvn->trigger->error('svn binary does not pass test');
72 }
73 }
74
75 /**
76 * Prepares data for output
77 *
78 * @access public
79 *
80 * @param string Standard data
81 *
82 * @return string Output-ready data
83 */
84 function format($string)
85 {
86 // convert entities
87 $string = htmlspecialchars($string);
88
89 // tabs to 5 spaces
90 $string = str_replace("\t", ' ', $string);
91
92 // spaces to nbsp
93 if (true)
94 {
95 $string = preg_replace('#( +)#e', '$this->format_spaces("\1")', $string);
96 }
97 // no word wrap
98 else
99 {
100 $string = str_replace(' ', '&nbsp;', $string);
101 }
102
103 // convert advanced diff
104 $string = str_replace(array('{@+' . '+}', '{@-' . '-}'), array('<span class="diff_add_bit">', '<span class="diff_del_bit">'), $string);
105 $string = str_replace(array('{/@+' . '+}', '{/@-' . '-}'), '</span>', $string);
106
107 // nl2br
108 $string = nl2br($string);
109
110 return $string;
111 }
112
113 /**
114 * Counts the spaces and replaces two or more ones
115 *
116 * @access private
117 *
118 * @param string Spaced string
119 *
120 * @return string &nbsp;'d string
121 */
122 function format_spaces($thestring)
123 {
124 if (strlen($thestring) >= 2)
125 {
126 $thestring = str_replace(' ', '&nbsp;', $thestring);
127 }
128
129 return $thestring;
130 }
131
132 /**
133 * Executes the SVN binary
134 *
135 * @access private
136 *
137 * @param string Command
138 *
139 * @return array Output
140 */
141 function svn($command)
142 {
143 global $viewsvn;
144
145 $output = $viewsvn->shell->exec($this->svnpath . ' ' . $command . ' 2>&1');
146
147 // make sure that we keep escaped chars
148 //$output = str_replace(array('\t', '\n', '\r'), array('\\\t', '\\\n', '\\\r'), $output);
149 $output = preg_replace('#\\\(.)#', '\\\\\\\\' . '\1', $output);
150
151 $temp = implode("\n", $output);
152 if (strpos($temp, '(apr' . '_err=') !== false)
153 {
154 $viewsvn->trigger->error(nl2br($temp));
155 }
156 return $output;
157 }
158
159 /**
160 * SVN Wrapper: standard command system
161 *
162 * @access private
163 *
164 * @param string SVN command
165 * @param string Repository
166 * @param string Path
167 * @param integer Revision
168 *
169 * @return array Lines of output
170 */
171 function std($command, $repos, $path, $revision)
172 {
173 global $viewsvn;
174
175 $revision = $this->rev($revision);
176 $repospath = $viewsvn->repos->fetch_path($repos, false);
177
178 return $this->svn($command . ' -r' . $revision . ' ' . $repospath . $path);
179 }
180
181 /**
182 * SVN Wrapper: blame
183 *
184 * @access protected
185 *
186 * @param string Repository
187 * @param string Path
188 * @param integer Revision
189 *
190 * @return array Lines of blame output
191 */
192 function blame($repos, $path, $revision)
193 {
194 return $this->std('blame', $repos, $path, $revision);
195 }
196
197 /**
198 * SVN Wrapper: cat
199 *
200 * @access protected
201 *
202 * @param string Repository
203 * @param string Path
204 * @param integer Revision
205 *
206 * @return array Lines of cat output
207 */
208 function cat($repos, $path, $revision)
209 {
210 return $this->std('cat', $repos, $path, $revision);
211 }
212
213 /**
214 * SVN Wrapper: diff
215 *
216 * @access protected
217 *
218 * @param string Repository
219 * @param string Path
220 * @param integer Lower revision
221 * @param integer Higher revision
222 *
223 * @return array Lines of diff output
224 */
225 function diff($repos, $path, $lorev, $hirev)
226 {
227 global $viewsvn;
228
229 $hirev = $this->rev($hirev);
230 $lorev = $this->rev($lorev);
231 if ($lorev == 'HEAD')
232 {
233 $lorev = 1;
234 }
235
236 if (is_integer($hirev) AND is_integer($lorev))
237 {
238 if ($lorev > $hirev)
239 {
240 $lorev = $hirev - 1;
241 }
242 if ($lorev == $hirev)
243 {
244 $lorev = 0;
245 }
246 }
247
248 $repospath = $viewsvn->repos->fetch_path($repos, false);
249
250 return $this->svn('diff -r' . $lorev . ':' . $hirev . ' ' . $repospath . $path);
251 }
252
253 /**
254 * SVN Wrapper: log
255 *
256 * @access protected
257 *
258 * @param string Repository
259 * @param string Path
260 * @param integer Lower revision
261 * @param integer Higher revision
262 *
263 * @return array Lines of log output
264 */
265 function log($repos, $path, $lorev, $hirev)
266 {
267 global $viewsvn;
268
269 $hirev = $this->rev($hirev);
270 $lorev = $this->rev($hirev);
271 if ($lorev == 'HEAD')
272 {
273 $lorev = 0;
274 }
275
276 if (is_integer($hirev) AND is_integer($lorev))
277 {
278 if ($lorev > $hirev)
279 {
280 $lorev = $hirev - 1;
281 }
282 if ($lorev == $hirev)
283 {
284 $lorev = 0;
285 }
286 }
287
288 $repospath = $viewsvn->repos->fetch_path($repos, false);
289
290 return $this->svn('log -v -r' . $hirev . ':' . $lorev . ' ' . $repospath . $path);
291 }
292
293 /**
294 * SVN Wrapper: ls (list)
295 *
296 * @access protected
297 *
298 * @param string Repository
299 * @param string Path
300 * @param integer Revision
301 *
302 * @return array Lines of list output
303 */
304 function ls($repos, $path, $revision)
305 {
306 return $this->std('list', $repos, $path, $revision);
307 }
308
309 /**
310 * Generates a clean revision number
311 *
312 * @access public
313 *
314 * @param integer Revision number
315 *
316 * @return mixed Cleaned revision or HEAD
317 */
318 function rev($revision)
319 {
320 if (($revision = intval($revision)) < 1)
321 {
322 $revision = 'HEAD';
323 }
324 return $revision;
325 }
326 }
327
328 /**
329 * Commonly executed SVN commands that return data
330 * used in many parts of the system
331 *
332 * @package ViewSVN
333 * @version $Id$
334 */
335 class SVNCommon
336 {
337 /**
338 * Registry object
339 * @var object
340 */
341 var $registry;
342
343 /**
344 * List of revisions
345 * @var array
346 */
347 var $revisions;
348
349 /**
350 * List of logs
351 * @var array
352 */
353 var $logs;
354
355 /**
356 * Constructor: bind with registry
357 */
358 function SVNCommon()
359 {
360 global $viewsvn;
361
362 $this->registry =& $viewsvn;
363 }
364
365 /**
366 * Checks to see if the given universal path is
367 * a directory
368 *
369 * @access public
370 *
371 * @param string Universal path
372 *
373 * @return bool Directory or not
374 */
375 function isdir($path)
376 {
377 $output = $this->registry->svn->std('info', $this->registry->paths->fetch_repos($path), $this->registry->paths->fetch_path($path), 'HEAD');
378
379 foreach ($output AS $line)
380 {
381 if (preg_match('#^Node Kind: (.*)#', $line, $matches))
382 {
383 if (trim(strtolower($matches[1])) == 'directory')
384 {
385 return true;
386 }
387 }
388 }
389
390 return false;
391 }
392
393 /**
394 * Get a list of revisions for a path
395 *
396 * @access public
397 *
398 * @param string Universal path
399 *
400 * @return array Key revisions
401 */
402 function fetch_revs($path)
403 {
404 if (!isset($this->revisions["$path"]))
405 {
406 $log = $this->fetch_logs($path);
407
408 $revs = array_keys($log);
409
410 $this->revisions["$path"] = array(
411 'HEAD' => $revs[0],
412 'START' => $revs[ count($revs) - 1 ],
413 'revs' => $revs
414 );
415 }
416
417 return $this->revisions["$path"];
418 }
419
420 /**
421 * Gets the revision that is marked as HEAD
422 *
423 * @access public
424 *
425 * @param string Universal path
426 *
427 * @return integer Revision
428 */
429 function fetch_head_rev($path)
430 {
431 $revs = $this->fetch_revs($path);
432 return $revs['HEAD'];
433 }
434
435 /**
436 * Returns the previous revision to the one given
437 *
438 * @access public
439 *
440 * @param string Universal path
441 * @param integer Arbitrary revision
442 *
443 * @return integer Previous revision (-1 if none)
444 */
445 function fetch_prev_rev($path, $current)
446 {
447 $revs = $this->fetch_revs($path);
448
449 if ($current == 'HEAD')
450 {
451 $current = $this->fetch_head_rev($path);
452 }
453
454 $index = array_search($current, $revs['revs']);
455 if ($current === false)
456 {
457 $message->trigger->error('revision ' . $current . ' is not in ' . $path);
458 }
459
460 if (isset($revs['revs'][ $index + 1 ]))
461 {
462 return $revs['revs'][ $index + 1 ];
463 }
464 else
465 {
466 return -1;
467 }
468 }
469
470 /**
471 * Get a list of logs
472 *
473 * @access public
474 *
475 * @param string Universal path
476 *
477 * @return array Log data
478 */
479 function fetch_logs($path)
480 {
481 if (!isset($this->logs["$path"]))
482 {
483 $log = new SVNLog($this->registry->paths->fetch_repos($path), $this->registry->paths->fetch_path($path), 0, 0);
484
485 $this->logs["$path"] = $log->fetch();
486 }
487
488 return $this->logs["$path"];
489 }
490
491 /**
492 * Returns a given log entry for a path
493 * and revision
494 *
495 * @access public
496 *
497 * @param string Universal path
498 * @param integer Arbitrary revision
499 *
500 * @return array Log entry
501 */
502 function fetch_log($path, $rev)
503 {
504 $logs = $this->fetch_logs($path);
505
506 $rev = $this->registry->svn->rev($rev);
507 if ($rev == 'HEAD')
508 {
509 $rev = $this->fetch_head_rev($path);
510 }
511
512 if (isset($logs["$rev"]))
513 {
514 return $logs["$rev"];
515 }
516 else
517 {
518 return null;
519 }
520 }
521 }
522
523 /**
524 * Annotation/blame system; constructs an array
525 * that is ready for output
526 *
527 * @package ViewSVN
528 * @version $Id$
529 */
530 class SVNBlame
531 {
532 /**
533 * Array of blame information
534 * @var array
535 */
536 var $blame = array();
537
538 /**
539 * Raw "svn blame" output
540 * @var array
541 */
542 var $rawoutput;
543
544 /**
545 * Constructor: create blame and store data
546 *
547 * @param string Repository
548 * @param string Path
549 * @param integer Revision
550 */
551 function SVNBlame($repos, $path, $revision)
552 {
553 global $viewsvn;
554
555 $this->rawoutput = $viewsvn->svn->blame($repos, $path, $revision);
556 $this->process();
557 }
558
559 /**
560 * Returns blame for display
561 *
562 * @access public
563 *
564 * @return array Blame data
565 */
566 function fetch()
567 {
568 return $this->blame;
569 }
570
571 /**
572 * Parses the blame data
573 *
574 * @access private
575 */
576 function process()
577 {
578 $lineno = 1;
579
580 foreach ($this->rawoutput AS $line)
581 {
582 if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)\s(.*)$#', $line, $matches))
583 {
584 $this->blame[] = array(
585 'rev' => $matches[1],
586 'author' => $matches[2],
587 'line' => $matches[3],
588 'lineno' => $lineno++
589 );
590 }
591 // a blank line
592 else if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)$#', $line, $matches))
593 {
594 $this->blame[] = array(
595 'rev' => $matches[1],
596 'author' => $matches[2],
597 'line' => '',
598 'lineno' => $lineno++
599 );
600 }
601 }
602 }
603 }
604
605 /**
606 * Log management system; creates a complex list
607 * of SVN log information
608 *
609 * @package ViewSVN
610 * @version $Id$
611 */
612 class SVNLog
613 {
614 /**
615 * Array of logs
616 * @var array
617 */
618 var $logs = array();
619
620 /**
621 * Raw "svn log" output
622 * @var array
623 */
624 var $rawoutput;
625
626 /**
627 * Constructor: create log store for the given file
628 *
629 * @param string Repository
630 * @param string Path
631 * @param integer Lower revision
632 * @param integer Higher revision
633 */
634 function SVNLog($repos, $path, $lorev, $hirev)
635 {
636 global $viewsvn;
637
638 $this->rawoutput = $viewsvn->svn->log($repos, $path, $lorev, $hirev);
639 $this->process();
640 }
641
642 /**
643 * Returns logs for display
644 *
645 * @access public
646 *
647 * @return array Log data
648 */
649 function fetch()
650 {
651 return $this->logs;
652 }
653
654 /**
655 * Splits up the raw output into a usable log
656 *
657 * @access private
658 */
659 function process()
660 {
661 $lastrev = 0;
662
663 for ($i = 1; $i <= count($this->rawoutput) - 1; $i++)
664 {
665 $line = $this->rawoutput["$i"];
666
667 if (preg_match('#^r([0-9]*) \| (.*?) \| (....-..-.. ..:..:..) ([0-9\-]*) \((.*?)\) \| ([0-9]*) lines?$#', $line, $matches))
668 {
669 if (isset($this->logs["$lastrev"]))
670 {
671 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
672 }
673
674 $this->logs["$matches[1]"] = array(
675 'rev' => $matches[1],
676 'author' => $matches[2],
677 'date' => $matches[3],
678 'timezone' => $matches[4],
679 'lines' => $matches[6],
680 'message' => ''
681 );
682
683 $lastrev = $matches[1];
684 }
685 else if (preg_match('#^\s+([ADMR])\s(.*)#', $line, $matches))
686 {
687 $this->logs["$lastrev"]['files'][] = array(
688 'action' => $matches[1],
689 'file' => $matches[2]
690 );
691 }
692 else
693 {
694 if (trim($line) != 'Changed paths:')
695 {
696 $this->logs["$lastrev"]['message'] .= $line . "\n";
697 }
698 }
699 }
700
701 if (isset($this->logs["$lastrev"]))
702 {
703 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
704 }
705 }
706
707 /**
708 * Trims the last dash line off a message
709 *
710 * @access private
711 *
712 * @param string Message with annoying-ass line
713 *
714 * @return string Clean string
715 */
716 function strip_last_line($string)
717 {
718 return trim(preg_replace("#\n(.*?)\n$#", '', $string));
719 }
720 }
721
722 /**
723 * Diff system; constructs a diff array that
724 * is ready for output
725 *
726 * @package ViewSVN
727 */
728 class SVNDiff
729 {
730 /**
731 * Array of diff information
732 * @var array
733 */
734 var $diff = array();
735
736 /**
737 * Raw "svn diff" output
738 * @var array
739 */
740 var $rawoutput;
741
742 /**
743 * Constructor: create and store diff data
744 *
745 * @param string Repository
746 * @param string Path
747 * @param integer Lower revision
748 * @param integer Higher revision
749 */
750 function SVNDiff($repos, $path, $lorev, $hirev)
751 {
752 global $viewsvn;
753
754 $this->rawoutput = $viewsvn->svn->diff($repos, $path, $lorev, $hirev);
755 $this->process();
756 }
757
758 /**
759 * Returns diffs for display
760 *
761 * @access public
762 *
763 * @return array Diff data
764 */
765 function fetch()
766 {
767 return $this->diff;
768 }
769
770 /**
771 * Processes and prepares diff data
772 *
773 * @access private
774 */
775 function process()
776 {
777 $chunk = 0;
778 $indexcounter = null;
779
780 $lastact = '';
781 $lastcontent = '';
782
783 foreach ($this->rawoutput AS $line)
784 {
785 if (preg_match('#^@@ \-([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@$#', $line, $bits))
786 {
787 $lastact = '';
788 $lastcontent = '';
789
790 $this->diff["$index"][ ++$chunk ]['hunk'] = array('old' => array('line' => $bits[1], 'count' => $bits[2]), 'new' => array('line' => $bits[3], 'count' => $bits[4]));
791 $lines['old'] = $this->diff["$index"]["$chunk"]['hunk']['old']['line'] - 1;
792 $lines['new'] = $this->diff["$index"]["$chunk"]['hunk']['new']['line'] - 1;
793 continue;
794 }
795
796 if ($indexcounter <= 5 AND $indexcounter !== null)
797 {
798 $indexcounter++;
799 continue;
800 }
801 else if ($indexcounter == 5)
802 {
803 $indexcounter = null;
804 continue;
805 }
806
807 if (preg_match('#^([\+\- ])(.*)#', $line, $matches))
808 {
809 $act = $matches[1];
810 $content = $matches[2];
811
812 if ($act == ' ')
813 {
814 $this->diff["$index"]["$chunk"][] = array(
815 'line' => $content,
816 'act' => '',
817 'oldlineno' => ++$lines['old'],
818 'newlineno' => ++$lines['new']
819 );
820 }
821 else if ($act == '+')
822 {
823 // potential line delta
824 if ($lastact == '-')
825 {
826 if ($delta = @$this->fetch_diff_extent($lastcontent, $content))
827 {
828 // create two sets of ends for the two contents
829 $delta['endo'] = strlen($lastcontent) - $delta['end'];
830 $delta['endn'] = strlen($content) - $delta['end'];
831
832 $diffo = $delta['endo'] - $delta['start'];
833 $diffn = $delta['endn'] - $delta['start'];
834
835 if (strlen($lastcontent) > $delta['endo'] - $diffo)
836 {
837 $removed = substr($lastcontent, $delta['start'], $diffo);
838 $this->diff["$index"]["$chunk"][ count($this->diff["$index"]["$chunk"]) - 2 ]['line'] = substr_replace($lastcontent, '{@-' . '-}' . $removed . '{/@-' . '-}', $delta['start'], $diffo);
839 }
840
841 if (strlen($content) > $delta['endn'] - $diffn)
842 {
843 $added = substr($content, $delta['start'], $diffn);
844 $content = substr_replace($content, '{@+' . '+}' . $added . '{/@+' . '+}', $delta['start'], $diffn);
845 }
846 }
847 }
848
849 $this->diff["$index"]["$chunk"][] = array(
850 'line' => $content,
851 'act' => '+',
852 'oldlineno' => '',
853 'newlineno' => ++$lines['new']
854 );
855 }
856 else if ($act == '-')
857 {
858 $lastcontent = $content;
859
860 $this->diff["$index"]["$chunk"][] = array(
861 'line' => $content,
862 'act' => '-',
863 'oldlineno' => ++$lines['old'],
864 'newlineno' => ''
865 );
866 }
867
868 $lastact = $act;
869 }
870 // whitespace lines
871 else
872 {
873 if (preg_match('#^Index: (.*?)$#', $line, $matches))
874 {
875 $index = $matches[1];
876 $indexcounter = 1;
877 $chunk = 0;
878 continue;
879 }
880
881 $lastact = '';
882
883 $this->diff["$index"]["$chunk"][] = array(
884 'line' => '',
885 'act' => '',
886 'oldlineno' => ++$lines['old'],
887 'newlineno' => ++$lines['new']
888 );
889 }
890 }
891 }
892
893 /**
894 * Returns the amount of change that occured
895 * between two lines
896 *
897 * @access private
898 *
899 * @param string Old line
900 * @param string New line
901 *
902 * @return array Difference of positions
903 */
904 function fetch_diff_extent($old, $new)
905 {
906 $start = 0;
907 $min = min(strlen($old), strlen($new));
908
909 for ($start = 0; $start < $min; $start++)
910 {
911 if ($old{"$start"} != $new{"$start"})
912 {
913 break;
914 }
915 }
916
917 $max = max(strlen($old), strlen($new));
918
919 for ($end = 0; $end < $min; $end++)
920 {
921 $oldpos = strlen($old) - $end;
922 $newpos = strlen($new) - $end;
923
924 if ($old{"$oldpos"} != $new{"$newpos"})
925 {
926 break;
927 }
928 }
929
930 $end--;
931
932 if ($start == 0 AND $end == $max)
933 {
934 return false;
935 }
936
937 return array('start' => $start, 'end' => $end);
938 }
939 }
940
941 /*=====================================================================*\
942 || ###################################################################
943 || # $HeadURL$
944 || # $Id$
945 || ###################################################################
946 \*=====================================================================*/
947 ?>