<?php
/**
* Interface for the SVN command line client
*/
class SVN
{
/// Emptied every time the repository root is changed
// $cache[type][revision][path] = ...
var $_debug;
var $_path;
var $_root;
var $_username;
var $_password;
var $_svncommand;
var $_svn_global_parameters;
function SVN()
{
$this->_cachedir = 'cache';
$this->_debug = array(
'exec_count' => 0,
'exec_time' => 0,
'exec_log' => array(),
);
$this->_path = '/';
$this->_svncommand = '/usr/bin/svn';
}
/**
* Returns some debug information
*/
function debugInfo()
{
$res = sprintf("svn binary path: '%s'\n", $this->_svncommand);
$res .= sprintf("svn root: '%s'\n", $this->_root);
$res .= sprintf("svn active path: '%s'\n", $this->getFullPath());
$res .= sprintf("\nExecs: %d Exec time: %f s\n", $this->_debug['exec_count'], $this->_debug['exec_time']);
foreach ($this->_debug['exec_log'] as $line) {
$res .= "$line\n";
}
return $res;
}
/**
* Download active path at revision as .tar.bz2.
* @param rev valid revision
* @return Binary data.
*/
function downloadArchive($rev)
{
if (!is_numeric($rev)) {
$rev = $this->getLastChanged($this->getPath(), $rev);
}
$rev = intval($rev);
// Create filename for the archive
$uniq = urlencode($this->getFullPath()).$rev;
$filename = $uniq .'.tar.bz2';
$cache = $this->_cachedir;
if (file_exists($cache.'/'.$filename)) {
return file_get_contents($cache.'/'. $filename);
}
// svn export
$this->_runsvn("export --force -r $rev ". $this->getEscapedURL() .' '. $cache.'/'. $uniq);
// tar
exec("tar --owner 0 --group 0 --directory $cache/$uniq --bzip2 -cf $cache/$filename .", $output, $ret);
return file_get_contents("$cache/$filename");
}
/**
* Returns output for svn annotate for the active path
*/
function getAnnotation($rev)
{
return $this->_runsvn("annotate -r$rev ". $this->getEscapedURL());
}
/**
*
*/
function getDiff($rev1, $rev2)
{
return $this->_runsvn("diff -r$rev1:$rev2 ". $this->getEscapedURL());
}
/**
* Returns full URL to the active directory that is escaped with
* escapeshellarg()
*/
function getEscapedURL()
{
return escapeshellarg($this->_root . $this->getPathEncoded());
}
/**
* Returns contents for current path if it is a file
*/
function getFileContents($rev = 'HEAD')
{
return $this->_runsvn("cat -r$rev ". $this->getEscapedURL());
}
/**
* Returns complete SVN path to the active directory
*/
function getFullPath()
{
return $this->_root . $this->_path;
}
/**
* Get last changed revision for path (at or before $rev).
*/
function getLastChanged($path, $rev = 'HEAD')
{
$output = $this->_runsvn("info -r$rev ". $this->getRoot().$path);
if (!preg_match('/^Last Changed Rev: (.*)$/m', $output, $matches)) {
return 0;
}
$rev = $matches[1];
return $rev;
}
/**
* Get last log message at or before $rev.
*/
function getLastLog($path, $rev = 'HEAD')
{
$output = $this->_runsvn("log --xml -r$rev:1 --limit 1 ". escapeshellarg($this->getRoot().$this->getPathEncoded()));
if (!preg_match('/\<msg\>(.*)\<\/msg\>/ms', $output, $matches)) {
return '(not found)';
}
return $matches[1];
}
/**
* Get recursive list of files at $rev
*/
function getList($rev = 'HEAD')
{
$output = $this->_runsvn("ls --recursive -r$rev ". $this->getFullPath());
return split("\n", $output);
}
// " 57 hoxu 563 Jun 13 2004 Makefile"
// " 128 hoxu 685 Dec 03 12:08 Makefile" // 1.3.0
/**
* Get recursive and verbose list of files at rev.
* @todo merge partly with listFiles()?
* @todo rewrite to use --xml? (does not contain file size at the moment)
*/
function getListVerbose($rev = 'HEAD')
{
$output = $this->_runsvn("ls --verbose --recursive -r$rev ". $this->getFullPath());
$lines = split("\n", $output);
$res = array();
foreach ($lines as $line) {
$tmp['rev'] = (int)substr($line, 0, 7);
$tmp['author'] = trim(substr($line, 8, 8));
$tmp['size'] = trim(substr($line, 22, 8));
$tmp['date'] = substr($line, 30, 12);
$tmp['age'] = $this->_svndatetounixstamp($tmp['date']);
$tmp['filename'] = substr($line, 43);
if (substr($tmp['filename'], -1, 1) == '/') {
//if (strstr($tmp['filename'], "/")) {
$tmp['isdir'] = true;
} else {
$tmp['isdir'] = false;
}
array_push($res, $tmp);
}
return $res;
}
/**
* Return log messages for active path
* @return array of maps, each describing a revision
*/
function getLog($rev_begin = 'HEAD', $rev_end = '1', $limit = NULL)
{
$result = array();
$log = $this->getLogXML($rev_begin, $rev_end, $limit);
$numentries = preg_match_all("#<logentry\s+revision=\"(\d+)\">(.*)</logentry>#sU", $log, $entries);
for ($i = 0; $i < $numentries; $i++) {
$entrytags = $entries[2][$i];
$entry['rev'] = $entries[1][$i];
// Get author, date and msg
$entry['author'] = '(n/a)';
if (preg_match("#<author>(.*)</author>#sU", $entrytags, $matches) > 0) {
$entry['author'] = $matches[1];
}
preg_match("#<date>(.*)</date>#sU", $entrytags, $matches);
$entry['fulldate'] = $matches[1];
$entry['date'] = substr($entry['fulldate'], 0, 10);
$entry['time'] = substr($entry['fulldate'], 11, 8);
preg_match("#<msg>(.*)</msg>#sU", $entrytags, $matches);
$entry['msg'] = $matches[1];
// First line of log message up to 80 characters
$tmp = explode("\n", $entry['msg']);
$entry['msg_snippet'] = substr(array_shift($tmp), 0, 80);
$entry['paths'] = array();
// Changed paths
$numpaths = preg_match_all("#<path\s*(copyfrom-path=\"(.*)\"\s*)?(copyfrom-rev=\"(.*)\"\s*)?action=\"(.*)\"\s*>(.*)</path>#sU", $entrytags, $paths);
for ($j = 0; $j < $numpaths; $j++) {
$path = null;
$path['action'] = $paths[5][$j];
$path['path'] = $paths[6][$j];
if (strlen($paths[2][$j])) {
$path['copyfrom-path'] = $paths[2][$j];
}
if (strlen($paths[4][$j])) {
$path['copyfrom-rev'] = $paths[4][$j];
}
array_push($entry['paths'], $path);
}
array_push($result, $entry);
}
return $result;
}
/**
* Get log message for $path exactly at $rev (if any).
*/
function getLogMsg($path, $rev)
{
$log = $this->_runsvn("log -r$rev --xml ". $this->getRoot() . $path);
if (preg_match("/<msg>([^<]*)/", $log, $matches) < 1) {
return "(not found)";
}
return $matches[1];
}
/**
* @return XML log for active path
*/
function getLogXML($rev_begin = 'HEAD', $rev_end = '1', $limit = NULL)
{
return $this->_listlog($rev_begin, $rev_end, $limit);
}
function getPath()
{
return $this->_path;
}
/**
* Get path encoded for internal usage.
*/
function getPathEncoded()
{
return implode('/', array_map('rawurlencode', explode('/', $this->_path)));
}
/**
* Returns output of svn proplist for the active path
*/
function getProperties($rev = 'HEAD')
{
return $this->_runsvn("proplist -v ". $this->getEscapedURL());
}
/**
* Returns an array of revisions for the active path
*/
function getRevisions()
{
$xml = $this->getLogXML();
preg_match_all('/revision="(.+)">/', $xml, $matches);
return $matches[1]; // Array
}
/**
* Returns the URI to svn repository root
*/
function getRoot()
{
return $this->_root;
}
/**
* Interface to the svn list command
* @param lastlog whether to fetch last log entry for paths
* @param recursive when true, fetch all paths recursively (ls --xml --recursive)
* @return Array of maps, each describing a subdirectory/file in active
* directory
*/
function listFiles($rev = 'HEAD', $lastlog = false, $recursive = false)
{
$files = array();
$xml = $this->_lsxml($rev, $recursive);
$numentries = preg_match_all("#<entry\s+kind=\"(dir|file)\">(.*)</entry>#sU", $xml, $entries);
// For each <entry> tree
for ($i = 0; $i < $numentries; $i++) {
unset($path);
$path['isdir'] = ($entries[1][$i] == 'dir');
$entryxml = $entries[2][$i];
preg_match("#<name>([^<]*)</name>#sU", $entryxml, $matches);
// The matched path is XML-encoded, unencode the entities (eg. & etc)
$path['filename_shown'] = $matches[1];
$path['filename'] = html_entity_decode($matches[1]);
if ($path['isdir']) { $path['filename'] .= '/'; }
preg_match("#<commit\s+revision=\"(\d+)\">#sU", $entryxml, $matches);
$path['rev'] = intval($matches[1]);
// Author tag isn't always present
$path['author'] = '(n/a)';
if (preg_match("#<author>([^<]*)</author>#sU", $entryxml, $matches) > 0) {
$path['author'] = $matches[1];
}
// ISO 8601 date string
preg_match("#<date>([^>]*)</date>#sU", $entryxml, $matches);
$path['age'] = $this->_isodatetounixstamp($matches[1]); // FIXME rename to datesecs?
// <size> seems to be present when using --recursive, check for it
$path['size'] = '';
if (preg_match("#<size>(\d*)</size>#sU", $entryxml, $matches) > 0) {
$path['size'] = intval($matches[1]);
}
if ($lastlog) {
$path['log'] = $this->getLastLog($this->getPath() . rawurlencode($path['filename']), $path['rev']);
$len = strlen($path['log']);
$path['log'] = substr($path['log'], 0, 80) . ($len > 80 ? '..' : '');
}
$files[] = $path;
}
return $files;
}
/**
* Returns true if active path is a file
*/
function pathIsFile($rev = 'HEAD')
{
if (substr($this->getPath(), -1) == '/') {
return false;
}
$ret = 0;
$this->_runsvnret("cat -r$rev ". $this->getEscapedURL(), $ret);
if ($ret) {
return false;
}
return true;
//return (substr($this->getPath(), -1) != '/');
}
/**
* Set username/password to use for SVN commands
*/
function setAuthentication($username, $password)
{
$this->_username = $username;
$this->_password = $password;
}
/**
* Set directory for temporary/cached files.
*/
function setCacheDir($cachedir)
{
$this->_cachedir = $cachedir;
}
/**
* Sets the path to the SVN binary
*/
function setCommand($command)
{
$this->_svncommand = $command;
}
/**
* Set parameters to be added for all svn commands.
*/
function setGlobalParameters($parameters)
{
$this->_svn_global_parameters = $parameters;
}
/**
* Changes current path inside the repository. Does a check to find out
* whether current path is a file or directory if it does not end in /
*/
function setPath($path)
{
$this->_path = '/';
// Remove extra slashes, '..'s etc
$comp = array();
foreach (explode('/', $path) as $p) {
if (strlen($p) > 0 && $p != '..') {
$comp[] = $p;
}
}
$this->_path .= join('/', $comp);
// If the input ends in slash the new path should end with it as well
if (strlen($path) > 1 && substr($path, -1) == '/') {
$this->_path .= '/';
}
// FIXME?
// Append slash if this path is actually a directory
/*
if (substr($this->getPath(), -1) != '/' && !$this->pathIsFile()) {
$this->_path .= '/';
}
*/
}
/**
* Changes current repository root
*/
function setRoot($root)
{
$this->_root = rtrim($root, '/ ');
$this->setPath('/');
}
// ------------------------------------------------------------
// Private functions
// ------------------------------------------------------------
// Parse ISO 8601 date string to unix timestamp
// Example: "2004-06-03T17:25:10.938292Z" (Z = zero meridian = UTC)
function _isodatetounixstamp($string)
{
$result = 0;
list($date, $time) = explode('T', $string);
list($yy, $mm, $dd) = explode('-', $date);
list($h, $m, $sectz) = explode(':', $time);
list($s, $sfrac) = explode('.', $sectz);
if (substr($sfrac, -1, 1) == 'Z') {
$sfrac = substr($sfrac, 0, -1);
$result = gmmktime($h, $m, $s, $mm, $dd, $yy);
}
else {
$result = mktime($h, $m, $s, $mm, $dd, $yy);
}
return $result;
}
function _listfiles($rev = 'HEAD')
{
return $this->_runsvn("ls -v -r$rev ". $this->getEscapedURL());
}
function _listlog($rev = 'HEAD', $rev_end = '1', $limit = NULL)
{
if (is_numeric($limit)) {
$limit = "--limit ". intval($limit) . " ";
} else {
$limit = '';
}
return $this->_runsvn("log --xml -v $limit -r$rev:$rev_end ". $this->getEscapedURL());
}
function _lsxml($rev = 'HEAD', $recursive = false)
{
return $this->_runsvn("ls --xml -r$rev ". ($recursive ? '--recursive ' : '') . $this->getEscapedURL());
}
function _runsvn($params)
{
$ret = 0;
return $this->_runsvnret($params, $ret);
}
function _runsvnret($params, &$ret)
{
$this->_debug['exec_count']++;
array_push($this->_debug['exec_log'], $params);
$time_start = explode(' ', microtime());
$cmd = $this->_svncommand ." ". $this->_svn_global_parameters ." "; //" --non-interactive --no-auth-cache --config-dir /tmp ";
if (isset($this->_username)) { $cmd .= "--username ". escapeshellarg($this->_username)." "; }
if (isset($this->_password)) { $cmd .= "--password ". escapeshellarg($this->_password)." "; }
$output = array();
exec($cmd . escapeshellcmd($params), $output, $ret);
#echo($cmd . escapeshellcmd($params));
if ($ret) {
header("Content-type: text/plain");
die('svn returned error status:' ."\n". $this->debugInfo());
}
#echo("Ret: $ret");
#die(join("\n", $output));
$time_end = explode(' ', microtime());
$usecs = ($time_end[1] - $time_start[1]) + ($time_end[0] - $time_start[0]);
$this->_debug['exec_time'] += $usecs;
return join("\n", $output);
}
// Converts timestamp from svn ls -v to unix timestamp.
// The format of date/time from "svn ls -v" is:
// Jun 03 20:25
// Jun 03 2004
function _svndatetounixstamp($stamp)
{
list($month_name, $day, $rest) = explode(' ', $stamp, 3);
$year = date('Y');
$hour = 0;
$min = 0;
if (strstr($rest, ':')) {
list($hour, $min) = explode(':', $rest);
} else {
$year = intval($rest);
}
$secs = strtotime("$day $month_name $year $hour:$min");
if ($secs > time()) {
$year--;
$secs = strtotime("$day $month_name $year $hour:$min");
}
return $secs;
}
}