Random pieces that don't work all quite yet. browse.php is getting there
[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($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 * Executes the SVN binary
213 *
214 * @access private
215 *
216 * @param string Command
217 *
218 * @return array Output
219 */
220 function svn($command)
221 {
222 global $viewsvn;
223
224 $output = $viewsvn->shell->exec($this->svnpath . ' ' . $command . ' 2>&1');
225
226 // make sure that we keep escaped chars
227 //$output = str_replace(array('\t', '\n', '\r'), array('\\\t', '\\\n', '\\\r'), $output);
228 //$output = preg_replace('#\\\(.)#', '\\\\\\\\' . '\1', $output);
229 //$output = str_replace('\\', '\\\\', $output);
230
231 $temp = implode("\n", $output);
232 if (strpos($temp, '(apr' . '_err=') !== false)
233 {
234 $viewsvn->trigger->error(nl2br($temp));
235 }
236 return $output;
237 }
238
239 /**
240 * SVN Wrapper: standard command system
241 *
242 * @access private
243 *
244 * @param string SVN command
245 * @param string Repository
246 * @param string Path
247 * @param integer Revision
248 *
249 * @return array Lines of output
250 */
251 function std($command, $repos, $path, $revision)
252 {
253 global $viewsvn;
254
255 $revision = $this->rev($revision);
256 $repospath = $viewsvn->repos->fetch_path($repos, false);
257
258 return $this->svn($command . ' ' . $repospath . $path . '@' . $revision);
259 }
260
261 /**
262 * SVN Wrapper: blame
263 *
264 * @access protected
265 *
266 * @param string Repository
267 * @param string Path
268 * @param integer Revision
269 *
270 * @return array Lines of blame output
271 */
272 function blame($repos, $path, $revision)
273 {
274 return $this->std('blame', $repos, $path, $revision);
275 }
276
277 /**
278 * SVN Wrapper: cat
279 *
280 * @access protected
281 *
282 * @param string Repository
283 * @param string Path
284 * @param integer Revision
285 *
286 * @return array Lines of cat output
287 */
288 function cat($repos, $path, $revision)
289 {
290 return $this->std('cat', $repos, $path, $revision);
291 }
292
293 /**
294 * SVN Wrapper: diff
295 *
296 * @access protected
297 *
298 * @param string Repository
299 * @param string Path
300 * @param integer Lower revision
301 * @param integer Higher revision
302 *
303 * @return array Lines of diff output
304 */
305 function diff($repos, $path, $lorev, $hirev)
306 {
307 global $viewsvn;
308
309 $hirev = $this->rev($hirev);
310 $lorev = $this->rev($lorev);
311 if ($lorev == 'HEAD')
312 {
313 $lorev = 1;
314 }
315
316 if (is_integer($hirev) AND is_integer($lorev))
317 {
318 if ($lorev > $hirev)
319 {
320 $lorev = $hirev - 1;
321 }
322 if ($lorev == $hirev)
323 {
324 $lorev = 0;
325 }
326 }
327
328 $repospath = $viewsvn->repos->fetch_path($repos, false);
329
330 return $this->svn('diff -r' . $lorev . ':' . $hirev . ' ' . $repospath . $path);
331 }
332
333 /**
334 * SVN Wrapper: log
335 *
336 * @access protected
337 *
338 * @param string Repository
339 * @param string Path
340 * @param integer Lower revision
341 * @param integer Higher revision
342 *
343 * @return array Lines of log output
344 */
345 function log($repos, $path, $lorev, $hirev)
346 {
347 global $viewsvn;
348
349 $hirev = $this->rev($hirev);
350 $lorev = $this->rev($hirev);
351 if ($lorev == 'HEAD')
352 {
353 $lorev = 0;
354 }
355
356 if (is_integer($hirev) AND is_integer($lorev))
357 {
358 if ($lorev > $hirev)
359 {
360 $lorev = $hirev - 1;
361 }
362 if ($lorev == $hirev)
363 {
364 $lorev = 0;
365 }
366 }
367
368 $repospath = $viewsvn->repos->fetch_path($repos, false);
369
370 return $this->svn('log -v -r' . $hirev . ':' . $lorev . ' ' . $repospath . $path);
371 }
372
373 /**
374 * SVN Wrapper: ls (list)
375 *
376 * @access protected
377 *
378 * @param string Repository
379 * @param string Path
380 * @param integer Revision
381 *
382 * @return array Lines of list output
383 */
384 function ls($repos, $path, $revision)
385 {
386 return $this->std('list', $repos, $path, $revision);
387 }
388
389 /**
390 * Generates a clean revision number
391 *
392 * @access public
393 *
394 * @param integer Revision number
395 *
396 * @return mixed Cleaned revision or HEAD
397 */
398 function rev($revision)
399 {
400 if (($revision = intval($revision)) < 1)
401 {
402 $revision = 'HEAD';
403 }
404 return $revision;
405 }
406 }
407
408 /**
409 * Commonly executed SVN commands that return data
410 * used in many parts of the system
411 *
412 * @package ViewSVN
413 * @version $Id$
414 */
415 class SVNCommon
416 {
417 /**
418 * Registry object
419 * @var object
420 */
421 var $registry;
422
423 /**
424 * List of revisions
425 * @var array
426 */
427 var $revisions;
428
429 /**
430 * List of logs
431 * @var array
432 */
433 var $logs;
434
435 /**
436 * Constructor: bind with registry
437 */
438 function SVNCommon()
439 {
440 global $viewsvn;
441
442 $this->registry =& $viewsvn;
443 }
444
445 /**
446 * Checks to see if the given universal path is
447 * a directory
448 *
449 * @access public
450 *
451 * @param string Universal path
452 *
453 * @return bool Directory or not
454 */
455 function isdir($path)
456 {
457 $output = $this->registry->svn->std('info', $this->registry->paths->fetch_repos($path), $this->registry->paths->fetch_path($path), $this->registry->paths->revnum);
458
459 foreach ($output AS $line)
460 {
461 if (preg_match('#^Node Kind: (.*)#', $line, $matches))
462 {
463 if (trim(strtolower($matches[1])) == 'directory')
464 {
465 return true;
466 }
467 }
468 }
469
470 return false;
471 }
472
473 /**
474 * Get a list of revisions for a path
475 *
476 * @access public
477 *
478 * @param string Universal path
479 *
480 * @return array Key revisions
481 */
482 function fetch_revs($path)
483 {
484 if (!isset($this->revisions["$path"]))
485 {
486 $log = $this->fetch_logs($path);
487
488 $revs = array_keys($log);
489
490 $this->revisions["$path"] = array(
491 'HEAD' => $revs[0],
492 'START' => $revs[ count($revs) - 1 ],
493 'revs' => $revs
494 );
495 }
496
497 return $this->revisions["$path"];
498 }
499
500 /**
501 * Gets the revision that is marked as HEAD
502 *
503 * @access public
504 *
505 * @param string Universal path
506 *
507 * @return integer Revision
508 */
509 function fetch_head_rev($path)
510 {
511 $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));
512
513 foreach ($output AS $line)
514 {
515 if (preg_match('#^Last Changed Rev: (.*)#', $line, $matches))
516 {
517 return $matches[1];
518 }
519 }
520
521 $revs = $this->fetch_revs($path);
522 return $revs['HEAD'];
523 }
524
525 /**
526 * Returns the previous revision to the one given
527 *
528 * @access public
529 *
530 * @param string Universal path
531 * @param integer Arbitrary revision
532 *
533 * @return integer Previous revision (-1 if none)
534 */
535 function fetch_prev_rev($path, $current)
536 {
537 global $viewsvn;
538
539 $revs = $this->fetch_revs($path);
540
541 if ($current == 'HEAD')
542 {
543 $current = $this->fetch_head_rev($path);
544 }
545
546 $index = array_search($current, $revs['revs']);
547 if ($current === false)
548 {
549 $message->trigger->error(sprintf($viewsvn->lang->string('Revision r%1$s is not in path %2$s'), $current, $path));
550 }
551
552 if (isset($revs['revs'][ $index + 1 ]))
553 {
554 return $revs['revs'][ $index + 1 ];
555 }
556 else
557 {
558 return -1;
559 }
560 }
561
562 /**
563 * Get a list of logs
564 *
565 * @access public
566 *
567 * @param string Universal path
568 * @param bool Override the cache system?
569 *
570 * @return array Log data
571 */
572 function fetch_logs($path)
573 {
574 if (!isset($this->logs["$path"]))
575 {
576 $log = new SVNLog($this->registry->paths->fetch_repos($path), $this->registry->paths->fetch_path($path), 0, $this->registry->paths->revnum);
577
578 $this->logs["$path"] = $log->fetch();
579 }
580
581 return $this->logs["$path"];
582 }
583
584 /**
585 * Returns a given log entry for a path
586 * and revision
587 *
588 * @access public
589 *
590 * @param string Universal path
591 * @param integer Arbitrary revision
592 *
593 * @return array Log entry
594 */
595 function fetch_log($path, $rev)
596 {
597 $logs = $this->fetch_logs($path);
598
599 $rev = $this->registry->svn->rev($rev);
600 if ($rev == 'HEAD')
601 {
602 $rev = $this->fetch_head_rev($path);
603 }
604
605 if (isset($logs["$rev"]))
606 {
607 return $logs["$rev"];
608 }
609 else
610 {
611 $keys = array_keys($logs);
612 sort($keys);
613
614 for ($i = 0; $i < count($keys); $i++)
615 {
616 if ($rev > $keys["$i"] AND $rev < $keys[ $i + 1 ])
617 {
618 return $logs["$keys[$i]"];
619 }
620 }
621
622 return null;
623 }
624 }
625
626 /**
627 * Prints the file changed list
628 *
629 * @access public
630 *
631 * @public array List of file changes
632 * @public string The repository
633 * @public integer Current revision
634 *
635 * @return string Processed HTML
636 */
637 function construct_file_changes($changes, $repos, $revision)
638 {
639 global $viewsvn;
640
641 $files = '';
642
643 foreach ($changes AS $file)
644 {
645 switch ($file['action'])
646 {
647 case 'A':
648 $class = 'file_add';
649 $tooltip = $viewsvn->lang->string('Added');
650 break;
651 case 'D':
652 $class = 'file_delete';
653 $tooltip = $viewsvn->lang->string('Deleted');
654 break;
655 case 'M':
656 $class = 'file_modify';
657 $tooltip = $viewsvn->lang->string('Modified');
658 break;
659 case 'R':
660 $class = 'file_replace';
661 $tooltip = $viewsvn->lang->string('Replaced');
662 break;
663 }
664
665 $show['from'] = (bool)$file['from'];
666
667 if ($file['from'])
668 {
669 $class = 'file_move';
670 $tooltip = 'Moved/Copied';
671 preg_match('#(.*):([0-9]+)#', $file['from'], $matches);
672 $link['from'] = $viewsvn->paths->out('view.php' . $viewsvn->paths->fetch_rev_str(false, $matches[2]), $repos . $matches[1]);
673 }
674
675 $link['file'] = $viewsvn->paths->out('view.php' . $viewsvn->paths->fetch_rev_str(false, $revision), $repos . $file['file']);
676
677 eval('$files .= "' . $viewsvn->template->fetch('file_change') . '";');
678 }
679
680 return $files;
681 }
682 }
683
684 /**
685 * Annotation/blame system; constructs an array
686 * that is ready for output
687 *
688 * @package ViewSVN
689 * @version $Id$
690 */
691 class SVNBlame
692 {
693 /**
694 * Array of blame information
695 * @var array
696 */
697 var $blame = array();
698
699 /**
700 * Raw "svn blame" output
701 * @var array
702 */
703 var $rawoutput;
704
705 /**
706 * Constructor: create blame and store data
707 *
708 * @param string Repository
709 * @param string Path
710 * @param integer Revision
711 */
712 function SVNBlame($repos, $path, $revision)
713 {
714 global $viewsvn;
715
716 $this->rawoutput = $viewsvn->svn->blame($repos, $path, $revision);
717 $this->process();
718 }
719
720 /**
721 * Returns blame for display
722 *
723 * @access public
724 *
725 * @return array Blame data
726 */
727 function fetch()
728 {
729 return $this->blame;
730 }
731
732 /**
733 * Parses the blame data
734 *
735 * @access private
736 */
737 function process()
738 {
739 $lineno = 1;
740
741 foreach ($this->rawoutput AS $line)
742 {
743 if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)\s(.*)$#', $line, $matches))
744 {
745 $this->blame[] = array(
746 'rev' => $matches[1],
747 'author' => $matches[2],
748 'line' => $matches[3],
749 'lineno' => $lineno++
750 );
751 }
752 // a blank line
753 else if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)$#', $line, $matches))
754 {
755 $this->blame[] = array(
756 'rev' => $matches[1],
757 'author' => $matches[2],
758 'line' => '',
759 'lineno' => $lineno++
760 );
761 }
762 }
763 }
764 }
765
766 /**
767 * Log management system; creates a complex list
768 * of SVN log information
769 *
770 * @package ViewSVN
771 * @version $Id$
772 */
773 class SVNLog
774 {
775 /**
776 * Array of logs
777 * @var array
778 */
779 var $logs = array();
780
781 /**
782 * Raw "svn log" output
783 * @var array
784 */
785 var $rawoutput;
786
787 /**
788 * Constructor: create log store for the given file
789 *
790 * @param string Repository
791 * @param string Path
792 * @param integer Lower revision
793 * @param integer Higher revision
794 */
795 function SVNLog($repos, $path, $lorev, $hirev)
796 {
797 global $viewsvn;
798
799 $this->rawoutput = $viewsvn->svn->log($repos, $path, $lorev, $hirev);
800 $this->process();
801 }
802
803 /**
804 * Returns logs for display
805 *
806 * @access public
807 *
808 * @return array Log data
809 */
810 function fetch()
811 {
812 return $this->logs;
813 }
814
815 /**
816 * Splits up the raw output into a usable log
817 *
818 * @access private
819 */
820 function process()
821 {
822 $lastrev = 0;
823
824 for ($i = 1; $i <= count($this->rawoutput) - 1; $i++)
825 {
826 $line = $this->rawoutput["$i"];
827
828 if (preg_match('#^r([0-9]*) \| (.*?) \| (....-..-.. ..:..:..) ([0-9\-]*) \((.*?)\) \| ([0-9]*) lines?$#', $line, $matches))
829 {
830 if (isset($this->logs["$lastrev"]))
831 {
832 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
833 }
834
835 $this->logs["$matches[1]"] = array(
836 'rev' => $matches[1],
837 'author' => $matches[2],
838 'date' => $matches[3],
839 'timezone' => $matches[4],
840 'lines' => $matches[6],
841 'message' => ''
842 );
843
844 $lastrev = $matches[1];
845 }
846 else if (preg_match('#^\s+([ADMR])\s(.*)#', $line, $matches))
847 {
848 if (preg_match('#(.*) \(from (.*?)\)$#', $matches[2], $amatches))
849 {
850 $matches[2] = $amatches[1];
851 }
852
853 $this->logs["$lastrev"]['files'][] = array(
854 'action' => $matches[1],
855 'file' => trim($matches[2]),
856 'from' => (isset($amatches[2]) ? $amatches[2] : '')
857 );
858 }
859 else
860 {
861 if (trim($line) != 'Changed paths:')
862 {
863 $this->logs["$lastrev"]['message'] .= $line . "\n";
864 }
865 }
866 }
867
868 if (isset($this->logs["$lastrev"]))
869 {
870 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
871 }
872 }
873
874 /**
875 * Trims the last dash line off a message
876 *
877 * @access private
878 *
879 * @param string Message with annoying-ass line
880 *
881 * @return string Clean string
882 */
883 function strip_last_line($string)
884 {
885 return trim(preg_replace("#\n(.*?)\n$#", '', $string));
886 }
887 }
888
889 /**
890 * Diff system; constructs a diff array that
891 * is ready for output
892 *
893 * @package ViewSVN
894 */
895 class SVNDiff
896 {
897 /**
898 * Array of diff information
899 * @var array
900 */
901 var $diff = array();
902
903 /**
904 * Raw "svn diff" output
905 * @var array
906 */
907 var $rawoutput;
908
909 /**
910 * Constructor: create and store diff data
911 *
912 * @param string Repository
913 * @param string Path
914 * @param integer Lower revision
915 * @param integer Higher revision
916 */
917 function SVNDiff($repos, $path, $lorev, $hirev)
918 {
919 global $viewsvn;
920
921 $this->rawoutput = $viewsvn->svn->diff($repos, $path, $lorev, $hirev);
922 $this->process();
923 }
924
925 /**
926 * Returns diffs for display
927 *
928 * @access public
929 *
930 * @return array Diff data
931 */
932 function fetch()
933 {
934 return $this->diff;
935 }
936
937 /**
938 * Processes and prepares diff data
939 *
940 * @access private
941 */
942 function process()
943 {
944 global $viewsvn;
945
946 $chunk = 0;
947 $indexcounter = null;
948 $curprop = '';
949
950 $delstack = array();
951
952 foreach ($this->rawoutput AS $line)
953 {
954 if (preg_match('#^@@ \-([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@$#', $line, $bits))
955 {
956 $property = false;
957 $delstack = array();
958 $this->diff["$index"][ ++$chunk ]['hunk'] = array('old' => array('line' => $bits[1], 'count' => $bits[2]), 'new' => array('line' => $bits[3], 'count' => $bits[4]));
959 $lines['old'] = $this->diff["$index"]["$chunk"]['hunk']['old']['line'] - 1;
960 $lines['new'] = $this->diff["$index"]["$chunk"]['hunk']['new']['line'] - 1;
961 continue;
962 }
963 else if (preg_match('#^Property changes on: (.*?)$#', $line, $bits))
964 {
965 $property = true;
966 $index = $bits[1];
967 $this->diff["$index"]['props'] = array();
968 continue;
969 }
970
971 if ($indexcounter <= 3 AND $indexcounter !== null)
972 {
973 $indexcounter++;
974 continue;
975 }
976 else if ($indexcounter == 3)
977 {
978 $indexcounter = null;
979 continue;
980 }
981
982 if (preg_match('#^([\+\- ])(.*)#', $line, $matches) AND !$property)
983 {
984 $act = $matches[1];
985 $content = $matches[2];
986
987 if ($act == ' ')
988 {
989 $this->diff["$index"]["$chunk"][] = array(
990 'line' => $content,
991 'act' => '',
992 'oldlineno' => ++$lines['old'],
993 'newlineno' => ++$lines['new']
994 );
995
996 $delstack = array();
997 }
998 else if ($act == '+')
999 {
1000 // potential line delta
1001 if (count($delstack) > 0)
1002 {
1003 $lastline = array_shift($delstack);
1004
1005 if ($delta = @$this->fetch_diff_extent($lastline['line'], $content))
1006 {
1007 if (strlen($lastline['line']) > ($delta['start'] - $delta['end']))
1008 {
1009 $end = strlen($lastline['line']) + $delta['end'];
1010 $viewsvn->debug("RM delta- = " . $end);
1011 $change = '{@-' . '-}' . substr($lastline['line'], $delta['start'], $end - $delta['start']) . '{/@-' . '-}';
1012 $this->diff["$index"]["$chunk"]["$lastline[INDEX]"]['line'] = substr($lastline['line'], 0, $delta['start']) . $change . substr($lastline['line'], $end);
1013 }
1014
1015 if (strlen($content) > $delta['start'] - $delta['end'])
1016 {
1017 $end = strlen($content) + $delta['end'];
1018 $viewsvn->debug("MK delta+ = " . $end);
1019 $change = '{@+' . '+}' . substr($content, $delta['start'], $end - $delta['start']) . '{/@+' . '+}';
1020 $content = substr($content, 0, $delta['start']) . $change . substr($content, $end);
1021 }
1022 }
1023 }
1024
1025 $this->diff["$index"]["$chunk"][] = array(
1026 'line' => $content,
1027 'act' => '+',
1028 'oldlineno' => '',
1029 'newlineno' => ++$lines['new']
1030 );
1031 }
1032 else if ($act == '-')
1033 {
1034 $this->diff["$index"]["$chunk"][] = $thearray = array(
1035 'line' => $content,
1036 'act' => '-',
1037 'oldlineno' => ++$lines['old'],
1038 'newlineno' => ''
1039 );
1040
1041 $key = count($this->diff["$index"]["$chunk"]) - 2;
1042 $thearray['INDEX'] = $key;
1043
1044 array_push($delstack, $thearray);
1045 }
1046 }
1047 // whitespace lines
1048 else
1049 {
1050 if (preg_match('#^Index: (.*?)$#', $line, $matches))
1051 {
1052 $index = $matches[1];
1053 $indexcounter = 1;
1054 $chunk = 0;
1055 continue;
1056 }
1057
1058 if ($property)
1059 {
1060 if (preg_match('#^__*_$#', trim($line)))
1061 {
1062 $viewsvn->debug("skipping: $line");
1063 continue;
1064 }
1065
1066 if (preg_match('#Name: (.*?)$#', $line, $matches))
1067 {
1068 $curprop = $matches[1];
1069 $viewsvn->debug("prop: $curprop");
1070 continue;
1071 }
1072 else
1073 {
1074 if (preg_match('#^\s+?\+(.*)#', $line, $matches))
1075 {
1076 $mode = 'add';
1077 $this->diff["$index"]['props']["$curprop"]['add'] .= $matches[1];
1078 }
1079 else if (preg_match('#^\s+?\-(.*)#', $line, $matches))
1080 {
1081 $mode = 'del';
1082 $this->diff["$index"]['props']["$curprop"]['del'] .= $matches[1];
1083 }
1084 else if (!preg_match('#^\s+[\+\- ](.*)#', $line) AND trim($line) != '')
1085 {
1086 $this->diff["$index"]['props']["$curprop"]["$mode"] .= "\n" . $line;
1087 }
1088 continue;
1089 }
1090 }
1091
1092 $this->diff["$index"]["$chunk"][] = array(
1093 'line' => '',
1094 'act' => '',
1095 'oldlineno' => ++$lines['old'],
1096 'newlineno' => ++$lines['new']
1097 );
1098
1099 $delstack = array();
1100 }
1101 }
1102 }
1103
1104 /**
1105 * Returns the amount of change that occured
1106 * between two lines
1107 *
1108 * @access private
1109 *
1110 * @param string Old line
1111 * @param string New line
1112 *
1113 * @return array Difference of positions
1114 */
1115 function fetch_diff_extent($old, $new)
1116 {
1117 global $viewsvn;
1118
1119 $start = 0;
1120 $min = min(strlen($old), strlen($new));
1121
1122 $viewsvn->debug("min1 = $min");
1123
1124 while ($start < $min AND $old["$start"] == $new["$start"])
1125 {
1126 $start++;
1127 }
1128
1129 $end = -1;
1130 $min = $min - $start;
1131
1132 $viewsvn->debug("min2 = $min");
1133
1134 $viewsvn->debug("checking: " . $old[ strlen($old) + $end ] . ' == ' . $new[ strlen($new) + $end ]);
1135
1136 while (-$end <= $min AND $old[ strlen($old) + $end ] == $new[ strlen($new) + $end ])
1137 {
1138 $end--;
1139 }
1140
1141 return array('start' => $start, 'end' => $end + 1);
1142 }
1143 }
1144
1145 /*=====================================================================*\
1146 || ###################################################################
1147 || # $HeadURL$
1148 || # $Id$
1149 || ###################################################################
1150 \*=====================================================================*/
1151 ?>