More refactoring. This time breaking up svnlib.php
[viewsvn.git] / includes / svncommon.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 * Common functions that aren't Xquery-related and advanced query systems
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($viewsvn->lang->string('The SVN binary could not be located'));
67 }
68
69 if (!preg_match('#^svn, version (.*?)\)$#i', trim($access[0])))
70 {
71 $viewsvn->trigger->error($viewsvn->lang->string('The SVN binary does not appear to be valid (it failed our tests)'));
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 * Formats a SVN log message
115 *
116 * @access public
117 *
118 * @param string Unformatted log message
119 *
120 * @return string Output-ready log message
121 */
122 function format_log_message($message)
123 {
124 global $viewsvn;
125
126 $message = $viewsvn->entity_encode($message);
127
128 $message = preg_replace('#r([0-9]+)#e', '"<a href=\"" . $viewsvn->path . "/" . $viewsvn->paths->out("diff.php" . $viewsvn->paths->fetch_rev_str(true, "\1", 0), $viewsvn->paths->fetch_repos($viewsvn->paths->parse()) . "/") . "\">r\1</a>"', $message);
129
130 $list = false;
131 $lines = explode("\n", $message);
132 $message = '';
133 foreach ($lines AS $line)
134 {
135 if (preg_match('#^\s*?(\*|\-)\s?(.*)#', $line, $matches))
136 {
137 if ($list)
138 {
139 $message .= '<li>' . $matches[2] . '</li>';
140 }
141 else
142 {
143 $message .= '<ul class="list">';
144 $message .= '<li>' . $matches[2] . '</li>';
145 }
146 $list = true;
147 }
148 else
149 {
150 if ($list)
151 {
152 $message .= '</ul>';
153 $message .= $line;
154 }
155 else
156 {
157 $message .= $line;
158 $message .= '<br />';
159 }
160 $list = false;
161 }
162
163 $message .= "\n";
164 }
165
166 if ($list)
167 {
168 $message .= '</ul>';
169 }
170
171 $message = preg_replace('#(<br />)*$#', '', $message);
172
173 return $message;
174 }
175
176 // ###################################################################
177 /**
178 * Parses a date from Xquery XML outut
179 *
180 * @access public
181 *
182 * @param string Date string
183 *
184 * @return string Formatted and readable date string
185 */
186 function format_date_string($string)
187 {
188 // 2005-01-23T20:42:53.703057Z
189 return preg_replace('#(....)\-(..)\-(..)T(..):(..):(..).(.*)Z#e', 'gmdate("r", mktime(\4, \5, \6, \2, \3, \1))', $string);
190 }
191
192 /**
193 * Counts the spaces and replaces two or more ones
194 *
195 * @access private
196 *
197 * @param string Spaced string
198 *
199 * @return string &nbsp;'d string
200 */
201 function format_spaces($thestring)
202 {
203 if (strlen($thestring) >= 2)
204 {
205 $thestring = str_replace(' ', '&nbsp;', $thestring);
206 }
207
208 return $thestring;
209 }
210 }
211
212 /**
213 * Commonly executed SVN commands that return data
214 * used in many parts of the system
215 *
216 * @package ViewSVN
217 * @version $Id$
218 */
219 class SVNCommon
220 {
221 /**
222 * Registry object
223 * @var object
224 */
225 var $registry;
226
227 /**
228 * List of revisions
229 * @var array
230 */
231 var $revisions;
232
233 /**
234 * List of logs
235 * @var array
236 */
237 var $logs;
238
239 /**
240 * Constructor: bind with registry
241 */
242 function SVNCommon()
243 {
244 global $viewsvn;
245
246 $this->registry =& $viewsvn;
247 }
248
249 /**
250 * Checks to see if the given universal path is
251 * a directory
252 *
253 * @access public
254 *
255 * @param string Universal path
256 *
257 * @return bool Directory or not
258 */
259 function isdir($path)
260 {
261 $output = $this->registry->svn->std('info', $this->registry->paths->fetch_repos($path), $this->registry->paths->fetch_path($path), $this->registry->paths->revnum);
262
263 foreach ($output AS $line)
264 {
265 if (preg_match('#^Node Kind: (.*)#', $line, $matches))
266 {
267 if (trim(strtolower($matches[1])) == 'directory')
268 {
269 return true;
270 }
271 }
272 }
273
274 return false;
275 }
276
277 /**
278 * Get a list of revisions for a path
279 *
280 * @access public
281 *
282 * @param string Universal path
283 *
284 * @return array Key revisions
285 */
286 function fetch_revs($path)
287 {
288 if (!isset($this->revisions["$path"]))
289 {
290 $log = $this->fetch_logs($path);
291
292 $revs = array_keys($log);
293
294 $this->revisions["$path"] = array(
295 'HEAD' => $revs[0],
296 'START' => $revs[ count($revs) - 1 ],
297 'revs' => $revs
298 );
299 }
300
301 return $this->revisions["$path"];
302 }
303
304 /**
305 * Gets the revision that is marked as HEAD
306 *
307 * @access public
308 *
309 * @param string Universal path
310 *
311 * @return integer Revision
312 */
313 function fetch_head_rev($path)
314 {
315 $output = $this->registry->shell->exec($this->registry->svn->svnpath . ' info ' . $this->registry->repos->fetch_path($this->registry->paths->fetch_repos($path), false) . $this->registry->paths->fetch_path($path));
316
317 foreach ($output AS $line)
318 {
319 if (preg_match('#^Last Changed Rev: (.*)#', $line, $matches))
320 {
321 return $matches[1];
322 }
323 }
324
325 $revs = $this->fetch_revs($path);
326 return $revs['HEAD'];
327 }
328
329 /**
330 * Returns the previous revision to the one given
331 *
332 * @access public
333 *
334 * @param string Universal path
335 * @param integer Arbitrary revision
336 *
337 * @return integer Previous revision (-1 if none)
338 */
339 function fetch_prev_rev($path, $current)
340 {
341 global $viewsvn;
342
343 $revs = $this->fetch_revs($path);
344
345 if ($current == 'HEAD')
346 {
347 $current = $this->fetch_head_rev($path);
348 }
349
350 $index = array_search($current, $revs['revs']);
351 if ($current === false)
352 {
353 $message->trigger->error(sprintf($viewsvn->lang->string('Revision r%1$s is not in path %2$s'), $current, $path));
354 }
355
356 if (isset($revs['revs'][ $index + 1 ]))
357 {
358 return $revs['revs'][ $index + 1 ];
359 }
360 else
361 {
362 return -1;
363 }
364 }
365
366 /**
367 * Get a list of logs
368 *
369 * @access public
370 *
371 * @param string Universal path
372 * @param bool Override the cache system?
373 *
374 * @return array Log data
375 */
376 function fetch_logs($path)
377 {
378 if (!isset($this->logs["$path"]))
379 {
380 $log = new SVNLog($this->registry->paths->fetch_repos($path), $this->registry->paths->fetch_path($path), 0, $this->registry->paths->revnum);
381
382 $this->logs["$path"] = $log->fetch();
383 }
384
385 return $this->logs["$path"];
386 }
387
388 /**
389 * Returns a given log entry for a path
390 * and revision
391 *
392 * @access public
393 *
394 * @param string Universal path
395 * @param integer Arbitrary revision
396 *
397 * @return array Log entry
398 */
399 function fetch_log($path, $rev)
400 {
401 $logs = $this->fetch_logs($path);
402
403 $rev = $this->registry->svn->rev($rev);
404 if ($rev == 'HEAD')
405 {
406 $rev = $this->fetch_head_rev($path);
407 }
408
409 if (isset($logs["$rev"]))
410 {
411 return $logs["$rev"];
412 }
413 else
414 {
415 $keys = array_keys($logs);
416 sort($keys);
417
418 for ($i = 0; $i < count($keys); $i++)
419 {
420 if ($rev > $keys["$i"] AND $rev < $keys[ $i + 1 ])
421 {
422 return $logs["$keys[$i]"];
423 }
424 }
425
426 return null;
427 }
428 }
429
430 /**
431 * Prints the file changed list
432 *
433 * @access public
434 *
435 * @public array List of file changes
436 * @public string The repository
437 * @public integer Current revision
438 *
439 * @return string Processed HTML
440 */
441 function construct_file_changes($changes, $repos, $revision)
442 {
443 global $viewsvn;
444
445 $files = '';
446
447 foreach ($changes AS $file)
448 {
449 switch ($file['action'])
450 {
451 case 'A':
452 $class = 'file_add';
453 $tooltip = $viewsvn->lang->string('Added');
454 break;
455 case 'D':
456 $class = 'file_delete';
457 $tooltip = $viewsvn->lang->string('Deleted');
458 break;
459 case 'M':
460 $class = 'file_modify';
461 $tooltip = $viewsvn->lang->string('Modified');
462 break;
463 case 'R':
464 $class = 'file_replace';
465 $tooltip = $viewsvn->lang->string('Replaced');
466 break;
467 }
468
469 $show['from'] = (bool)$file['from'];
470
471 if ($file['from'])
472 {
473 $class = 'file_move';
474 $tooltip = 'Moved/Copied';
475 preg_match('#(.*):([0-9]+)#', $file['from'], $matches);
476 $link['from'] = $viewsvn->paths->out('view.php' . $viewsvn->paths->fetch_rev_str(false, $matches[2]), $repos . $matches[1]);
477 }
478
479 $link['file'] = $viewsvn->paths->out('view.php' . $viewsvn->paths->fetch_rev_str(false, $revision), $repos . $file['file']);
480
481 eval('$files .= "' . $viewsvn->template->fetch('file_change') . '";');
482 }
483
484 return $files;
485 }
486 }
487
488 /**
489 * Annotation/blame system; constructs an array
490 * that is ready for output
491 *
492 * @package ViewSVN
493 * @version $Id$
494 */
495 class SVNBlame
496 {
497 /**
498 * Array of blame information
499 * @var array
500 */
501 var $blame = array();
502
503 /**
504 * Raw "svn blame" output
505 * @var array
506 */
507 var $rawoutput;
508
509 /**
510 * Constructor: create blame and store data
511 *
512 * @param string Repository
513 * @param string Path
514 * @param integer Revision
515 */
516 function SVNBlame($repos, $path, $revision)
517 {
518 global $viewsvn;
519
520 $this->rawoutput = $viewsvn->svn->blame($repos, $path, $revision);
521 $this->process();
522 }
523
524 /**
525 * Returns blame for display
526 *
527 * @access public
528 *
529 * @return array Blame data
530 */
531 function fetch()
532 {
533 return $this->blame;
534 }
535
536 /**
537 * Parses the blame data
538 *
539 * @access private
540 */
541 function process()
542 {
543 $lineno = 1;
544
545 foreach ($this->rawoutput AS $line)
546 {
547 if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)\s(.*)$#', $line, $matches))
548 {
549 $this->blame[] = array(
550 'rev' => $matches[1],
551 'author' => $matches[2],
552 'line' => $matches[3],
553 'lineno' => $lineno++
554 );
555 }
556 // a blank line
557 else if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)$#', $line, $matches))
558 {
559 $this->blame[] = array(
560 'rev' => $matches[1],
561 'author' => $matches[2],
562 'line' => '',
563 'lineno' => $lineno++
564 );
565 }
566 }
567 }
568 }
569
570 /**
571 * Log management system; creates a complex list
572 * of SVN log information
573 *
574 * @package ViewSVN
575 * @version $Id$
576 */
577 class SVNLog
578 {
579 /**
580 * Array of logs
581 * @var array
582 */
583 var $logs = array();
584
585 /**
586 * Raw "svn log" output
587 * @var array
588 */
589 var $rawoutput;
590
591 /**
592 * Constructor: create log store for the given file
593 *
594 * @param string Repository
595 * @param string Path
596 * @param integer Lower revision
597 * @param integer Higher revision
598 */
599 function SVNLog($repos, $path, $lorev, $hirev)
600 {
601 global $viewsvn;
602
603 $this->rawoutput = $viewsvn->svn->log($repos, $path, $lorev, $hirev);
604 $this->process();
605 }
606
607 /**
608 * Returns logs for display
609 *
610 * @access public
611 *
612 * @return array Log data
613 */
614 function fetch()
615 {
616 return $this->logs;
617 }
618
619 /**
620 * Splits up the raw output into a usable log
621 *
622 * @access private
623 */
624 function process()
625 {
626 $lastrev = 0;
627
628 for ($i = 1; $i <= count($this->rawoutput) - 1; $i++)
629 {
630 $line = $this->rawoutput["$i"];
631
632 if (preg_match('#^r([0-9]*) \| (.*?) \| (....-..-.. ..:..:..) ([0-9\-]*) \((.*?)\) \| ([0-9]*) lines?$#', $line, $matches))
633 {
634 if (isset($this->logs["$lastrev"]))
635 {
636 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
637 }
638
639 $this->logs["$matches[1]"] = array(
640 'rev' => $matches[1],
641 'author' => $matches[2],
642 'date' => $matches[3],
643 'timezone' => $matches[4],
644 'lines' => $matches[6],
645 'message' => ''
646 );
647
648 $lastrev = $matches[1];
649 }
650 else if (preg_match('#^\s+([ADMR])\s(.*)#', $line, $matches))
651 {
652 if (preg_match('#(.*) \(from (.*?)\)$#', $matches[2], $amatches))
653 {
654 $matches[2] = $amatches[1];
655 }
656
657 $this->logs["$lastrev"]['files'][] = array(
658 'action' => $matches[1],
659 'file' => trim($matches[2]),
660 'from' => (isset($amatches[2]) ? $amatches[2] : '')
661 );
662 }
663 else
664 {
665 if (trim($line) != 'Changed paths:')
666 {
667 $this->logs["$lastrev"]['message'] .= $line . "\n";
668 }
669 }
670 }
671
672 if (isset($this->logs["$lastrev"]))
673 {
674 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
675 }
676 }
677
678 /**
679 * Trims the last dash line off a message
680 *
681 * @access private
682 *
683 * @param string Message with annoying-ass line
684 *
685 * @return string Clean string
686 */
687 function strip_last_line($string)
688 {
689 return trim(preg_replace("#\n(.*?)\n$#", '', $string));
690 }
691 }
692
693 /**
694 * Diff system; constructs a diff array that
695 * is ready for output
696 *
697 * @package ViewSVN
698 */
699 class SVNDiff
700 {
701 /**
702 * Array of diff information
703 * @var array
704 */
705 var $diff = array();
706
707 /**
708 * Raw "svn diff" output
709 * @var array
710 */
711 var $rawoutput;
712
713 /**
714 * Constructor: create and store diff data
715 *
716 * @param string Repository
717 * @param string Path
718 * @param integer Lower revision
719 * @param integer Higher revision
720 */
721 function SVNDiff($repos, $path, $lorev, $hirev)
722 {
723 global $viewsvn;
724
725 $this->rawoutput = $viewsvn->svn->diff($repos, $path, $lorev, $hirev);
726 $this->process();
727 }
728
729 /**
730 * Returns diffs for display
731 *
732 * @access public
733 *
734 * @return array Diff data
735 */
736 function fetch()
737 {
738 return $this->diff;
739 }
740
741 /**
742 * Processes and prepares diff data
743 *
744 * @access private
745 */
746 function process()
747 {
748 global $viewsvn;
749
750 $chunk = 0;
751 $indexcounter = null;
752 $curprop = '';
753
754 $delstack = array();
755
756 foreach ($this->rawoutput AS $line)
757 {
758 if (preg_match('#^@@ \-([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@$#', $line, $bits))
759 {
760 $property = false;
761 $delstack = array();
762 $this->diff["$index"][ ++$chunk ]['hunk'] = array('old' => array('line' => $bits[1], 'count' => $bits[2]), 'new' => array('line' => $bits[3], 'count' => $bits[4]));
763 $lines['old'] = $this->diff["$index"]["$chunk"]['hunk']['old']['line'] - 1;
764 $lines['new'] = $this->diff["$index"]["$chunk"]['hunk']['new']['line'] - 1;
765 continue;
766 }
767 else if (preg_match('#^Property changes on: (.*?)$#', $line, $bits))
768 {
769 $property = true;
770 $index = $bits[1];
771 $this->diff["$index"]['props'] = array();
772 continue;
773 }
774
775 if ($indexcounter <= 3 AND $indexcounter !== null)
776 {
777 $indexcounter++;
778 continue;
779 }
780 else if ($indexcounter == 3)
781 {
782 $indexcounter = null;
783 continue;
784 }
785
786 if (preg_match('#^([\+\- ])(.*)#', $line, $matches) AND !$property)
787 {
788 $act = $matches[1];
789 $content = $matches[2];
790
791 if ($act == ' ')
792 {
793 $this->diff["$index"]["$chunk"][] = array(
794 'line' => $content,
795 'act' => '',
796 'oldlineno' => ++$lines['old'],
797 'newlineno' => ++$lines['new']
798 );
799
800 $delstack = array();
801 }
802 else if ($act == '+')
803 {
804 // potential line delta
805 if (count($delstack) > 0)
806 {
807 $lastline = array_shift($delstack);
808
809 if ($delta = @$this->fetch_diff_extent($lastline['line'], $content))
810 {
811 if (strlen($lastline['line']) > ($delta['start'] - $delta['end']))
812 {
813 $end = strlen($lastline['line']) + $delta['end'];
814 $viewsvn->debug("RM delta- = " . $end);
815 $change = '{@-' . '-}' . substr($lastline['line'], $delta['start'], $end - $delta['start']) . '{/@-' . '-}';
816 $this->diff["$index"]["$chunk"]["$lastline[INDEX]"]['line'] = substr($lastline['line'], 0, $delta['start']) . $change . substr($lastline['line'], $end);
817 }
818
819 if (strlen($content) > $delta['start'] - $delta['end'])
820 {
821 $end = strlen($content) + $delta['end'];
822 $viewsvn->debug("MK delta+ = " . $end);
823 $change = '{@+' . '+}' . substr($content, $delta['start'], $end - $delta['start']) . '{/@+' . '+}';
824 $content = substr($content, 0, $delta['start']) . $change . substr($content, $end);
825 }
826 }
827 }
828
829 $this->diff["$index"]["$chunk"][] = array(
830 'line' => $content,
831 'act' => '+',
832 'oldlineno' => '',
833 'newlineno' => ++$lines['new']
834 );
835 }
836 else if ($act == '-')
837 {
838 $this->diff["$index"]["$chunk"][] = $thearray = array(
839 'line' => $content,
840 'act' => '-',
841 'oldlineno' => ++$lines['old'],
842 'newlineno' => ''
843 );
844
845 $key = count($this->diff["$index"]["$chunk"]) - 2;
846 $thearray['INDEX'] = $key;
847
848 array_push($delstack, $thearray);
849 }
850 }
851 // whitespace lines
852 else
853 {
854 if (preg_match('#^Index: (.*?)$#', $line, $matches))
855 {
856 $index = $matches[1];
857 $indexcounter = 1;
858 $chunk = 0;
859 continue;
860 }
861
862 if ($property)
863 {
864 if (preg_match('#^__*_$#', trim($line)))
865 {
866 $viewsvn->debug("skipping: $line");
867 continue;
868 }
869
870 if (preg_match('#Name: (.*?)$#', $line, $matches))
871 {
872 $curprop = $matches[1];
873 $viewsvn->debug("prop: $curprop");
874 continue;
875 }
876 else
877 {
878 if (preg_match('#^\s+?\+(.*)#', $line, $matches))
879 {
880 $mode = 'add';
881 $this->diff["$index"]['props']["$curprop"]['add'] .= $matches[1];
882 }
883 else if (preg_match('#^\s+?\-(.*)#', $line, $matches))
884 {
885 $mode = 'del';
886 $this->diff["$index"]['props']["$curprop"]['del'] .= $matches[1];
887 }
888 else if (!preg_match('#^\s+[\+\- ](.*)#', $line) AND trim($line) != '')
889 {
890 $this->diff["$index"]['props']["$curprop"]["$mode"] .= "\n" . $line;
891 }
892 continue;
893 }
894 }
895
896 $this->diff["$index"]["$chunk"][] = array(
897 'line' => '',
898 'act' => '',
899 'oldlineno' => ++$lines['old'],
900 'newlineno' => ++$lines['new']
901 );
902
903 $delstack = array();
904 }
905 }
906 }
907
908 /**
909 * Returns the amount of change that occured
910 * between two lines
911 *
912 * @access private
913 *
914 * @param string Old line
915 * @param string New line
916 *
917 * @return array Difference of positions
918 */
919 function fetch_diff_extent($old, $new)
920 {
921 global $viewsvn;
922
923 $start = 0;
924 $min = min(strlen($old), strlen($new));
925
926 $viewsvn->debug("min1 = $min");
927
928 while ($start < $min AND $old["$start"] == $new["$start"])
929 {
930 $start++;
931 }
932
933 $end = -1;
934 $min = $min - $start;
935
936 $viewsvn->debug("min2 = $min");
937
938 $viewsvn->debug("checking: " . $old[ strlen($old) + $end ] . ' == ' . $new[ strlen($new) + $end ]);
939
940 while (-$end <= $min AND $old[ strlen($old) + $end ] == $new[ strlen($new) + $end ])
941 {
942 $end--;
943 }
944
945 return array('start' => $start, 'end' => $end + 1);
946 }
947 }
948
949 /*=====================================================================*\
950 || ###################################################################
951 || # $HeadURL$
952 || # $Id$
953 || ###################################################################
954 \*=====================================================================*/
955 ?>