Location: PHPKode > projects > ViewSVN > include/libsvn.php
<?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. &amp; 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;
	}
}
Return current item: ViewSVN