<?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(' ', ' ', $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$
|| ###################################################################
\*=====================================================================*/
?>