In short:
[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 SVNCommon
36 {
37 /**
38 * Path to the SVN binary
39 * @var string
40 */
41 var $svnpath;
42
43 /**
44 * Constructor
45 */
46 function SVNLib()
47 {
48 global $viewsvn;
49 }
50
51 /**
52 * Prepares data for output
53 *
54 * @access public
55 *
56 * @param string Standard data
57 *
58 * @return string Output-ready data
59 */
60 function format($string)
61 {
62 // convert entities
63 $string = htmlspecialchars($string);
64
65 // tabs to 5 spaces
66 $string = str_replace("\t", ' ', $string);
67
68 // spaces to nbsp
69 if (true)
70 {
71 $string = preg_replace('#( +)#e', '$this->format_spaces("\1")', $string);
72 }
73 // no word wrap
74 else
75 {
76 $string = str_replace(' ', '&nbsp;', $string);
77 }
78
79 // convert advanced diff
80 $string = str_replace(array('{@+' . '+}', '{@-' . '-}'), array('<span class="diff_add_bit">', '<span class="diff_del_bit">'), $string);
81 $string = str_replace(array('{/@+' . '+}', '{/@-' . '-}'), '</span>', $string);
82
83 // nl2br
84 $string = nl2br($string);
85
86 return $string;
87 }
88
89 /**
90 * Formats a SVN log message
91 *
92 * @access public
93 *
94 * @param string Unformatted log message
95 *
96 * @return string Output-ready log message
97 */
98 function format_log_message($message)
99 {
100 global $viewsvn;
101
102 $message = $viewsvn->entity_encode($message);
103
104 $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);
105
106 $list = false;
107 $lines = explode("\n", $message);
108 $message = '';
109 foreach ($lines AS $line)
110 {
111 if (preg_match('#^\s*?(\*|\-)\s?(.*)#', $line, $matches))
112 {
113 if ($list)
114 {
115 $message .= '<li>' . $matches[2] . '</li>';
116 }
117 else
118 {
119 $message .= '<ul class="list">';
120 $message .= '<li>' . $matches[2] . '</li>';
121 }
122 $list = true;
123 }
124 else
125 {
126 if ($list)
127 {
128 $message .= '</ul>';
129 $message .= $line;
130 }
131 else
132 {
133 $message .= $line;
134 $message .= '<br />';
135 }
136 $list = false;
137 }
138
139 $message .= "\n";
140 }
141
142 if ($list)
143 {
144 $message .= '</ul>';
145 }
146
147 $message = preg_replace('#(<br />)*$#', '', $message);
148
149 return $message;
150 }
151
152 // ###################################################################
153 /**
154 * Parses a date from Xquery XML outut
155 *
156 * @access public
157 *
158 * @param string Date string
159 *
160 * @return string Formatted and readable date string
161 */
162 function format_date_string($string)
163 {
164 // 2005-01-23T20:42:53.703057Z
165 return preg_replace('#(....)\-(..)\-(..)T(..):(..):(..).(.*)Z#e', 'gmdate("r", mktime(\4, \5, \6, \2, \3, \1))', $string);
166 }
167
168 /**
169 * Counts the spaces and replaces two or more ones
170 *
171 * @access private
172 *
173 * @param string Spaced string
174 *
175 * @return string &nbsp;'d string
176 */
177 function format_spaces($thestring)
178 {
179 if (strlen($thestring) >= 2)
180 {
181 $thestring = str_replace(' ', '&nbsp;', $thestring);
182 }
183
184 return $thestring;
185 }
186
187 /**
188 * Prints the file changed list
189 *
190 * @access public
191 *
192 * @public array List of file changes
193 * @public string The repository
194 * @public integer Current revision
195 *
196 * @return string Processed HTML
197 */
198 function construct_file_changes($changes, $repos, $revision)
199 {
200 global $viewsvn;
201
202 $files = '';
203
204 foreach ($changes AS $file)
205 {
206 switch ($file['action'])
207 {
208 case 'A':
209 $class = 'file_add';
210 $tooltip = $viewsvn->lang->string('Added');
211 break;
212 case 'D':
213 $class = 'file_delete';
214 $tooltip = $viewsvn->lang->string('Deleted');
215 break;
216 case 'M':
217 $class = 'file_modify';
218 $tooltip = $viewsvn->lang->string('Modified');
219 break;
220 case 'R':
221 $class = 'file_replace';
222 $tooltip = $viewsvn->lang->string('Replaced');
223 break;
224 }
225
226 $show['from'] = (bool)$file['from'];
227
228 if ($file['from'])
229 {
230 $class = 'file_move';
231 $tooltip = 'Moved/Copied';
232 preg_match('#(.*):([0-9]+)#', $file['from'], $matches);
233 $link['from'] = $viewsvn->paths->out('view.php' . $viewsvn->paths->fetch_rev_str(false, $matches[2]), $repos . $matches[1]);
234 }
235
236 $link['file'] = $viewsvn->paths->out('view.php' . $viewsvn->paths->fetch_rev_str(false, $revision), $repos . $file['file']);
237
238 eval('$files .= "' . $viewsvn->template->fetch('file_change') . '";');
239 }
240
241 return $files;
242 }
243
244 /**
245 * Generates a clean revision number
246 *
247 * @access public
248 *
249 * @param integer Revision number
250 *
251 * @return mixed Cleaned revision or HEAD
252 */
253 function rev($revision)
254 {
255 if (($revision = intval($revision)) < 1)
256 {
257 $revision = 'HEAD';
258 }
259 return $revision;
260 }
261 }
262
263 /**
264 * Annotation/blame system; constructs an array
265 * that is ready for output
266 *
267 * @package ViewSVN
268 * @version $Id$
269 */
270 class SVNBlame
271 {
272 /**
273 * Array of blame information
274 * @var array
275 */
276 var $blame = array();
277
278 /**
279 * Raw "svn blame" output
280 * @var array
281 */
282 var $rawoutput;
283
284 /**
285 * Constructor: create blame and store data
286 *
287 * @param string Repository
288 * @param string Path
289 * @param integer Revision
290 */
291 function SVNBlame($repos, $path, $revision)
292 {
293 global $viewsvn;
294
295 $this->rawoutput = $viewsvn->svn->blame($repos, $path, $revision);
296 $this->process();
297 }
298
299 /**
300 * Returns blame for display
301 *
302 * @access public
303 *
304 * @return array Blame data
305 */
306 function fetch()
307 {
308 return $this->blame;
309 }
310
311 /**
312 * Parses the blame data
313 *
314 * @access private
315 */
316 function process()
317 {
318 $lineno = 1;
319
320 foreach ($this->rawoutput AS $line)
321 {
322 if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)\s(.*)$#', $line, $matches))
323 {
324 $this->blame[] = array(
325 'rev' => $matches[1],
326 'author' => $matches[2],
327 'line' => $matches[3],
328 'lineno' => $lineno++
329 );
330 }
331 // a blank line
332 else if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)$#', $line, $matches))
333 {
334 $this->blame[] = array(
335 'rev' => $matches[1],
336 'author' => $matches[2],
337 'line' => '',
338 'lineno' => $lineno++
339 );
340 }
341 }
342 }
343 }
344
345 /**
346 * Log management system; creates a complex list
347 * of SVN log information
348 *
349 * @package ViewSVN
350 * @version $Id$
351 */
352 class SVNLog
353 {
354 /**
355 * Array of logs
356 * @var array
357 */
358 var $logs = array();
359
360 /**
361 * Raw "svn log" output
362 * @var array
363 */
364 var $rawoutput;
365
366 /**
367 * Constructor: create log store for the given file
368 *
369 * @param string Repository
370 * @param string Path
371 * @param integer Lower revision
372 * @param integer Higher revision
373 */
374 function SVNLog($repos, $path, $lorev, $hirev)
375 {
376 global $viewsvn;
377
378 $this->rawoutput = $viewsvn->svn->log($repos, $path, $lorev, $hirev);
379 $this->process();
380 }
381
382 /**
383 * Returns logs for display
384 *
385 * @access public
386 *
387 * @return array Log data
388 */
389 function fetch()
390 {
391 return $this->logs;
392 }
393
394 /**
395 * Splits up the raw output into a usable log
396 *
397 * @access private
398 */
399 function process()
400 {
401 $lastrev = 0;
402
403 for ($i = 1; $i <= count($this->rawoutput) - 1; $i++)
404 {
405 $line = $this->rawoutput["$i"];
406
407 if (preg_match('#^r([0-9]*) \| (.*?) \| (....-..-.. ..:..:..) ([0-9\-]*) \((.*?)\) \| ([0-9]*) lines?$#', $line, $matches))
408 {
409 if (isset($this->logs["$lastrev"]))
410 {
411 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
412 }
413
414 $this->logs["$matches[1]"] = array(
415 'rev' => $matches[1],
416 'author' => $matches[2],
417 'date' => $matches[3],
418 'timezone' => $matches[4],
419 'lines' => $matches[6],
420 'message' => ''
421 );
422
423 $lastrev = $matches[1];
424 }
425 else if (preg_match('#^\s+([ADMR])\s(.*)#', $line, $matches))
426 {
427 if (preg_match('#(.*) \(from (.*?)\)$#', $matches[2], $amatches))
428 {
429 $matches[2] = $amatches[1];
430 }
431
432 $this->logs["$lastrev"]['files'][] = array(
433 'action' => $matches[1],
434 'file' => trim($matches[2]),
435 'from' => (isset($amatches[2]) ? $amatches[2] : '')
436 );
437 }
438 else
439 {
440 if (trim($line) != 'Changed paths:')
441 {
442 $this->logs["$lastrev"]['message'] .= $line . "\n";
443 }
444 }
445 }
446
447 if (isset($this->logs["$lastrev"]))
448 {
449 $this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
450 }
451 }
452
453 /**
454 * Trims the last dash line off a message
455 *
456 * @access private
457 *
458 * @param string Message with annoying-ass line
459 *
460 * @return string Clean string
461 */
462 function strip_last_line($string)
463 {
464 return trim(preg_replace("#\n(.*?)\n$#", '', $string));
465 }
466 }
467
468 /**
469 * Diff system; constructs a diff array that
470 * is ready for output
471 *
472 * @package ViewSVN
473 */
474 class SVNDiff
475 {
476 /**
477 * Array of diff information
478 * @var array
479 */
480 var $diff = array();
481
482 /**
483 * Raw "svn diff" output
484 * @var array
485 */
486 var $rawoutput;
487
488 /**
489 * Constructor: create and store diff data
490 *
491 * @param string Repository
492 * @param string Path
493 * @param integer Lower revision
494 * @param integer Higher revision
495 */
496 function SVNDiff($repos, $path, $lorev, $hirev)
497 {
498 global $viewsvn;
499
500 $this->rawoutput = $viewsvn->svn->diff($repos, $path, $lorev, $hirev);
501 $this->process();
502 }
503
504 /**
505 * Returns diffs for display
506 *
507 * @access public
508 *
509 * @return array Diff data
510 */
511 function fetch()
512 {
513 return $this->diff;
514 }
515
516 /**
517 * Processes and prepares diff data
518 *
519 * @access private
520 */
521 function process()
522 {
523 global $viewsvn;
524
525 $chunk = 0;
526 $indexcounter = null;
527 $curprop = '';
528
529 $delstack = array();
530
531 foreach ($this->rawoutput AS $line)
532 {
533 if (preg_match('#^@@ \-([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@$#', $line, $bits))
534 {
535 $property = false;
536 $delstack = array();
537 $this->diff["$index"][ ++$chunk ]['hunk'] = array('old' => array('line' => $bits[1], 'count' => $bits[2]), 'new' => array('line' => $bits[3], 'count' => $bits[4]));
538 $lines['old'] = $this->diff["$index"]["$chunk"]['hunk']['old']['line'] - 1;
539 $lines['new'] = $this->diff["$index"]["$chunk"]['hunk']['new']['line'] - 1;
540 continue;
541 }
542 else if (preg_match('#^Property changes on: (.*?)$#', $line, $bits))
543 {
544 $property = true;
545 $index = $bits[1];
546 $this->diff["$index"]['props'] = array();
547 continue;
548 }
549
550 if ($indexcounter <= 3 AND $indexcounter !== null)
551 {
552 $indexcounter++;
553 continue;
554 }
555 else if ($indexcounter == 3)
556 {
557 $indexcounter = null;
558 continue;
559 }
560
561 if (preg_match('#^([\+\- ])(.*)#', $line, $matches) AND !$property)
562 {
563 $act = $matches[1];
564 $content = $matches[2];
565
566 if ($act == ' ')
567 {
568 $this->diff["$index"]["$chunk"][] = array(
569 'line' => $content,
570 'act' => '',
571 'oldlineno' => ++$lines['old'],
572 'newlineno' => ++$lines['new']
573 );
574
575 $delstack = array();
576 }
577 else if ($act == '+')
578 {
579 // potential line delta
580 if (count($delstack) > 0)
581 {
582 $lastline = array_shift($delstack);
583
584 if ($delta = @$this->fetch_diff_extent($lastline['line'], $content))
585 {
586 if (strlen($lastline['line']) > ($delta['start'] - $delta['end']))
587 {
588 $end = strlen($lastline['line']) + $delta['end'];
589 $viewsvn->debug("RM delta- = " . $end);
590 $change = '{@-' . '-}' . substr($lastline['line'], $delta['start'], $end - $delta['start']) . '{/@-' . '-}';
591 $this->diff["$index"]["$chunk"]["$lastline[INDEX]"]['line'] = substr($lastline['line'], 0, $delta['start']) . $change . substr($lastline['line'], $end);
592 }
593
594 if (strlen($content) > $delta['start'] - $delta['end'])
595 {
596 $end = strlen($content) + $delta['end'];
597 $viewsvn->debug("MK delta+ = " . $end);
598 $change = '{@+' . '+}' . substr($content, $delta['start'], $end - $delta['start']) . '{/@+' . '+}';
599 $content = substr($content, 0, $delta['start']) . $change . substr($content, $end);
600 }
601 }
602 }
603
604 $this->diff["$index"]["$chunk"][] = array(
605 'line' => $content,
606 'act' => '+',
607 'oldlineno' => '',
608 'newlineno' => ++$lines['new']
609 );
610 }
611 else if ($act == '-')
612 {
613 $this->diff["$index"]["$chunk"][] = $thearray = array(
614 'line' => $content,
615 'act' => '-',
616 'oldlineno' => ++$lines['old'],
617 'newlineno' => ''
618 );
619
620 $key = count($this->diff["$index"]["$chunk"]) - 2;
621 $thearray['INDEX'] = $key;
622
623 array_push($delstack, $thearray);
624 }
625 }
626 // whitespace lines
627 else
628 {
629 if (preg_match('#^Index: (.*?)$#', $line, $matches))
630 {
631 $index = $matches[1];
632 $indexcounter = 1;
633 $chunk = 0;
634 continue;
635 }
636
637 if ($property)
638 {
639 if (preg_match('#^__*_$#', trim($line)))
640 {
641 $viewsvn->debug("skipping: $line");
642 continue;
643 }
644
645 if (preg_match('#Name: (.*?)$#', $line, $matches))
646 {
647 $curprop = $matches[1];
648 $viewsvn->debug("prop: $curprop");
649 continue;
650 }
651 else
652 {
653 if (preg_match('#^\s+?\+(.*)#', $line, $matches))
654 {
655 $mode = 'add';
656 $this->diff["$index"]['props']["$curprop"]['add'] .= $matches[1];
657 }
658 else if (preg_match('#^\s+?\-(.*)#', $line, $matches))
659 {
660 $mode = 'del';
661 $this->diff["$index"]['props']["$curprop"]['del'] .= $matches[1];
662 }
663 else if (!preg_match('#^\s+[\+\- ](.*)#', $line) AND trim($line) != '')
664 {
665 $this->diff["$index"]['props']["$curprop"]["$mode"] .= "\n" . $line;
666 }
667 continue;
668 }
669 }
670
671 $this->diff["$index"]["$chunk"][] = array(
672 'line' => '',
673 'act' => '',
674 'oldlineno' => ++$lines['old'],
675 'newlineno' => ++$lines['new']
676 );
677
678 $delstack = array();
679 }
680 }
681 }
682
683 /**
684 * Returns the amount of change that occured
685 * between two lines
686 *
687 * @access private
688 *
689 * @param string Old line
690 * @param string New line
691 *
692 * @return array Difference of positions
693 */
694 function fetch_diff_extent($old, $new)
695 {
696 global $viewsvn;
697
698 $start = 0;
699 $min = min(strlen($old), strlen($new));
700
701 $viewsvn->debug("min1 = $min");
702
703 while ($start < $min AND $old["$start"] == $new["$start"])
704 {
705 $start++;
706 }
707
708 $end = -1;
709 $min = $min - $start;
710
711 $viewsvn->debug("min2 = $min");
712
713 $viewsvn->debug("checking: " . $old[ strlen($old) + $end ] . ' == ' . $new[ strlen($new) + $end ]);
714
715 while (-$end <= $min AND $old[ strlen($old) + $end ] == $new[ strlen($new) + $end ])
716 {
717 $end--;
718 }
719
720 return array('start' => $start, 'end' => $end + 1);
721 }
722 }
723
724 /*=====================================================================*\
725 || ###################################################################
726 || # $HeadURL$
727 || # $Id$
728 || ###################################################################
729 \*=====================================================================*/
730 ?>