Deprecated: Assigning the return value of new by reference is deprecated in /home/bluestat/public_html/source/index.php on line 477
ViewSVN - Blob - ViewGit - Blue Static
<?php
/*=====================================================================*\
|| ################################################################### ||
|| # ViewSVN [#]version[#]
|| # --------------------------------------------------------------- # ||
|| # Copyright ©2002-[#]year[#] by Iris Studios, Inc. All Rights Reserved. # ||
|| # This file may not be reproduced in any way without permission.  # ||
|| # --------------------------------------------------------------- # ||
|| # User License Agreement at http://www.iris-studios.com/license/  # ||
|| ################################################################### ||
\*=====================================================================*/

/**
* Command line interface with the SVN commands
*
* @package	ViewSVN
*/

/**
* Interacts with the command line subsystem to
* access SVN information
*
* @package	ViewSVN
* @version	$Id$
*/
class SVNLib
{
	/**
	* Path to the SVN binary
	* @var	string
	*/
	var $svnpath;

	/**
	* Common command system
	* @var	object
	*/
	var $common;

	/**
	* Constructor: validate SVN path
	*
	* @param	string	Path to SVN binary
	*/
	function SVNLib($svnpath)
	{
		global $viewsvn;

		$this->svnpath = $viewsvn->shell->cmd($svnpath);

		$this->common =& new SVNCommon();

		$access = $viewsvn->shell->exec($this->svnpath . ' --version');

		if (!$access)
		{
			$viewsvn->trigger->error('svn binary could not be found');
		}

		if (!preg_match('#^svn, version (.*?)\)$#i', trim($access[0])))
		{
			$viewsvn->trigger->error('svn binary does not pass test');
		}
	}

	/**
	* Prepares data for output
	*
	* @access	public
	*
	* @param	string	Standard data
	*
	* @return	string	Output-ready data
	*/
	function format($string)
	{
		// convert entities
		$string = htmlspecialchars($string);

		// tabs to 5 spaces
		$string = str_replace("\t", '     ', $string);

		// spaces to nbsp
		$string = str_replace(' ', '&nbsp;', $string);

		// convert advanced diff
		$string = str_replace(array('{@++}', '{@--}'), array('<span class="diff_add">', '<span class="diff_del">'), $string);
		$string = str_replace(array('{/@++}', '{/@--}'), '</span>', $string);

		// nl2br
		$string = nl2br($string);

		return $string;
	}

	/**
	* Executes the SVN binary
	*
	* @access	private
	*
	* @param	string	Command
	*
	* @return	array	Output
	*/
	function svn($command)
	{
		global $viewsvn;

		$output = $viewsvn->shell->exec($this->svnpath . ' ' . $command . ' 2>&1');
		$temp = implode("\n", $output);
		if (strpos($temp, '(apr_err=') !== false)
		{
			$viewsvn->trigger->error(nl2br($temp));
		}
		return $output;
	}

	/**
	* SVN Wrapper: standard command system
	*
	* @access	private
	*
	* @param	string	SVN command
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Revision
	*
	* @return	array	Lines of output
	*/
	function std($command, $repos, $path, $revision)
	{
		global $viewsvn;

		$revision = $this->rev($revision);
		$repospath = $viewsvn->repos->fetch_path($repos, false);

		return $this->svn($command . ' -r' . $revision . ' ' . $repospath . $path);
	}

	/**
	* SVN Wrapper: blame
	*
	* @access	protected
	*
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Revision
	*
	* @return	array	Lines of blame output
	*/
	function blame($repos, $path, $revision)
	{
		return $this->std('blame', $repos, $path, $revision);
	}

	/**
	* SVN Wrapper: cat
	*
	* @access	protected
	*
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Revision
	*
	* @return	array	Lines of cat output
	*/
	function cat($repos, $path, $revision)
	{
		return $this->std('cat', $repos, $path, $revision);
	}

	/**
	* SVN Wrapper: diff
	*
	* @access	protected
	*
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Lower revision
	* @param	integer	Higher revision
	*
	* @return	array	Lines of diff output
	*/
	function diff($repos, $path, $lorev, $hirev)
	{
		global $viewsvn;

		$hirev = $this->rev($hirev);
		$lorev = $this->rev($lorev);
		if ($lorev == 'HEAD')
		{
			$lorev = 1;
		}

		if (is_integer($hirev) AND is_integer($lorev))
		{
			if ($lorev > $hirev)
			{
				$lorev = $hirev - 1;
			}
			if ($lorev == $hirev)
			{
				$lorev = 0;
			}
		}

		$repospath = $viewsvn->repos->fetch_path($repos, false);

		return $this->svn('diff -r' . $lorev . ':' . $hirev . ' ' . $repospath . $path);
	}

	/**
	* SVN Wrapper: log
	*
	* @access	protected
	*
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Lower revision
	* @param	integer	Higher revision
	*
	* @return	array	Lines of log output
	*/
	function log($repos, $path, $lorev, $hirev)
	{
		global $viewsvn;

		$hirev = $this->rev($hirev);
		$lorev = $this->rev($hirev);
		if ($lorev == 'HEAD')
		{
			$lorev = 0;
		}

		if (is_integer($hirev) AND is_integer($lorev))
		{
			if ($lorev > $hirev)
			{
				$lorev = $hirev - 1;
			}
			if ($lorev == $hirev)
			{
				$lorev = 0;
			}
		}

		$repospath = $viewsvn->repos->fetch_path($repos, false);

		return $this->svn('log -v -r' . $hirev . ':' . $lorev . ' ' . $repospath . $path);
	}

	/**
	* SVN Wrapper: ls (list)
	*
	* @access	protected
	*
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Revision
	*
	* @return	array	Lines of list output
	*/
	function ls($repos, $path, $revision)
	{
		return $this->std('list', $repos, $path, $revision);
	}

	/**
	* Generates a clean revision number
	*
	* @access	public
	*
	* @param	integer	Revision number
	*
	* @return	mixed	Cleaned revision or HEAD
	*/
	function rev($revision)
	{
		if (($revision = intval($revision)) < 1)
		{
			$revision = 'HEAD';
		}
		return $revision;
	}
}

/**
* Commonly executed SVN commands that return data
* used in many parts of the system
*
* @package	ViewSVN
* @version	$Id$
*/
class SVNCommon
{
	/**
	* Registry object
	* @var	object
	*/
	var $registry;

	/**
	* List of revisions
	* @var	array
	*/
	var $revisions;

	/**
	* List of logs
	* @var	array
	*/
	var $logs;

	/**
	* Constructor: bind with registry
	*/
	function SVNCommon()
	{
		global $viewsvn;

		$this->registry =& $viewsvn;
	}

	/**
	* Checks to see if the given universal path is
	* a directory
	*
	* @access	public
	*
	* @param	string	Universal path
	*
	* @return	bool	Directory or not
	*/
	function isdir($path)
	{
		$output = $this->registry->svn->std('info', $this->registry->paths->fetch_repos($path), $this->registry->paths->fetch_path($path), 'HEAD');

		foreach ($output AS $line)
		{
			if (preg_match('#^Node Kind: (.*)#', $line, $matches))
			{
				if (trim(strtolower($matches[1])) == 'directory')
				{
					return true;
				}
			}
		}

		return false;
	}

	/**
	* Get a list of revisions for a path
	*
	* @access	public
	*
	* @param	string	Universal path
	*
	* @return	array	Key revisions
	*/
	function fetch_revs($path)
	{
		if (!isset($this->revisions["$path"]))
		{
			$log = $this->fetch_logs($path);

			$revs = array_keys($log);

			$this->revisions["$path"] = array(
				'HEAD'	=> $revs[0],
				'START'	=> $revs[ count($revs) - 1 ],
				'revs'	=> $revs
			);
		}

		return $this->revisions["$path"];
	}

	/**
	* Gets the revision that is marked as HEAD
	*
	* @access	public
	*
	* @param	string	Universal path
	*
	* @return	integer	Revision
	*/
	function fetch_head_rev($path)
	{
		$revs = $this->fetch_revs($path);
		return $revs['HEAD'];
	}

	/**
	* Returns the previous revision to the one given
	*
	* @access	public
	*
	* @param	string	Universal path
	* @param	integer	Arbitrary revision
	*
	* @return	integer	Previous revision (-1 if none)
	*/
	function fetch_prev_rev($path, $current)
	{
		$revs = $this->fetch_revs($path);

		if ($current == 'HEAD')
		{
			$current = $this->fetch_head_rev($path);
		}

		$index = array_search($current, $revs['revs']);
		if ($current === false)
		{
			$message->trigger->error('revision ' . $current . ' is not in ' . $path);
		}

		if (isset($revs['revs'][ $index + 1 ]))
		{
			return $revs['revs'][ $index + 1 ];
		}
		else
		{
			return -1;
		}
	}

	/**
	* Get a list of logs
	*
	* @access	public
	*
	* @param	string	Universal path
	*
	* @return	array	Log data
	*/
	function fetch_logs($path)
	{
		if (!isset($this->logs["$path"]))
		{
			$log = new SVNLog($this->registry->paths->fetch_repos($path), $this->registry->paths->fetch_path($path), 0, 0);

			$this->logs["$path"] = $log->fetch();
		}

		return $this->logs["$path"];
	}

	/**
	* Returns a given log entry for a path
	* and revision
	*
	* @access	public
	*
	* @param	string	Universal path
	* @param	integer	Arbitrary revision
	*
	* @return	array	Log entry
	*/
	function fetch_log($path, $rev)
	{
		$logs = $this->fetch_logs($path);

		$rev = $this->registry->svn->rev($rev);
		if ($rev == 'HEAD')
		{
			$rev = $this->fetch_head_rev($path);
		}

		if (isset($logs["$rev"]))
		{
			return $logs["$rev"];
		}
		else
		{
			return null;
		}
	}
}

/**
* Annotation/blame system; constructs an array
* that is ready for output
*
* @package	ViewSVN
* @version	$Id$
*/
class SVNBlame
{
	/**
	* Array of blame information
	* @var	array
	*/
	var $blame = array();

	/**
	* Raw "svn blame" output
	* @var	array
	*/
	var $rawoutput;

	/**
	* Constructor: create blame and store data
	*
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Revision
	*/
	function SVNBlame($repos, $path, $revision)
	{
		global $viewsvn;

		$this->rawoutput = $viewsvn->svn->blame($repos, $path, $revision);
		$this->process();
	}

	/**
	* Returns blame for display
	*
	* @access	public
	*
	* @return	array	Blame data
	*/
	function fetch()
	{
		return $this->blame;
	}

	/**
	* Parses the blame data
	*
	* @access	private
	*/
	function process()
	{
		$lineno = 1;

		foreach ($this->rawoutput AS $line)
		{
			if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)\s(.*)$#', $line, $matches))
			{
				$this->blame[] = array(
					'rev'		=> $matches[1],
					'author'	=> $matches[2],
					'line'		=> $matches[3],
					'lineno'	=> $lineno++
				);
			}
			// a blank line
			else if (preg_match('#^\s+([0-9]+)\s+([\w\.\-_]+)$#', $line, $matches))
			{
				$this->blame[] = array(
					'rev'		=> $matches[1],
					'author'	=> $matches[2],
					'line'		=> '',
					'lineno'	=> $lineno++
				);
			}
		}
	}
}

/**
* Log management system; creates a complex list
* of SVN log information
*
* @package	ViewSVN
* @version	$Id$
*/
class SVNLog
{
	/**
	* Array of logs
	* @var	array
	*/
	var $logs = array();

	/**
	* Raw "svn log" output
	* @var	array
	*/
	var $rawoutput;

	/**
	* Constructor: create log store for the given file
	*
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Lower revision
	* @param	integer	Higher revision
	*/
	function SVNLog($repos, $path, $lorev, $hirev)
	{
		global $viewsvn;

		$this->rawoutput = $viewsvn->svn->log($repos, $path, $lorev, $hirev);
		$this->process();
	}

	/**
	* Returns logs for display
	*
	* @access	public
	*
	* @return	array	Log data
	*/
	function fetch()
	{
		return $this->logs;
	}

	/**
	* Splits up the raw output into a usable log
	*
	* @access	private
	*/
	function process()
	{
		$lastrev = 0;

		for ($i = 1; $i <= count($this->rawoutput) - 1; $i++)
		{
			$line = $this->rawoutput["$i"];

			if (preg_match('#^r([0-9]*) \| (.*?) \| (....-..-.. ..:..:..) ([0-9\-]*) \((.*?)\) \| ([0-9]*) lines?$#', $line, $matches))
			{
				if (isset($this->logs["$lastrev"]))
				{
					$this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
				}

				$this->logs["$matches[1]"] = array(
					'rev'		=> $matches[1],
					'author'	=> $matches[2],
					'date'		=> $matches[3],
					'timezone'	=> $matches[4],
					'lines'		=> $matches[6],
					'message'	=> ''
				);

				$lastrev = $matches[1];
			}
			else if (preg_match('#^\s+([ADMR])\s(.*)#', $line, $matches))
			{
				$this->logs["$lastrev"]['files'][] = array(
					'action'	=> $matches[1],
					'file'		=> $matches[2]
				);
			}
			else
			{
				if (trim($line) != 'Changed paths:')
				{
					$this->logs["$lastrev"]['message'] .= $line . "\n";
				}
			}
		}

		if (isset($this->logs["$lastrev"]))
		{
			$this->logs["$lastrev"]['message'] = $this->strip_last_line($this->logs["$lastrev"]['message']);
		}
	}

	/**
	* Trims the last dash line off a message
	*
	* @access	private
	*
	* @param	string	Message with annoying-ass line
	*
	* @return	string	Clean string
	*/
	function strip_last_line($string)
	{
		return trim(preg_replace("#\n(.*?)\n$#", '', $string));
	}
}

/**
* Diff system; constructs a diff array that
* is ready for output
*
* @package	ViewSVN
*/
class SVNDiff
{
	/**
	* Array of diff information
	* @var	array
	*/
	var $diff = array();

	/**
	* Raw "svn diff" output
	* @var	array
	*/
	var $rawoutput;

	/**
	* Constructor: create and store diff data
	*
	* @param	string	Repository
	* @param	string	Path
	* @param	integer	Lower revision
	* @param	integer	Higher revision
	*/
	function SVNDiff($repos, $path, $lorev, $hirev)
	{
		global $viewsvn;

		$this->rawoutput = $viewsvn->svn->diff($repos, $path, $lorev, $hirev);
		$this->process();
	}

	/**
	* Returns diffs for display
	*
	* @access	public
	*
	* @return	array	Diff data
	*/
	function fetch()
	{
		return $this->diff;
	}

	/**
	* Processes and prepares diff data
	*
	* @access	private
	*/
	function process()
	{
		$chunk = 0;
		$indexcounter = null;

		$lastact = '';
		$lastcontent = '';

		foreach ($this->rawoutput AS $line)
		{
			if (preg_match('#^@@ \-([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@$#', $line, $bits))
			{
				$lastact = '';
				$lastcontent = '';

				$this->diff["$index"][ ++$chunk ]['hunk'] = array('old' => array('line' => $bits[1], 'count' => $bits[2]), 'new' => array('line' => $bits[3], 'count' => $bits[4]));
				$lines['old'] = $this->diff["$index"]["$chunk"]['hunk']['old']['line'] - 1;
				$lines['new'] = $this->diff["$index"]["$chunk"]['hunk']['new']['line'] - 1;
				continue;
			}

			if ($indexcounter <= 5 AND $indexcounter !== null)
			{
				$indexcounter++;
				continue;
			}
			else if ($indexcounter == 5)
			{
				$indexcounter = null;
				continue;
			}

			if (preg_match('#^([\+\- ])(.*)#', $line, $matches))
			{
				$act = $matches[1];
				$content = $matches[2];

				if ($act == ' ')
				{
					$this->diff["$index"]["$chunk"][] = array(
						'line'		=> $content,
						'act'		=> '',
						'oldlineno'	=> ++$lines['old'],
						'newlineno'	=> ++$lines['new']
					);
				}
				else if ($act == '+')
				{
					// potential line delta
					if ($lastact == '-')
					{
						if ($delta = @$this->fetch_diff_extent($lastcontent, $content))
						{
							// create two sets of ends for the two contents
							$delta['endo'] = strlen($lastcontent) - $delta['end'];
							$delta['endn'] = strlen($content) - $delta['end'];

							$diffo = $delta['endo'] - $delta['start'];
							$diffn = $delta['endn'] - $delta['start'];

							if (strlen($lastcontent) > $delta['endo'] - $diffo)
							{
								$removed = substr($lastcontent, $delta['start'], $diffo);
								$this->diff["$index"]["$chunk"][ count($this->diff["$index"]["$chunk"]) - 2 ]['line'] = substr_replace($lastcontent, '{@--}' . $removed . '{/@--}', $delta['start'], $diffo);
							}

							if (strlen($content) > $delta['endn'] - $diffn)
							{
								$added = substr($content, $delta['start'], $diffn);
								$content = substr_replace($content, '{@++}' . $added . '{/@++}', $delta['start'], $diffn);
							}
						}
					}

					$this->diff["$index"]["$chunk"][] = array(
						'line'		=> $content,
						'act'		=> '+',
						'oldlineno'	=> '',
						'newlineno'	=> ++$lines['new']
					);
				}
				else if ($act == '-')
				{
					$lastcontent = $content;

					$this->diff["$index"]["$chunk"][] = array(
						'line'		=> $content,
						'act'		=> '-',
						'oldlineno'	=> ++$lines['old'],
						'newlineno'	=> ''
					);
				}

				$lastact = $act;
			}
			// whitespace lines
			else
			{
				if (preg_match('#^Index: (.*?)$#', $line, $matches))
				{
					$index = $matches[1];
					$indexcounter = 1;
					$chunk = 0;
					continue;
				}

				$lastact = '';

				$this->diff["$index"]["$chunk"][] = array(
					'line'		=> '',
					'act'		=> '',
					'oldlineno'	=> ++$lines['old'],
					'newlineno'	=> ++$lines['new']
				);
			}
		}
	}

	/**
	* Returns the amount of change that occured
	* between two lines
	*
	* @access	private
	*
	* @param	string	Old line
	* @param	string	New line
	*
	* @return	array	Difference of positions
	*/
	function fetch_diff_extent($old, $new)
	{
		$start = 0;
		$min = min(strlen($old), strlen($new));

		for ($start = 0; $start < $min; $start++)
		{
			if ($old{"$start"} != $new{"$start"})
			{
				break;
			}
		}

		$max = max(strlen($old), strlen($new));

		for ($end = 0; $end < $min; $end++)
		{
			$oldpos = strlen($old) - $end;
			$newpos = strlen($new) - $end;

			if ($old{"$oldpos"} != $new{"$newpos"})
			{
				break;
			}
		}

		$end--;

		if ($start == 0 AND $end == $max)
		{
			return false;
		}

		return array('start' => $start, 'end' => $end);
	}
}

/*=====================================================================*\
|| ###################################################################
|| # $HeadURL$
|| # $Id$
|| ###################################################################
\*=====================================================================*/
?>