Rev Author Line No. Line
130 kaklik 1 <?php
2 # vim:et:ts=3:sts=3:sw=3:fdm=marker:
3  
4 // WebSVN - Subversion repository viewing via the web using PHP
5 // Copyright © 2004-2006 Tim Armes, Matt Sicker
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; either version 2 of the License, or
10 // (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 //
21 // --
22 //
23 // svn-look.inc
24 //
25 // Svn bindings
26 //
27 // These binding currently use the svn command line to achieve their goal. Once a proper
28 // SWIG binding has been produced for PHP, there'll be an option to use that instead.
29  
30 require_once("include/utils.inc");
31  
32 // {{{ Classes for retaining log information ---
33  
34 $debugxml = false;
35  
36 Class SVNMod
37 {
38 var $action = '';
39 var $copyfrom = '';
40 var $copyrev = '';
41 var $path = '';
42 }
43  
44 Class SVNLogEntry
45 {
46 var $rev = 1;
47 var $author = '';
48 var $date = '';
49 var $committime;
50 var $age = '';
51 var $msg = '';
52 var $path = '';
53  
54 var $mods;
55 var $curMod;
56 }
57  
58 Class SVNLog
59 {
60 var $entries; // Array of entries
61 var $curEntry; // Current entry
62  
63 var $path = ''; // Temporary variable used to trace path history
64  
65 // findEntry
66 //
67 // Return the entry for a given revision
68  
69 function findEntry($rev)
70 {
71 foreach ($this->entries as $index => $entry)
72 {
73 if ($entry->rev == $rev)
74 return $index;
75  
76 }
77 }
78 }
79  
80 // }}}
81  
82 // {{{ XML parsing functions---
83  
84 $curLog = 0;
85 $curTag = '';
86  
87 // {{{ startElement
88  
89 function startElement($parser, $name, $attrs)
90 {
91 global $curLog, $curTag, $debugxml;
92  
93 switch ($name)
94 {
95 case "LOGENTRY":
96 if ($debugxml) print "Creating new log entry\n";
97 $curLog->curEntry = new SVNLogEntry;
98 $curLog->curEntry->mods = array();
99  
100 $curLog->curEntry->path = $curLog->path;
101  
102 if (sizeof($attrs))
103 {
104 while (list($k, $v) = each($attrs))
105 {
106 switch ($k)
107 {
108 case "REVISION":
109 if ($debugxml) print "Revision $v\n";
110 $curLog->curEntry->rev = $v;
111 break;
112 }
113 }
114 }
115 break;
116  
117 case "PATH":
118 if ($debugxml) print "Creating new path\n";
119 $curLog->curEntry->curMod = new SVNMod;
120  
121 if (sizeof($attrs))
122 {
123 while (list($k, $v) = each($attrs))
124 {
125 switch ($k)
126 {
127 case "ACTION":
128 if ($debugxml) print "Action $v\n";
129 $curLog->curEntry->curMod->action = $v;
130 break;
131  
132 case "COPYFROM-PATH":
133 if ($debugxml) print "Copy from: $v\n";
134 $curLog->curEntry->curMod->copyfrom = $v;
135 break;
136  
137 case "COPYFROM-REV":
138 $curLog->curEntry->curMod->copyrev = $v;
139 break;
140 }
141 }
142 }
143  
144 $curTag = $name;
145 break;
146  
147 default:
148 $curTag = $name;
149 break;
150 }
151 }
152  
153 // }}}
154  
155 // {{{ endElement
156  
157 function endElement($parser, $name)
158 {
159 global $curLog, $debugxml, $curTag;
160  
161 switch ($name)
162 {
163 case "LOGENTRY":
164 if ($debugxml) print "Ending new log entry\n";
165 $curLog->entries[] = $curLog->curEntry;
166 break;
167  
168 case "PATH":
169 if ($debugxml) print "Ending path\n";
170 $curLog->curEntry->mods[] = $curLog->curEntry->curMod;
171 break;
172  
173 case "MSG":
174 $curLog->curEntry->msg = trim($curLog->curEntry->msg);
175 if ($debugxml) print "Completed msg = '".$curLog->curEntry->msg."'\n";
176 break;
177 }
178  
179 $curTag = "";
180 }
181  
182 // }}}
183  
184 // {{{ characterData
185  
186 function characterData($parser, $data)
187 {
188 global $curLog, $curTag, $lang, $debugxml;
189  
190 switch ($curTag)
191 {
192 case "AUTHOR":
193 if ($debugxml) print "Author: $data\n";
194 if (empty($data)) return;
195 $curLog->curEntry->author .= htmlentities($data, ENT_COMPAT, "UTF-8");
196 break;
197  
198 case "DATE":
199 if ($debugxml) print "Date: $data\n";
200 $data = trim($data);
201 if (empty($data)) return;
202  
203 sscanf($data, "%d-%d-%dT%d:%d:%d.", $y, $mo, $d, $h, $m, $s);
204  
205 $mo = substr("00".$mo, -2);
206 $d = substr("00".$d, -2);
207 $h = substr("00".$h, -2);
208 $m = substr("00".$m, -2);
209 $s = substr("00".$s, -2);
210  
211 $curLog->curEntry->date = "$y-$mo-$d $h:$m:$s GMT";
212  
213 $committime = strtotime($curLog->curEntry->date);
214 $curLog->curEntry->committime = $committime;
215 $curtime = time();
216  
217 // Get the number of seconds since the commit
218 $agesecs = $curtime - $committime;
219 if ($agesecs < 0) $agesecs = 0;
220  
221 $curLog->curEntry->age = datetimeFormatDuration($agesecs, true, true);
222  
223 break;
224  
225 case "MSG":
226 if ($debugxml) print "Msg: '$data'\n";
227 $curLog->curEntry->msg .= htmlentities($data, ENT_COMPAT, "UTF-8");
228 break;
229  
230 case "PATH":
231 if ($debugxml) print "Path name: '$data'\n";
232 $data = trim($data);
233 if (empty($data)) return;
234  
235 $curLog->curEntry->curMod->path .= $data;
236  
237 // The XML returned when a file is renamed/branched in inconsistant. In the case
238 // of a branch, the path information doesn't include the leafname. In the case of
239 // a rename, it does. Ludicrous.
240  
241 if (!empty($curLog->path))
242 {
243 $pos = strrpos($curLog->path, "/");
244 $curpath = substr($curLog->path, 0, $pos);
245 $leafname = substr($curLog->path, $pos + 1);
246 }
247 else
248 {
249 $curpath = "";
250 $leafname = "";
251 }
252  
253 if ($curLog->curEntry->curMod->action == "A")
254 {
255 if ($debugxml) print "Examining added path '".$curLog->curEntry->curMod->copyfrom."' - Current path = '$curpath', leafname = '$leafname'\n";
256 if ($data == $curLog->path) // For directories and renames
257 {
258 if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."'\n";
259 $curLog->path = $curLog->curEntry->curMod->copyfrom;
260 }
261 else if ($data == $curpath || $data == $curpath."/") // Logs of files that have moved due to branching
262 {
263 if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."/$leafname'\n";
264 $curLog->path = $curLog->curEntry->curMod->copyfrom."/$leafname";
265 }
266 }
267 break;
268 }
269 }
270  
271 // }}}
272  
273 // }}}
274  
275 // Function returns true if the give entry in a directory tree is at the top level
276  
277 function _topLevel($entry)
278 {
279 // To be at top level, there must be one space before the entry
280 return (strlen($entry) > 1 && $entry{0} == " " && $entry{1} != " ");
281 }
282  
283 // Function to sort two given directory entries. Directories go at the top
284  
285 function _dirSort($e1, $e2)
286 {
287 $isDir1 = $e1{strlen($e1) - 1} == "/";
288 $isDir2 = $e2{strlen($e2) - 1} == "/";
289  
290 if ($isDir1 && !$isDir2) return -1;
291 if ($isDir2 && !$isDir1) return 1;
292  
293 return strnatcasecmp($e1, $e2);
294 }
295  
296 // Return the revision string to pass to a command
297  
298 function _revStr($rev)
299 {
300 if ($rev > 0)
301 return "-r $rev";
302 else
303 return "";
304 }
305  
306 // {{{ encodePath
307  
308 // Function to encode a URL without encoding the /'s
309  
310 function encodePath($uri)
311 {
312 global $config;
313  
314 $uri = str_replace(DIRECTORY_SEPARATOR, "/", $uri);
315  
316 $parts = explode('/', $uri);
317 for ($i = 0; $i < count($parts); $i++)
139 root 318 {
319  
130 kaklik 320 {
321 $parts[$i] = mb_convert_encoding($parts[$i], "UTF-8", mb_detect_encoding($parts[$i]));
322 }
323
324  
325 }
326
327  
328
329  
330
331  
332
333  
334 if ( $config->serverIsWindows==true )
335 {
336 if ( substr($uri, 0,2)=="//" )
337 $uri="\\".substr($uri, 2, strlen($uri));
338 }
339
340  
341 }
342
343  
344
345  
346
347  
348 {
349 var $repConfig;
350
351  
352 {
353 $this->repConfig = $repConfig;
354 }
355
356  
357
358  
359 {
360 global $config, $locwebsvnreal;
361
362  
363
364  
365
366  
367 {
368 $headlog = $this->getLog("/", "", "", true, 1);
369 $rev = $headlog->entries[0]->rev;
370 }
371
372  
373 $output = runCommand($config->svn." list $revstr ".$this->repConfig->svnParams().quote($path), true);
374
375  
376 {
377 if ($entry != "")
378 $tree[] = $entry;
379 }
380
381  
382 usort($tree, "_dirSort");
383
384  
385 }
386
387  
388
389  
390 //
391 // Distill line-spanning syntax highlighting so that each line can stand alone
392 // (when invoking on the first line, $attributes should be an empty array)
393 // Invoked to make sure all open syntax highlighting tags (<font>, <i>, <b>, etc.)
394 // are closed at the end of each line and re-opened on the next line
395
396  
397 {
398 $hline = "";
399
400  
401 foreach($attributes as $attr)
402 {
403 $hline.=$attr['text'];
404 }
405
406  
407 $hline.=$line;
408
409  
410 for ($line = strstr($line, "<"); $line; $line = strstr(substr($line,1), "<"))
411 {
412 // if this closes a tag, remove most recent corresponding opener
413 if (substr($line,1,1) == "/")
414 {
415 $tagNamLen = strcspn($line, "> \t", 2);
416 $tagNam = substr($line,2,$tagNamLen);
417 foreach(array_reverse(array_keys($attributes)) as $k)
418 {
419 if ($attributes[$k]['tag'] == $tagNam)
420 {
421 unset($attributes[$k]);
422 break;
423 }
424 }
425 }
426 else
427 // if this opens a tag, add it to the list
428 {
429 $tagNamLen = strcspn($line, "> \t", 1);
430 $tagNam = substr($line,1,$tagNamLen);
431 $tagLen = strcspn($line, ">") + 1;
432 $attributes[] = array('tag' => $tagNam, 'text' => substr($line,0,$tagLen));
433 }
434 }
435
436  
437 foreach(array_reverse($attributes) as $attr)
438 {
439 $hline.="</".$attr['tag'].">";
440 }
441
442  
443 }
444
445  
446
447  
448 //
449 // Dump the content of a file to the given filename
450
451  
452 {
453 global $config, $extEnscript;
454
455  
456
457  
458 if ($filename == "")
459 {
460 $path = encodepath($this->repConfig->path.$path);
461 passthru(quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." $pipe", false));
462 return;
463 }
464
465  
466
467  
468 $l = @$extEnscript[$ext];
469
470  
471 {
472 // Output the file to the filename
473 $path = encodepath($this->repConfig->path.$path);
474 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);
475 @exec($cmd);
476
477  
478 $content = highlight_file($filename, true);
479
480  
481 $f = fopen($filename, "w");
482 if ($f)
483 {
484 // The highlight file function doesn't deal with line endings very nicely at all. We'll have to do it
485 // by hand.
486
487  
488 $pos = strpos($content, "\n");
489 $content = substr($content, $pos+1);
490
491  
492
493  
494 {
495 // If we need each line independently highlighted (e.g. for diff or blame)
496 // hen we'll need to filter the output of the highlighter
497 // to make sure tags like <font>, <i> or <b> don't span lines
498
499  
500 // are in effect from one line to the next
501 $attributes = array(); // start with no attributes in effect
502
503  
504 {
505 fputs($f, $this->highlightLine(rtrim($line),$attributes)."\n");
506 }
507 }
508 else
509 {
510 foreach ($content as $line)
511 {
512 fputs($f, rtrim($line)."\n");
513 }
514 }
515
516  
517 }
518 }
519 else
520 {
521 if ($config->useEnscript)
522 {
523 // Get the files, feed it through enscript, then remove the enscript headers using sed
524 //
525 // Note that the sec command returns only the part of the file between <PRE> and </PRE>.
526 // It's complicated because it's designed not to return those lines themselves.
527
528  
529 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." | ".
530 $config->enscript." --language=html ".
531 ($l ? "--color --pretty-print=$l" : "")." -o - | ".
532 $config->sed." -n ".$config->quote."1,/^<PRE.$/!{/^<\\/PRE.$/,/^<PRE.$/!p;}".$config->quote." > $filename", false);
533 @exec($cmd);
534 }
535 else
536 {
537 $path = encodepath(str_replace(DIRECTORY_SEPARATOR, "/", $this->repConfig->path.$path));
538 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);
539 @exec($cmd);
540 }
541 }
542 }
543
544  
545
546  
547 //
548 // Print the contents of a file without filling up Apache's memory
549
550  
551 {
552 global $config, $extEnscript;
553
554  
555 $pre = false;
556
557  
558
559  
560 $l = @$extEnscript[$ext];
561
562  
563 if ($l == "php")
564 {
565 $tmp = tempnam("temp", "wsvn");
566
567  
568 $path = encodepath($this->repConfig->path.$path);
569 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $tmp", false);
570 @exec($cmd);
571 highlight_file($tmp);
572 unlink($tmp);
573 }
574 else
575 {
576 if ($config->useEnscript)
577 {
578 $path = encodepath($this->repConfig->path.$path);
579 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." | ".
580 $config->enscript." --language=html ".
581 ($l ? "--color --pretty-print=$l" : "")." -o - | ".
582 $config->sed." -n ".$config->quote."/^<PRE.$/,/^<\\/PRE.$/p".$config->quote." 2>&1", false);
583
584  
585 return;
586 }
587 else
588 {
589 $path = encodepath($this->repConfig->path.$path);
590 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." 2>&1", false);
591
592  
593 return;
594
595  
596 }
597
598  
599 echo "<PRE>";
600
601  
602 {
603 $line = fgets($result, 1024);
604 if ($pre) $line = replaceEntities($line, $this->repConfig);
605
606  
607 }
608
609  
610 echo "</PRE>";
611
612  
613 }
614 }
615
616  
617
618  
619 //
620 // Dump the blame content of a file to the given filename
621
622  
623 {
624 global $config;
625
626  
627
628  
629 $cmd = quoteCommand($config->svn." blame $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);
630
631  
632 }
633
634  
635
636  
637
638  
639 {
640 global $config;
641
642  
643
644  
645 $ret = runCommand($config->svn." propget $property $revstr ".$this->repConfig->svnParams().quote($path), true);
646
647  
648 if (count($ret))
649 unset($ret[count($ret) - 1]);
650
651  
652 }
653
654  
655
656  
657 //
658 // Exports the directory to the given location
659
660  
661 {
662 global $config;
663
664  
665
666  
667 $cmd = quoteCommand($config->svn." export $revstr ".$this->repConfig->svnParams().quote($path)." ".quote($filename), false);
668
669  
670 }
671
672  
673
674  
675
676  
677 {
678 global $config, $curLog, $vars, $lang;
679
680  
681 xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true);
682 xml_set_element_handler($xml_parser, "startElement", "endElement");
683 xml_set_character_data_handler($xml_parser, "characterData");
684
685  
686 // the trailing slash from the path for comparison purposes
687
688  
689 $path = substr($path, 0, -1);
690
691  
692 $curLog->entries = array();
693 $curLog->path = $path;
694
695  
696
697  
698 $revStr = "-r$brev:$erev";
699 else if ($brev)
700 $revStr = "-r$brev:1";
701
702  
703 $revStr .= " --limit $limit";
704
705  
706 $path = encodepath($this->repConfig->path.$path);
707 $info = "--verbose";
708 if ($quiet)
709 $info = "--quiet";
710
711  
712
713  
714 {
715 $firstline = true;
716 while (!feof($handle))
717 {
718 $line = fgets($handle);
719 if (!xml_parse($xml_parser, $line, feof($handle)))
720 {
721 if (xml_get_error_code($xml_parser) != 5)
722 {
723 die(sprintf("XML error: %s (%d) at line %d column %d byte %d<br>cmd: %s<nr>",
724 xml_error_string(xml_get_error_code($xml_parser)),
725 xml_get_error_code($xml_parser),
726 xml_get_current_line_number($xml_parser),
727 xml_get_current_column_number($xml_parser),
728 xml_get_current_byte_index($xml_parser),
729 $cmd));
730 }
731 else
732 {
733 $vars["error"] = $lang["UNKNOWNREVISION"];
734 return 0;
735 }
736 }
737 }
738 pclose($handle);
739 }
740
741  
742 return $curLog;
743 }
744
745  
746
747  
748
749  
750
751  
752 {
753 global $config;
754
755  
756
757  
758 {
759 $major = $matches[1];
760 $minor = $matches[2];
761 }
762
763  
764 $config->setSubversionMinorVersion($minor);
765 }
766
767  
768
769  
770