From ded8291334a2fa695005f704aec98fd7cbb95309 Mon Sep 17 00:00:00 2001 From: "Sam Boyer (sdboyer)" Date: Sat, 28 Mar 2009 11:36:29 -0500 Subject: [PATCH] Massive improvements and refactoring. Way too much to break down now into individual commits, so just throwing the lot in and picking back up with smaller commits. --- TODO | 7 +- commands/svn.commands.inc | 387 ++++++++++++++++++++++++++++++-------- lib.inc | 37 ++++ parsers.inc | 135 +++++++++---- svn.php | 187 +++++++++++++----- 5 files changed, 591 insertions(+), 162 deletions(-) diff --git a/TODO b/TODO index 987cc76..cdee755 100644 --- a/TODO +++ b/TODO @@ -24,4 +24,9 @@ X = done destruction notwithstanding the circular references. Which could mean refactoring out the circular references. - + Implement arbitrary piping system. \ No newline at end of file + + Implement arbitrary piping system. + + + Consider refactoring some of the flexibility at the SvnCommand::prepare() + level - it's now seeming difficult to imagine a use case where that's really + necessary, as most things can be accomplished indirectly using execute() and + clear(). \ No newline at end of file diff --git a/commands/svn.commands.inc b/commands/svn.commands.inc index caa90a1..11f5138 100644 --- a/commands/svn.commands.inc +++ b/commands/svn.commands.inc @@ -1,6 +1,11 @@ array('pipe', 'w'), + ); /** * @@ -107,7 +121,7 @@ abstract class SvnCommand implements CLICommand { /** *Temporary klugey func - * @return + * @return string */ public function getPrependPath() { return $this->svnRoot->getPrependPath(); @@ -121,14 +135,17 @@ abstract class SvnCommand implements CLICommand { */ protected function procOpen() { $this->procClose(); - $this->process = proc_open(implode(' ', $this->cmds), $this->procDescriptor, $this->procPipes, $this->svnRoot->getWorkingPath(), NULL); + $this->process = proc_open(implode(' ', $this->cmds), $this->getProcDescriptor(), $this->procPipes, $this->svnRoot->getWorkingPath(), NULL); } + abstract protected function getProcDescriptor(); + abstract protected function procHandle(); + /** * Wrapper for proc_close() that cleans up the currently running process. * @return void */ - protected function procClose() { + protected function procClose($destruct = FALSE) { if (is_resource($this->process)) { foreach ($this->procPipes as $pipe) { fclose($pipe); @@ -138,6 +155,48 @@ abstract class SvnCommand implements CLICommand { } } + /** + * Flush the object of its internal data and state, readying it for a new + * command to be run. + * + * This method can safely be called regardless of whether or not execute() has + * been called with the current object. + * + * @param int $preserve_flags + * The contents of this bitmask determine which parts of the object's state + * will be flushed. If no flags are passed, the entire internal state will be + * flushed. Subclasses may define additional flags, but the top-level + * abstract flags are as follows: + * #- SvnCommand::PRESERVE_CMD_OPTS - passing this will cause all command + * opts to be preserved. If the flag is not present, all opts are + * forcibly unset, making them completely irretrievable. + * #- SvnCommand::PRESERVE_CMD_SWITCHES - passing this will cause the + * bitmask containing all the currently set command parameters to be + * preserved. + * #- SvnCommand::PRESERVE_INT_SWITCHES - passing this will cause the + * bitmask reflecting the internal object configuration and state to be + * preserved. The SvnCommand::PREPARED flag will always be turned off, + * whether or not this flag is present. + * @return SvnCommand $this + */ + public function clear($preserve_flags = 0) { + $this->procClose(); + if (!$preserve_flags & self::PRESERVE_CMD_OPTS) { + unset($this->cmdOpts); + $this->cmdOpts = array(); + } + if (!$preserve_flags & self::PRESERVE_CMD_SWITCHES) { + $this->cmdSwitches = 0; + } + if (!$preserve_flags & self::PRESERVE_INT_SWITCHES) { + $this->internalSwitches = 0; + } + // ALWAYS reset the prepared bit. + $this->internalSwitches &= ~self::PREPARED; + + return $this; + } + /** * Gets the version number for the svn binary that will be called by * SvnCommand::procOpen. @@ -147,6 +206,23 @@ abstract class SvnCommand implements CLICommand { return system('svn -q --version'); } + /** + * Internal state interrogating method that indicates whether or not there are + * any commands queuing that will be executed if SvnCommand::execute() is + * called. + * + * @return bool + */ + public function isEmpty() { + // return empty($this->cmdOpts[self::TARGET]) && empty($this->cmdOpts[self::TARGETS]); + return empty($this->cmdOpts); + } + + /** + * + * @param string $arg + * @return SvnCommand $this + */ public function depth($arg) { if (!isset($this->cmdOpts[self::DEPTH])) { $this->cmdOpts[self::DEPTH] = new SvnOptDepth($this, $arg); @@ -173,48 +249,97 @@ abstract class SvnCommand implements CLICommand { return $this->cmdOpts[self::TARGETS]; } + /** + * + * @param string $name + * @return SvnCommand + */ public function username($name) { $this->cmdOpts[self::USERNAME] = new SvnOptUsername($this, $name); return $this; } + /** + * + * @param string $pass + * @return SvnCommand + */ public function password($pass) { $this->cmdOpts[self::PASSWORD] = new SvnOptPassword($this, $pass); return $this; } + /** + * + * @param string $dir + * @return SvnCommand $this + */ public function configDir($dir) { $this->cmdOpts[self::CONFIG_DIR] = new SvnOptConfigDir($this, $dir); return $this; } + /** + * + * @return SvnCommand $this + */ public function recursive() { return $this->depth('infinity'); } + /** + * + * @return SvnCommand $this + */ public function nonRecursive() { return $this->depth('none'); } + /** + * + * @param bool $arg + * Boolean indicating whether the command switch should be turned on (TRUE) + * or off FALSE). + * @param int $switch + * The command switch to be fiddled with. + * @return void + */ protected function fiddleSwitch($arg, $switch) { if ($arg) { $this->cmdSwitches |= $switch; } else { - $this->cmdSwitches ^= ($this->cmdSwitches & $switch) ? $switch : 0; + $this->cmdSwitches &= ~$switch; } } + /** + * + * @param int $bits + * A valid bitmask comprised from the command switch constants attached to + * this class. + * @return SvnCommand + */ public function toggleSwitches($bits) { $this->cmdSwitches ^= $bits; return $this; } + /** + * + * @param bool $arg + * @return SvnCommand + */ public function verbose($arg = TRUE) { $this->fiddleSwitch($arg, self::VERBOSE); return $this; } + /** + * + * @param bool $arg + * @return SvnCommand + */ public function quiet($arg = TRUE) { $this->fiddleSwitch($arg, self::QUIET); return $this; @@ -238,26 +363,51 @@ abstract class SvnCommand implements CLICommand { return $this; } + /** + * + * @param bool $arg + * @return SvnCommand + */ public function force($arg = TRUE) { $this->fiddleSwitch($arg, self::FORCE); return $this; } + /** + * + * @param bool $arg + * @return SvnCommand + */ public function forceLog($arg = TRUE) { $this->fiddleSwitch($arg, self::DRY_RUN); return $this; } + /** + * + * @param bool $arg + * @return SvnCommand + */ public function noIgnore($arg = TRUE) { $this->fiddleSwitch($arg, self::NO_IGNORE); return $this; } + /** + * + * @param bool $arg + * @return SvnCommand + */ public function autoProps($arg = TRUE) { $this->fiddleSwitch($arg, self::AUTO_PROPS); return $this; } + /** + * + * @param bool $arg + * @return SvnCommand + */ public function parents($arg = TRUE) { $this->fiddleSwitch($arg, self::PARENTS); return $this; @@ -296,6 +446,31 @@ abstract class SvnCommand implements CLICommand { return $fluent ? $this : $this->cmds; } + /** + * Execute the command according to dimensions of the object's internal state. + * + * Prepares (if necessary) all the various dimensions of the cli invocation's + * state, then fires up a process and gets into output and/or error handling. + * + * @param bool $fluent + * Indicates whether or not this method should behave fluently (should return + * $this instead of the possibly parsed return value). Defaults to FALSE. + * @return mixed + */ + public function execute($fluent = FALSE) { + if (!($this->internalSwitches & self::PREPARED)) { + $this->prepare(FALSE); + } + + $this->procOpen(); + $this->procHandle(); + $this->procClose(); + + if ($fluent) { + return $this; + } + } + /** * Helper function for SvnCommand::prepare(). * @@ -337,7 +512,7 @@ abstract class SvnCommand implements CLICommand { * more than three or four targets. * @see SvnOptTargets * - * @return SvnRead + * @return SvnCommand $this */ public function target($target, $peg_rev = NULL, $aggregate = FALSE) { if ($aggregate) { @@ -354,71 +529,76 @@ abstract class SvnCommand implements CLICommand { } public function __destruct() { - $this->procClose(); + $this->procClose(TRUE); } } abstract class SvnWrite extends SvnCommand { - protected $procDescriptor = array( - 2 => array('pipe', 'w'), - ); + + public static $operatesOnRepositories = FALSE; public function dryRun($arg = TRUE) { $this->fiddleSwitch($arg, self::DRY_RUN); return $this; } - /** - * Execute the command according to dimensions of the object's internal state. - * - * Prepares (if necessary) all the various dimensions of the cli invocation's - * state, then fires up a process and gets into output and/or error handling. - * - * @see CLICommand::execute() - * - * @param bool $fluent - * Indicates whether or not this method should behave fluently (should return - * $this instead of the possibly parsed return value). Defaults to FALSE. - * @return mixed - */ - public function execute($fluent = FALSE) { - if (!($this->internalSwitches & self::PREPARED)) { - $this->prepare(FALSE); - } - - $this->procOpen(); + protected function getProcDescriptor() { + return array( + 2 => array('pipe', 'w'), + ); + } + protected function procHandle() { if ($this->svnRoot->errContainer[$this] = stream_get_contents($this->procPipes[2])) { throw new Exception('svn failed with the following message: ' . $this->svnRoot->errContainer[$this], E_RECOVERABLE_ERROR); return; } - - $this->procClose(); - - if ($fluent) { - return $this; - } } } +/** + * Abstract intermediate parent class for subversion commands that are strictly + * read-only. + * + * The primary difference between read and write operations is the need SvnRead + * commands have for output handling/parsing. Most of the additions here are a + * reflection of those needs. + */ abstract class SvnRead extends SvnCommand { // internal switches const PARSE_OUTPUT = 0x004; - protected $procDescriptor = array( - 1 => array('pipe', 'w'), - 2 => array('pipe', 'w'), - ); + // clear flags + const PRESERVE_PARSER = 0x008; /** - * Used to spawn the the parsing class object, if/as needed. * - * @var ReflectionClass + * @var CLIParser */ protected $parser; - + /** + * + * @var CLIParser + */ + protected $activeParser; + + protected $ret; + + public static $operatesOnRepositories = TRUE; + +// public function __construct(SvnInstance $svnRoot, $defaults = TRUE) { +// parent::__construct($svnRoot, $defaults); +// } + + protected function getProcDescriptor() { + return array( + 1 => $this->activeParser->openOutputHandle(), + 2 => array('pipe', 'w'), + ); + } + /** * Adds an operative revision to the currently queuing command. Note that * operative revisions are somewhat less intuitive than peg revisions. If you @@ -442,7 +622,7 @@ abstract class SvnRead extends SvnCommand { parent::setDefaults(); $this->internalSwitches |= self::PARSE_OUTPUT; if (isset($this->parserClass)) { - $this->setParserClass($this->parserClass); + $this->setParser(); } } @@ -450,60 +630,92 @@ abstract class SvnRead extends SvnCommand { * If set to provide output parsing, set the workhorse class that will do the * parsing. * - * @param string $class - * @return SvnCommand + * @param mixed $class + * @return SvnRead */ - public function setParserClass($class) { - if (!class_exists($class)) { - // Until we have late static binding (PHP 5.3), __CLASS__ used in this way - // will always output 'SvnRead'. Keeping it in anyway, in anticipation. - throw new Exception("Undeclared class '$class' provided to " . __CLASS__ . "::setParserClass.", E_RECOVERABLE_ERROR); + public function setParser($parser = NULL) { + if (!$parser instanceof CLIParser) { + if (is_null($parser)) { + // No parser provided at all; set it to the parserClass. + $parser = $this->parserClass; + } + elseif (!is_string($parser) || !class_exists($parser)) { + // Until we have late static binding (PHP 5.3), __CLASS__ used in this way + // will always output 'SvnRead'. Keeping it in anyway, in anticipation. + throw new Exception("Unsupported operand type passed to " . __CLASS__ . "::setParser.", E_RECOVERABLE_ERROR); + } + elseif (!class_exists($parser)) { + throw new Exception("Undeclared class '$parser' provided to " . __CLASS__ . "::setParser.", E_RECOVERABLE_ERROR); + } + $this->parser = new $parser(); + } + else { + $this->parser = $parser; } - $this->parser = new ReflectionClass($class); return $this; } - /** - * Execute the command according to dimensions of the object's internal state. - * - * Prepares (if necessary) all the various dimensions of the cli invocation's - * state, then fires up a process and gets into output and/or error handling. - * - * @param bool $fluent - * Indicates whether or not this method should behave fluently (should return - * $this instead of the possibly parsed return value). Defaults to FALSE. - * @return mixed - */ public function execute($fluent = FALSE) { - if (!($this->internalSwitches & self::PREPARED)) { - $this->prepare(FALSE); + // If we're set to parse output, use the currently set parser; otherwise, + if ($this->internalSwitches & self::PARSE_OUTPUT) { + $this->activeParser =& $this->parser; } - - $this->procOpen(); - - $ret = stream_get_contents($this->procPipes[1]); - - if ($this->svnRoot->errContainer[$this] = stream_get_contents($this->procPipes[2])) { - throw new Exception('svn failed with the following message: ' . $this->svnRoot->errContainer[$this], E_RECOVERABLE_ERROR); - return; + elseif (!$this->activeParser instanceof DummyParser) { + $this->activeParser = new DummyParser(); } + parent::execute(FALSE); + return $fluent ? $this : $this->ret; + } - $this->svnRoot->retContainer[$this] = - ($this->internalSwitches & self::PARSE_OUTPUT) ? $this->parser->newInstance($ret) : $ret; - - + protected function procOpen() { $this->procClose(); + $this->process = proc_open(implode(' ', $this->cmds), $this->getProcDescriptor(), $this->procPipes, $this->svnRoot->getWorkingPath(), NULL); + } + + protected function procHandle() { + $status = proc_get_status($this->process); + // FIXME delegating responsibility to the parser object to decide how stdout + // is grabbed means that the parser could request it via a pipe. That would + // be completely wrong, but it's not prevented by the interface, and would + // cause PHP to hang here as the stderr pipe won't be filled until after the + // stdout pipe is fetched. So, checking the error pipe first in this way is + // risky for broken code, but it's better to do it this way b/c we ensure + // there's no error before continuing on to parse the output. + // $this->stdErr = stream_get_contents($this->procPipes[2]); + if ($status['exitcode']) { + throw new Exception('svn failed with the following message: ' . stream_get_contents($this->procPipes[2]), E_RECOVERABLE_ERROR); + } + $this->ret = $this->activeParser->parseOutput(); + } - if ($fluent) { - return $this; + protected function procClose($destruct = FALSE) { + $this->activeParser->procClose($destruct); + parent::procClose(); + } + + /** + * Flush the object of its internal data and state, readying it for a new + * command to be run. + * + * @see SvnCommand::clear() + * + * @param int $preserve_flags + * SvnRead adds one additional flag to those already provided by SvnCommand: + * #- SvnRead::PRESERVE_PARSER - passing this flag will cause the existing + * parser (contained in SvnRead::$parser) to be preserved. + * @return SvnRead $this + */ + public function clear($preserve_flags = 0) { + if (!$preserve_flags & self::PRESERVE_PARSER) { + unset($this->parser); } - return $this->svnRoot->retContainer[$this]; + parent::clear($preserve_flags); + return $this; } } /** * Class that handles invocation of `svn info`. - * @author sdboyer * */ class SvnInfo extends SvnRead { @@ -554,6 +766,7 @@ class SvnMerge extends SvnWrite { const REINTEGRATE = 0x20000; const RECORD_ONLY = 0x40000; + public static $operatesOnRepositories = TRUE; protected $command = 'merge'; public function setSwitches() { @@ -565,7 +778,7 @@ class SvnMerge extends SvnWrite { class SvnPropGet extends SvnRead { const STRICT = 0x20000; - + // public static $operatesOnRepositories = TRUE; protected $command = 'propget'; public function setSwitches() { @@ -587,6 +800,8 @@ class SvnCommit extends SvnWrite { class SvnDelete extends SvnWrite { const KEEP_LOCAL = 0x20000; + + public static $operatesOnRepositories = TRUE; protected $command = 'delete'; public function setSwitches() { @@ -612,6 +827,7 @@ class SvnChangelist extends SvnWrite { } class SvnCheckout extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'checkout'; } @@ -620,6 +836,7 @@ class SvnCleanup extends SvnWrite { } class SvnCopy extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'copy'; } @@ -628,10 +845,12 @@ class SvnDiff extends SvnRead { } class SvnExport extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'export'; } class SvnImport extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'import'; } @@ -644,18 +863,22 @@ class SvnMergeinfo extends SvnRead { } class SvnMkdir extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'mkdir'; } class SvnMove extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'move'; } class SvnPropdel extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'propdel'; } class SvnPropedit extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'propedit'; } @@ -664,6 +887,7 @@ class SvnProplist extends SvnRead { } class SvnPropset extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'propset'; } @@ -684,6 +908,7 @@ class SvnSwitch extends SvnWrite { } class SvnUnlock extends SvnWrite { + public static $operatesOnRepositories = TRUE; protected $command = 'unlock'; } diff --git a/lib.inc b/lib.inc index faaf1a7..6d028c3 100644 --- a/lib.inc +++ b/lib.inc @@ -43,10 +43,47 @@ interface CLICommandOpt { public function getShellString(); } +interface CLIParser { + /** + * @return Resource + */ + public function openOutputHandle(); + public function parseOutput(); + public function procClose($destruct = FALSE); + public function clear(); +} + interface CLIPipeStdIn { } interface CLIPipeStdOut { +} + +/** + * Dummy parser class that transparently passes output straight back without + * any modification, but still implements the CLIParser interface so as to + * reduce complexity in CLICommand implementations. + */ +class DummyParser implements CLIParser { + protected $output; + + public function __construct() {} + public function openOutputHandle() { + $this->output = fopen('php://temp', 'rw'); + return $this->output; + } + + public function parseOutput() { + return stream_get_contents($this->output); + } + + public function clear() {} + + public function procClose($destruct = FALSE) { + if (is_resource($this->output)) { + fclose($this->output); + } + } } \ No newline at end of file diff --git a/parsers.inc b/parsers.inc index 807ca87..e8a8a21 100644 --- a/parsers.inc +++ b/parsers.inc @@ -1,14 +1,33 @@ parse($this->getInnerIterator()->current()); + } + + public function openOutputHandle() { + $this->output = fopen('php://temp', 'rw'); + return $this->output; + } + + // abstract public function parseOutput(); + abstract protected function parse($item); + + public function procClose($destruct = FALSE) { + fclose($this->output); + } +} /** * A class specifically tailored to parse the incremental xml output of an @@ -17,56 +36,85 @@ * @author sdboyer * */ -class SvnInfoXMLParser extends SimpleXMLIterator { +class SvnInfoXMLParser extends SvnOutputHandler implements SeekableIterator { + protected $raw; - public function current() { - $entry = parent::current(); - $item = array(); - $item['url'] = (string) $entry->url; - $item['repository_root'] = (string) $entry->repository->root; - $item['repository_uuid'] = (string) $entry->repository->uuid; + public function parseOutput() { + $this->raw = stream_get_contents($this->output); + parent::__construct(new SimpleXMLIterator($this->raw)); + } + protected function parse($entry) { + $item = array( + 'url' => (string) $entry->url, + 'repository_root' => (string) $entry->repository->root, + 'repository_uuid' => (string) $entry->repository->uuid, + 'type' => (string) $entry['kind'], + 'rev' => intval((string) $entry['revision']), // current state of the item + 'created_rev' => intval((string) $entry->commit['revision']), // last edit + 'last_author' => (string) $entry->commit->author, + 'time_t' => strtotime((string) $entry->commit->date), + ); if ($item['url'] == $item['repository_root']) { $item['path'] = '/'; } else { $item['path'] = substr($item['url'], strlen($item['repository_root'])); } - $item['type'] = (string) $entry['kind']; - $relative_path = (string) $entry['path']; - $item['rev'] = intval((string) $entry['revision']); // current state of the item - $item['created_rev'] = intval((string) $entry->commit['revision']); // last edit - $item['last_author'] = (string) $entry->commit->author; - $item['time_t'] = strtotime((string) $entry->commit->date); + // $relative_path = (string) $entry['path']; return $item; } - /** - * Override the parent implementation and always return FALSE on hasChildren, - * because we know we'll never need to recurse. - */ - public function hasChildren() { - return FALSE; + public function seekDouble($rev = NULL, $path = NULL) { + $items = $query = array(); + if (!is_null($rev)) { + $query[] = "@revision = '$rev'"; + } + if (!is_null($path)) { + $query[] = "@path = '$rev'"; + } + elseif (empty($query)) { + throw new Exception('No arguments provided for xpath query.', E_RECOVERABLE_ERROR); + } + foreach ($this->getInnerIterator()->xpath('/info/entry[' . implode(' and ', $query) . ']') as $entry) { + $item = $this->parse($entry); + $items[$item['path']] = $item; + } + return $items; + } + + public function seek($rev) { + $items = array(); + foreach ($this->parse($this->getInnerIterator()->xpath("/info/entry[@revision='$rev']")) as $item) { + $items[$item['path']] = $item; + } + return $items; + } + + public function clear() { + unset($this->raw); } } -class SvnLogParser extends IteratorIterator { +class SvnLogParser extends SvnOutputHandler implements SeekableIterator { + protected $raw; protected $rev; - public function __construct($xml) { - parent::__construct(new SimpleXMLIterator($xml)); +// public function __construct($xml) { +// parent::__construct(new SimpleXMLIterator($xml)); +// } +// public function current() { +// return $this->parse($this->getInnerIterator()->current()); +// } + + public function parseOutput() { + $this->raw = stream_get_contents($this->output); + parent::__construct(new SimpleXMLIterator($this->raw)); } - public function current() { - $this->rev = $this->getInnerIterator()->current(); - $revision = array(); - $revision['rev'] = intval((string) $this->rev['revision']); - $revision['author'] = (string) $this->rev->author; - $revision['msg'] = rtrim((string) $this->rev->msg); // no trailing linebreaks - $revision['time_t'] = strtotime((string) $this->rev->date); + protected function parse($rev) { $paths = array(); - - foreach ($this->rev->paths->path as $logpath) { + foreach ($rev->paths->path as $logpath) { $path = array( 'path' => (string) $logpath, 'action' => (string) $logpath['action'], @@ -79,9 +127,24 @@ class SvnLogParser extends IteratorIterator { } $paths[$path['path']] = $path; } - $revision['paths'] = $paths; + + $revision = array( + 'rev' => intval((string) $rev['revision']), + 'author' => (string) $rev->author, + 'msg' => rtrim($rev->msg), // no trailing linebreaks + 'time_t' => strtotime($rev->date), + 'paths' => $paths, + ); return $revision; } + + public function seek($position) { + return $this->parse($this->getInnerIterator()->xpath("/log/logentry[@revision='$position']")); + } + + public function clear() { + unset($this->raw); + } } class SvnListParser { diff --git a/svn.php b/svn.php index 81e86c0..36081b8 100644 --- a/svn.php +++ b/svn.php @@ -7,39 +7,53 @@ require_once dirname(__FILE__) . '/opts/svn.opts.inc'; /** - * Abstract class that allows for commands that can be used on both an svn repo - * and working copy to be handled via inheritance. + * Abstract parent class for a Subversion 'instance,' i.e., working copy or + * repository. + * + * SvnWorkingCopy and SvnRepository are the concrete subclasses that extend this + * class to provide that functionality. + * * @author sdboyer * */ abstract class SvnInstance extends SplFileInfo implements CLIWrapper { - protected $defaults = TRUE; + public $defaults = TRUE; protected $cmd; - protected $cmdSwitches = 0, $cmdOpts = array(); - protected $cache = array(); - public $invocations, $cmdContainer, $retContainer, $errContainer; + // protected $cmdSwitches = 0, $cmdOpts = array(); + // public $invocations, $cmdContainer, $retContainer, $errContainer; protected $subPath = ''; + public $username, $password, $configDir; public function __construct($path, $verify = TRUE) { parent::__construct($path); if ($verify) { $this->verify(); } - $this->retContainer = new SplObjectMap(); - $this->errContainer = new SplObjectMap(); + $this->getInfo(); + // $this->retContainer = new SplObjectMap(); + // $this->errContainer = new SplObjectMap(); } - public function defaults($use = TRUE) { - $this->defaults = $use; - return $this; + protected function getInfo() { + $orig_subpath = $this->subPath; + $this->subPath = NULL; + + $output = $this->svn('info', FALSE)->target('.')->configDir(dirname(__FILE__) . '/configdir')->execute(); + + preg_match('/^Repository Root: (.*)\n/m', $output, $root); + $this->repoRoot = $root[1]; + preg_match('/^Revision: (.*)\n/m', $output, $rev); + $this->latestRev = (int) $rev[1]; + + $this->subPath = $orig_subpath; } /** * Set a path, relative to the base path that was passed in to the SvnInstance * constructor, that should be used as the base path for all path-based * operations. Primarily useful for specifying a particular branch or tag that - * operations should be run against in a fashion that will be transparent to - * the subcommand invocations. + * operations should be run against in a way that will be transparent to the + * subcommand invocations. * * IMPORTANT NOTE: internal handling of subpaths becomes copmlex if you change * the subpath while in the midst of queuing up a command. This internal @@ -48,18 +62,16 @@ public function defaults($use = TRUE) { * @param string $path */ public function setSubPath($path) { - $this->subPath = $path; + $this->subPath = trim($path, '/'); } public function verify() { if (!$this->isDir()) { - throw new Exception(__CLASS__ . ' requires a directory argument, but "' . $this->getPath() . '" was provided.', E_RECOVERABLE_ERROR); + throw new Exception(__CLASS__ . ' requires a directory argument, but "' . $this->getPathname() . '" was provided.', E_RECOVERABLE_ERROR); } - } - - abstract protected function getInfo(); + } - public function getFullPath() { + public function getRootPath() { if (empty($this->subPath)) { return (string) $this; } @@ -69,6 +81,13 @@ public function getFullPath() { } abstract public function getPrependPath(); + /** + * + * @param string $subcommand + * @param bool $defaults + * @return SvnCommand + */ + abstract public function svn($subcommand, $defaults = NULL); } /** @@ -83,23 +102,11 @@ abstract public function getPrependPath(); class SvnWorkingCopy extends SvnInstance { protected $repoRoot; protected $latestRev; - public $username, $password, $configDir; const NO_AUTH_CACHE = 0x001; - protected function getInfo() { - $orig_subpath = $this->subPath; - $this->subPath = NULL; - - $info = new SvnInfo($this, FALSE); - $output = $info->target('.')->configDir(dirname(__FILE__) . '/configdir')->execute(); - - preg_match('/^Repository Root: (.*)\n/m', $output, $root); - $this->repoRoot = $root[1]; - preg_match('/^Revision: (.*)\n/m', $output, $rev); - $this->latestRev = (int) $rev[1]; - - $this->subPath = $orig_subpath; + public function getRepository() { + return new SvnRepository($this->repoRoot); } public function __get($name) { @@ -111,7 +118,7 @@ public function __get($name) { } return $this->$name; } - return NULL; + return; } public function verify() { @@ -122,20 +129,19 @@ public function verify() { } public function getWorkingPath() { - return $this->getFullPath(); + return $this->getRootPath(); } public function getPrependPath() { - return NULL; + return; } public function svn($subcommand, $defaults = NULL) { - $fullcommand = 'svn' . $subcommand; - if (!class_exists($fullcommand)) { + $classname = 'svn' . $subcommand; + if (!class_exists($classname)) { throw new Exception("Invalid svn subcommand '$subcommand' was requested.", E_RECOVERABLE_ERROR); - return; } - $this->cmd = new $fullcommand($this, is_null($defaults) ? $this->defaults : $defaults); + $this->cmd = new $classname($this, is_null($defaults) ? $this->defaults : $defaults); // Add any global working copy opts that are set. foreach (array('username', 'password', 'configDir') as $prop) { @@ -148,25 +154,118 @@ public function svn($subcommand, $defaults = NULL) { } } +/** + * Defines a Subversion repository location, and allows commands ordinarily + * associated with the command line to be invoked on it. + * + * Note that this class is a bad SplFileInfo citizen, and calling certain of the + * SplFileInfo methods on it WILL cause a php fatal error. We use it because the + * methods SplFileInfo provides that do work are very handy, and it would be + * foolish to reimplement in userland what's already been done in C. + */ class SvnRepository extends SvnInstance { + public static $protocols = array( + 'http' => array( + 'write capable' => FALSE, + ), + 'https' => array( + 'write capable' => FALSE, + ), + 'svn' => array( + 'write capable' => FALSE, + ), + 'svn+ssh' => array( + 'write capable' => TRUE, + ), + 'file' => array( + 'write capable' => TRUE, + ), + ); + + protected $protocol; + +// public function __construct($url, $verify = TRUE) { +// parent::__construct($url, $verify); +// } public function verify() { // Run a fast, low-overhead operation, verifying this is a working svn repository. - system('svnadmin lstxns ' . escapeshellarg($this->getPath()), $exit); + system('svnadmin lstxns ' . escapeshellarg($this->getPathname()), $exit); if ($exit) { - throw new Exception($this->getPath() . " is not a valid Subversion repository.", E_RECOVERABLE_ERROR); + throw new Exception($this->getPathname() . " is not a valid Subversion repository.", E_RECOVERABLE_ERROR); } } protected function getInfo() { - + parent::getInfo(); + $pieces = explode('://', (string) $this); + $this->protocol = $pieces[0]; } + /** + * Get the path to be prepended to individual file items + * @return string + */ public function getPrependPath() { - return $this->getFullPath() . DIRECTORY_SEPARATOR; + return $this->getRootPath() . DIRECTORY_SEPARATOR; } public function getWorkingPath() { return NULL; } + + public function svn($subcommand, $defaults = NULL) { + $classname = 'Svn' . $subcommand; + if (!class_exists($classname)) { + throw new Exception("Invalid svn subcommand '$subcommand' was requested.", E_RECOVERABLE_ERROR); + } + $reflection = new ReflectionClass($classname); + if (!$reflection->getStaticPropertyValue('operatesOnRepositories')) { + throw new Exception('Subversion repositories cannot do anything with the ' . $subcommand . ' svn subcommand.', E_RECOVERABLE_ERROR); + } + if ($reflection->isSubclassOf('SvnWrite') && !$this->isWritable()) { + throw new Exception("Write operation '$subcommand' was requested, but the repository is not writable from here.", E_RECOVERABLE_ERROR); + } + + $this->cmd = new $classname($this, is_null($defaults) ? $this->defaults : $defaults); + return $this->cmd; + } + + /** + * Indicate whether or not it is possible to perform write operations directly + * on the repository. + * + * @return bool + */ + public function isWritable() { + return self::$protocols[$this->protocol]['write capable']; + } + + public function svnadmin($subcommand, $defaults = NULL) { + $classname = 'Svnadmin' . $subcommand; + if (!class_exists($classname)) { + throw new Exception("Invalid svnadmin subcommand '$subcommand' was requested.", E_RECOVERABLE_ERROR); + } + + $this->cmd = new $classname($this, is_null($defaults) ? $this->defaults : $defaults); + return $this->cmd; + } +} + +/** + * Helper function that retrieves an actual Subversion repository as an + * SvnInstance object, even if a working copy path is passed in. + * + * @param string $path + * @return SvnRepository + */ +function svnlib_get_repository($path) { + try { + $repo = new SvnRepository($path); + } catch (Exception $e) { + $wc = new SvnWorkingCopy($path); + $repo = $wc->getRepository(); + unset($wc); + } + return $repo; }