<?php# vim:et:ts=3:sts=3:sw=3:fdm=marker:// WebSVN - Subversion repository viewing via the web using PHP// Copyright © 2004-2006 Tim Armes, Matt Sicker//// This program is free software; you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation; either version 2 of the License, or// (at your option) any later version.//// This program is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with this program; if not, write to the Free Software// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA//// --//// svn-look.inc//// Svn bindings//// These binding currently use the svn command line to achieve their goal. Once a proper// SWIG binding has been produced for PHP, there'll be an option to use that instead.require_once("include/utils.inc");// {{{ Classes for retaining log information ---$debugxml = false;Class SVNMod{var $action = '';var $copyfrom = '';var $copyrev = '';var $path = '';}Class SVNLogEntry{var $rev = 1;var $author = '';var $date = '';var $committime;var $age = '';var $msg = '';var $path = '';var $mods;var $curMod;}Class SVNLog{var $entries; // Array of entriesvar $curEntry; // Current entryvar $path = ''; // Temporary variable used to trace path history// findEntry//// Return the entry for a given revisionfunction findEntry($rev){foreach ($this->entries as $index => $entry){if ($entry->rev == $rev)return $index;}}}// }}}// {{{ XML parsing functions---$curLog = 0;$curTag = '';// {{{ startElementfunction startElement($parser, $name, $attrs){global $curLog, $curTag, $debugxml;switch ($name){case "LOGENTRY":if ($debugxml) print "Creating new log entry\n";$curLog->curEntry = new SVNLogEntry;$curLog->curEntry->mods = array();$curLog->curEntry->path = $curLog->path;if (sizeof($attrs)){while (list($k, $v) = each($attrs)){switch ($k){case "REVISION":if ($debugxml) print "Revision $v\n";$curLog->curEntry->rev = $v;break;}}}break;case "PATH":if ($debugxml) print "Creating new path\n";$curLog->curEntry->curMod = new SVNMod;if (sizeof($attrs)){while (list($k, $v) = each($attrs)){switch ($k){case "ACTION":if ($debugxml) print "Action $v\n";$curLog->curEntry->curMod->action = $v;break;case "COPYFROM-PATH":if ($debugxml) print "Copy from: $v\n";$curLog->curEntry->curMod->copyfrom = $v;break;case "COPYFROM-REV":$curLog->curEntry->curMod->copyrev = $v;break;}}}$curTag = $name;break;default:$curTag = $name;break;}}// }}}// {{{ endElementfunction endElement($parser, $name){global $curLog, $debugxml, $curTag;switch ($name){case "LOGENTRY":if ($debugxml) print "Ending new log entry\n";$curLog->entries[] = $curLog->curEntry;break;case "PATH":if ($debugxml) print "Ending path\n";$curLog->curEntry->mods[] = $curLog->curEntry->curMod;break;case "MSG":$curLog->curEntry->msg = trim($curLog->curEntry->msg);if ($debugxml) print "Completed msg = '".$curLog->curEntry->msg."'\n";break;}$curTag = "";}// }}}// {{{ characterDatafunction characterData($parser, $data){global $curLog, $curTag, $lang, $debugxml;switch ($curTag){case "AUTHOR":if ($debugxml) print "Author: $data\n";if (empty($data)) return;$curLog->curEntry->author .= htmlentities($data, ENT_COMPAT, "UTF-8");break;case "DATE":if ($debugxml) print "Date: $data\n";$data = trim($data);if (empty($data)) return;sscanf($data, "%d-%d-%dT%d:%d:%d.", $y, $mo, $d, $h, $m, $s);$mo = substr("00".$mo, -2);$d = substr("00".$d, -2);$h = substr("00".$h, -2);$m = substr("00".$m, -2);$s = substr("00".$s, -2);$curLog->curEntry->date = "$y-$mo-$d $h:$m:$s GMT";$committime = strtotime($curLog->curEntry->date);$curLog->curEntry->committime = $committime;$curtime = time();// Get the number of seconds since the commit$agesecs = $curtime - $committime;if ($agesecs < 0) $agesecs = 0;$curLog->curEntry->age = datetimeFormatDuration($agesecs, true, true);break;case "MSG":if ($debugxml) print "Msg: '$data'\n";$curLog->curEntry->msg .= htmlentities($data, ENT_COMPAT, "UTF-8");break;case "PATH":if ($debugxml) print "Path name: '$data'\n";$data = trim($data);if (empty($data)) return;$curLog->curEntry->curMod->path .= $data;// The XML returned when a file is renamed/branched in inconsistant. In the case// of a branch, the path information doesn't include the leafname. In the case of// a rename, it does. Ludicrous.if (!empty($curLog->path)){$pos = strrpos($curLog->path, "/");$curpath = substr($curLog->path, 0, $pos);$leafname = substr($curLog->path, $pos + 1);}else{$curpath = "";$leafname = "";}if ($curLog->curEntry->curMod->action == "A"){if ($debugxml) print "Examining added path '".$curLog->curEntry->curMod->copyfrom."' - Current path = '$curpath', leafname = '$leafname'\n";if ($data == $curLog->path) // For directories and renames{if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."'\n";$curLog->path = $curLog->curEntry->curMod->copyfrom;}else if ($data == $curpath || $data == $curpath."/") // Logs of files that have moved due to branching{if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."/$leafname'\n";$curLog->path = $curLog->curEntry->curMod->copyfrom."/$leafname";}}break;}}// }}}// }}}// Function returns true if the give entry in a directory tree is at the top levelfunction _topLevel($entry){// To be at top level, there must be one space before the entryreturn (strlen($entry) > 1 && $entry{0} == " " && $entry{1} != " ");}// Function to sort two given directory entries. Directories go at the topfunction _dirSort($e1, $e2){$isDir1 = $e1{strlen($e1) - 1} == "/";$isDir2 = $e2{strlen($e2) - 1} == "/";if ($isDir1 && !$isDir2) return -1;if ($isDir2 && !$isDir1) return 1;return strnatcasecmp($e1, $e2);}// Return the revision string to pass to a commandfunction _revStr($rev){if ($rev > 0)return "-r $rev";elsereturn "";}// {{{ encodePath// Function to encode a URL without encoding the /'sfunction encodePath($uri){global $config;$uri = str_replace(DIRECTORY_SEPARATOR, "/", $uri);$parts = explode('/', $uri);for ($i = 0; $i < count($parts); $i++){if ( function_exists("mb_detect_encoding") && function_exists("mb_convert_encoding")){$parts[$i] = mb_convert_encoding($parts[$i], "UTF-8", mb_detect_encoding($parts[$i]));}$parts[$i] = rawurlencode($parts[$i]);}$uri = implode('/', $parts);// Quick hack. Subversion seems to have a bug surrounding the use of %3A instead of :$uri = str_replace("%3A" ,":", $uri);// Correct for Window share namesif ( $config->serverIsWindows==true ){if ( substr($uri, 0,2)=="//" )$uri="\\".substr($uri, 2, strlen($uri));}return $uri;}// }}}// The SVNRepository ClassClass SVNRepository{var $repConfig;function SVNRepository($repConfig){$this->repConfig = $repConfig;}// {{{ dirContentsfunction dirContents($path, $rev = 0){global $config, $locwebsvnreal;$revstr = _revStr($rev);$tree = array();if ($rev == 0){$headlog = $this->getLog("/", "", "", true, 1);$rev = $headlog->entries[0]->rev;}$path = encodepath($this->repConfig->path.$path);$output = runCommand($config->svn." list $revstr ".$this->repConfig->svnParams().quote($path), true);foreach ($output as $entry){if ($entry != "")$tree[] = $entry;}// Sort the entries into alphabetical order with the directories at the top of the listusort($tree, "_dirSort");return $tree;}// }}}// {{{ highlightLine//// Distill line-spanning syntax highlighting so that each line can stand alone// (when invoking on the first line, $attributes should be an empty array)// Invoked to make sure all open syntax highlighting tags (<font>, <i>, <b>, etc.)// are closed at the end of each line and re-opened on the next linefunction highlightLine($line, &$attributes){$hline = "";// Apply any highlighting in effect from the previous lineforeach($attributes as $attr){$hline.=$attr['text'];}// append the new line$hline.=$line;// update attributesfor ($line = strstr($line, "<"); $line; $line = strstr(substr($line,1), "<")){// if this closes a tag, remove most recent corresponding openerif (substr($line,1,1) == "/"){$tagNamLen = strcspn($line, "> \t", 2);$tagNam = substr($line,2,$tagNamLen);foreach(array_reverse(array_keys($attributes)) as $k){if ($attributes[$k]['tag'] == $tagNam){unset($attributes[$k]);break;}}}else// if this opens a tag, add it to the list{$tagNamLen = strcspn($line, "> \t", 1);$tagNam = substr($line,1,$tagNamLen);$tagLen = strcspn($line, ">") + 1;$attributes[] = array('tag' => $tagNam, 'text' => substr($line,0,$tagLen));}}// close any still-open tagsforeach(array_reverse($attributes) as $attr){$hline.="</".$attr['tag'].">";}return($hline);}// }}}// {{{ getFileContents//// Dump the content of a file to the given filenamefunction getFileContents($path, $filename, $rev = 0, $pipe = "", $perLineHighlighting = false){global $config, $extEnscript;$revstr = _revStr($rev);// If there's no filename, we'll just deliver the contents as it is to the userif ($filename == ""){$path = encodepath($this->repConfig->path.$path);passthru(quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." $pipe", false));return;}// Get the file contents info$ext = strrchr($path, ".");$l = @$extEnscript[$ext];if ($l == "php"){// Output the file to the filename$path = encodepath($this->repConfig->path.$path);$cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);@exec($cmd);// Get the file as a string (memory hogging, but we have no other options)$content = highlight_file($filename, true);// Destroy the previous version, and replace it with the highlighted version$f = fopen($filename, "w");if ($f){// The highlight file function doesn't deal with line endings very nicely at all. We'll have to do it// by hand.// Remove the first line generated by highlight()$pos = strpos($content, "\n");$content = substr($content, $pos+1);$content = explode("<br />", $content);if ($perLineHighlighting){// If we need each line independently highlighted (e.g. for diff or blame)// hen we'll need to filter the output of the highlighter// to make sure tags like <font>, <i> or <b> don't span lines// $attributes is used to remember what highlighting attributes// are in effect from one line to the next$attributes = array(); // start with no attributes in effectforeach ($content as $line){fputs($f, $this->highlightLine(rtrim($line),$attributes)."\n");}}else{foreach ($content as $line){fputs($f, rtrim($line)."\n");}}fclose($f);}}else{if ($config->useEnscript){// Get the files, feed it through enscript, then remove the enscript headers using sed//// Note that the sec command returns only the part of the file between <PRE> and </PRE>.// It's complicated because it's designed not to return those lines themselves.$path = encodepath($this->repConfig->path.$path);$cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." | ".$config->enscript." --language=html ".($l ? "--color --pretty-print=$l" : "")." -o - | ".$config->sed." -n ".$config->quote."1,/^<PRE.$/!{/^<\\/PRE.$/,/^<PRE.$/!p;}".$config->quote." > $filename", false);@exec($cmd);}else{$path = encodepath(str_replace(DIRECTORY_SEPARATOR, "/", $this->repConfig->path.$path));$cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);@exec($cmd);}}}// }}}// {{{ listFileContents//// Print the contents of a file without filling up Apache's memoryfunction listFileContents($path, $rev = 0){global $config, $extEnscript;$revstr = _revStr($rev);$pre = false;// Get the file contents info$ext = strrchr($path, ".");$l = @$extEnscript[$ext];// Deal with php highlighting internallyif ($l == "php"){$tmp = tempnam("temp", "wsvn");// Output the file to a temporary file$path = encodepath($this->repConfig->path.$path);$cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $tmp", false);@exec($cmd);highlight_file($tmp);unlink($tmp);}else{if ($config->useEnscript){$path = encodepath($this->repConfig->path.$path);$cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." | ".$config->enscript." --language=html ".($l ? "--color --pretty-print=$l" : "")." -o - | ".$config->sed." -n ".$config->quote."/^<PRE.$/,/^<\\/PRE.$/p".$config->quote." 2>&1", false);if (!($result = popen($cmd, "r")))return;}else{$path = encodepath($this->repConfig->path.$path);$cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." 2>&1", false);if (!($result = popen($cmd, "r")))return;$pre = true;}if ($pre)echo "<PRE>";while (!feof($result)){$line = fgets($result, 1024);if ($pre) $line = replaceEntities($line, $this->repConfig);print hardspace($line);}if ($pre)echo "</PRE>";pclose($result);}}// }}}// {{{ getBlameDetails//// Dump the blame content of a file to the given filenamefunction getBlameDetails($path, $filename, $rev = 0){global $config;$revstr = _revStr($rev);$path = encodepath($this->repConfig->path.$path);$cmd = quoteCommand($config->svn." blame $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);@exec($cmd);}// }}}// {{{ getPropertyfunction getProperty($path, $property, $rev = 0){global $config;$revstr = _revStr($rev);$path = encodepath($this->repConfig->path.$path);$ret = runCommand($config->svn." propget $property $revstr ".$this->repConfig->svnParams().quote($path), true);// Remove the surplus newlineif (count($ret))unset($ret[count($ret) - 1]);return implode("\n", $ret);}// }}}// {{{ exportDirectory//// Exports the directory to the given locationfunction exportDirectory($path, $filename, $rev = 0){global $config;$revstr = _revStr($rev);$path = encodepath($this->repConfig->path.$path);$cmd = quoteCommand($config->svn." export $revstr ".$this->repConfig->svnParams().quote($path)." ".quote($filename), false);@exec($cmd);}// }}}// {{{ getLogfunction getLog($path, $brev = "", $erev = 1, $quiet = false, $limit = 2){global $config, $curLog, $vars, $lang;$xml_parser = xml_parser_create("UTF-8");xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true);xml_set_element_handler($xml_parser, "startElement", "endElement");xml_set_character_data_handler($xml_parser, "characterData");// Since directories returned by svn log don't have trailing slashes (:-(), we need to remove// the trailing slash from the path for comparison purposesif ($path{strlen($path) - 1} == "/" && $path != "/")$path = substr($path, 0, -1);$curLog = new SVNLog;$curLog->entries = array();$curLog->path = $path;$revStr = "";if ($brev && $erev)$revStr = "-r$brev:$erev";else if ($brev)$revStr = "-r$brev:1";if (($config->subversionMajorVersion > 1 || $config->subversionMinorVersion >=2) && $limit != 0)$revStr .= " --limit $limit";// Get the log info$path = encodepath($this->repConfig->path.$path);$info = "--verbose";if ($quiet)$info = "--quiet";$cmd = quoteCommand($config->svn." log --xml $info $revStr ".$this->repConfig->svnParams().quote($path), false);if ($handle = popen($cmd, "r")){$firstline = true;while (!feof($handle)){$line = fgets($handle);if (!xml_parse($xml_parser, $line, feof($handle))){if (xml_get_error_code($xml_parser) != 5){die(sprintf("XML error: %s (%d) at line %d column %d byte %d<br>cmd: %s<nr>",xml_error_string(xml_get_error_code($xml_parser)),xml_get_error_code($xml_parser),xml_get_current_line_number($xml_parser),xml_get_current_column_number($xml_parser),xml_get_current_byte_index($xml_parser),$cmd));}else{$vars["error"] = $lang["UNKNOWNREVISION"];return 0;}}}pclose($handle);}xml_parser_free($xml_parser);return $curLog;}// }}}}// {{{ initSvnVersionfunction initSvnVersion(&$major, &$minor){global $config;$ret = runCommand($config->svn_noparams." --version", false);if (preg_match("~([0-9]?)\.([0-9]?)\.([0-9]?)~",$ret[0],$matches)){$major = $matches[1];$minor = $matches[2];}$config->setSubversionMajorVersion($major);$config->setSubversionMinorVersion($minor);}// }}}?>