| * Monotone utils.␊ | 
| *␊ | 
| */␊ | 
| ␊ | 
| class IDF_Scm_Monotone_Stdio␊ | 
| {␊ | 
| public static $SUPPORTED_STDIO_VERSION = 2;␊ | 
| ␊ | 
| private $repo;␊ | 
| private $proc;␊ | 
| private $pipes;␊ | 
| private $oob;␊ | 
| private $cmdnum;␊ | 
| private $lastcmd;␊ | 
| ␊ | 
| public function __construct($repo)␊ | 
| {␊ | 
| $this->repo = $repo;␊ | 
| $this->start();␊ | 
| }␊ | 
| ␊ | 
| public function __destruct()␊ | 
| {␊ | 
| $this->stop();␊ | 
| }␊ | 
| ␊ | 
| public function start()␊ | 
| {␊ | 
| if (is_resource($this->proc))␊ | 
| $this->stop();␊ | 
| ␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate stdio --no-workspace --norc",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo));␊ | 
| ␊ | 
| $descriptors = array(␊ | 
| 0 => array("pipe", "r"),␊ | 
| 1 => array("pipe", "w"),␊ | 
| 2 => array("pipe", "r")␊ | 
| );␊ | 
| ␊ | 
| $this->proc = proc_open($cmd, $descriptors, $this->pipes);␊ | 
| ␊ | 
| if (!is_resource($this->proc))␊ | 
| {␊ | 
| throw new IDF_Scm_Exception("could not start stdio process");␊ | 
| }␊ | 
| ␊ | 
| $this->_checkVersion();␊ | 
| ␊ | 
| $this->cmdnum = -1;␊ | 
| }␊ | 
| ␊ | 
| public function stop()␊ | 
| {␊ | 
| if (!is_resource($this->proc))␊ | 
| return;␊ | 
| ␊ | 
| fclose($this->pipes[0]);␊ | 
| fclose($this->pipes[1]);␊ | 
| fclose($this->pipes[2]);␊ | 
| ␊ | 
| proc_close($this->proc);␊ | 
| $this->proc = null;␊ | 
| }␊ | 
| ␊ | 
| private function _checkVersion()␊ | 
| {␊ | 
| $version = fgets($this->pipes[1]);␊ | 
| if (!preg_match('/^format-version: (\d+)$/', $version, $m) ||␊ | 
| $m[1] != self::$SUPPORTED_STDIO_VERSION)␊ | 
| {␊ | 
| throw new IDF_Scm_Exception(␊ | 
| "stdio format version mismatch, expected '".␊ | 
| self::$SUPPORTED_STDIO_VERSION."', got '".@$m[1]."'"␊ | 
| );␊ | 
| }␊ | 
| fgets($this->pipes[1]);␊ | 
| }␊ | 
| ␊ | 
| private function _write($args, $options = array())␊ | 
| {␊ | 
| $cmd = "";␊ | 
| if (count($options) > 0)␊ | 
| {␊ | 
| $cmd = "o";␊ | 
| foreach ($options as $k => $v)␊ | 
| {␊ | 
| $cmd .= strlen((string)$k) . ":" . (string)$k;␊ | 
| $cmd .= strlen((string)$v) . ":" . (string)$v;␊ | 
| }␊ | 
| $cmd .= "e ";␊ | 
| }␊ | 
| ␊ | 
| $cmd .= "l";␊ | 
| foreach ($args as $arg)␊ | 
| {␊ | 
| $cmd .= strlen((string)$arg) . ":" . (string)$arg;␊ | 
| }␊ | 
| $cmd .= "e\n";␊ | 
| ␊ | 
| if (!fwrite($this->pipes[0], $cmd))␊ | 
| {␊ | 
| throw new IDF_Scm_Exception("could not write '$cmd' to process");␊ | 
| }␊ | 
| ␊ | 
| $this->lastcmd = $cmd;␊ | 
| $this->cmdnum++;␊ | 
| }␊ | 
| ␊ | 
| private function _read()␊ | 
| {␊ | 
| $this->oob = array('w' => array(),␊ | 
| 'p' => array(),␊ | 
| 't' => array(),␊ | 
| 'e' => array());␊ | 
| ␊ | 
| $output = "";␊ | 
| $errcode = 0;␊ | 
| ␊ | 
| while (true)␊ | 
| {␊ | 
| $read = array($this->pipes[1]);␊ | 
| $write = null;␊ | 
| $except = null;␊ | 
| ␊ | 
| $streamsChanged = stream_select(␊ | 
| $read, $write, $except, 0, 20000␊ | 
| );␊ | 
| ␊ | 
| if ($streamsChanged === false)␊ | 
| {␊ | 
| throw new IDF_Scm_Exception(␊ | 
| "Could not select() on read pipe"␊ | 
| );␊ | 
| }␊ | 
| ␊ | 
| if ($streamsChanged == 0)␊ | 
| {␊ | 
| continue;␊ | 
| }␊ | 
| ␊ | 
| $data = array(0,"",0);␊ | 
| $idx = 0;␊ | 
| while (true)␊ | 
| {␊ | 
| $c = fgetc($this->pipes[1]);␊ | 
| if ($c == ':')␊ | 
| {␊ | 
| if ($idx == 2)␊ | 
| break;␊ | 
| ␊ | 
| ++$idx;␊ | 
| continue;␊ | 
| }␊ | 
| ␊ | 
| if (is_numeric($c))␊ | 
| $data[$idx] = $data[$idx] * 10 + $c;␊ | 
| else␊ | 
| $data[$idx] .= $c;␊ | 
| }␊ | 
| ␊ | 
| // sanity␊ | 
| if ($this->cmdnum != $data[0])␊ | 
| {␊ | 
| throw new IDF_Scm_Exception(␊ | 
| "command numbers out of sync; ".␊ | 
| "expected {$this->cmdnum}, got {$data[0]}"␊ | 
| );␊ | 
| }␊ | 
| ␊ | 
| $toRead = $data[2];␊ | 
| $buffer = "";␊ | 
| while ($toRead > 0)␊ | 
| {␊ | 
| $buffer .= fread($this->pipes[1], $toRead);␊ | 
| $toRead = $data[2] - strlen($buffer);␊ | 
| }␊ | 
| ␊ | 
| switch ($data[1])␊ | 
| {␊ | 
| case 'w':␊ | 
| case 'p':␊ | 
| case 't':␊ | 
| case 'e':␊ | 
| $this->oob[$data[1]][] = $buffer;␊ | 
| continue;␊ | 
| case 'm':␊ | 
| $output .= $buffer;␊ | 
| continue;␊ | 
| case 'l':␊ | 
| $errcode = $buffer;␊ | 
| break 2;␊ | 
| }␊ | 
| }␊ | 
| ␊ | 
| if ($errcode != 0)␊ | 
| {␊ | 
| throw new IDF_Scm_Exception(␊ | 
| "command '{$this->lastcmd}' returned error code $errcode: ".␊ | 
| implode(" ", $this->oob['e'])␊ | 
| );␊ | 
| }␊ | 
| ␊ | 
| return $output;␊ | 
| }␊ | 
| ␊ | 
| public function exec($args, $options = array())␊ | 
| {␊ | 
| $this->_write($args, $options);␊ | 
| return $this->_read();␊ | 
| }␊ | 
| ␊ | 
| public function getLastWarnings()␊ | 
| {␊ | 
| return array_key_exists('w', $this->oob) ?␊ | 
| $this->oob['w'] : array();␊ | 
| }␊ | 
| ␊ | 
| public function getLastProgress()␊ | 
| {␊ | 
| return array_key_exists('p', $this->oob) ?␊ | 
| $this->oob['p'] : array();␊ | 
| }␊ | 
| ␊ | 
| public function getLastTickers()␊ | 
| {␊ | 
| return array_key_exists('t', $this->oob) ?␊ | 
| $this->oob['t'] : array();␊ | 
| }␊ | 
| ␊ | 
| public function getLastErrors()␊ | 
| {␊ | 
| return array_key_exists('e', $this->oob) ?␊ | 
| $this->oob['e'] : array();␊ | 
| }␊ | 
| }␊ | 
| ␊ | 
| class IDF_Scm_Monotone extends IDF_Scm␊ | 
| {␊ | 
| public static $MIN_INTERFACE_VERSION = 12.0;␊ | 
| ␊ | 
| private $stdio;␊ | 
| ␊ | 
| /* ============================================== *␊ | 
| *                                                *␊ | 
| *   Common Methods Implemented By All The SCMs   *␊ | 
|  | 
| {␊ | 
| $this->repo = $repo;␊ | 
| $this->project = $project;␊ | 
| $this->stdio = new IDF_Scm_Monotone_Stdio($repo);␊ | 
| }␊ | 
| ␊ | 
| public function getRepositorySize()␊ | 
|  | 
| ␊ | 
| public function isAvailable()␊ | 
| {␊ | 
| $out = array();␊ | 
| try {␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate interface_version",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo));␊ | 
| self::exec('IDF_Scm_Monotone::isAvailable',␊ | 
| $cmd, $out, $return);␊ | 
| } catch (IDF_Scm_Exception $e) {␊ | 
| return false;␊ | 
| try␊ | 
| {␊ | 
| $out = $this->stdio->exec(array("interface_version"));␊ | 
| return floatval($out) >= self::$MIN_INTERFACE_VERSION;␊ | 
| }␊ | 
| catch (IDF_Scm_Exception $e) {}␊ | 
| ␊ | 
| return count($out) > 0 && floatval($out[0]) >= self::$MIN_INTERFACE_VERSION;␊ | 
| return false;␊ | 
| }␊ | 
| ␊ | 
| public function getBranches()␊ | 
|  | 
| return $this->cache['branches'];␊ | 
| }␊ | 
| // FIXME: introduce handling of suspended branches␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate branches",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo));␊ | 
| self::exec('IDF_Scm_Monotone::getBranches',␊ | 
| $cmd, $out, $return);␊ | 
| if ($return != 0) {␊ | 
| throw new IDF_Scm_Exception(sprintf($this->error_tpl,␊ | 
| $cmd, $return,␊ | 
| implode("\n", $out)));␊ | 
| }␊ | 
| $res = array();␊ | 
| $out = $this->stdio->exec(array("branches"));␊ | 
| ␊ | 
| // FIXME: we could expand each branch with one of its head revisions␊ | 
| // here, but these would soon become bogus anyway and we cannot␊ | 
| // map multiple head revisions here either, so we just use the␊ | 
| // selector as placeholder␊ | 
| foreach ($out as $b) {␊ | 
| $res = array();␊ | 
| foreach (preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY) as $b)␊ | 
| {␊ | 
| $res["h:$b"] = $b;␊ | 
| }␊ | 
| ␊ | 
| $this->cache['branches'] = $res;␊ | 
| return $res;␊ | 
| }␊ | 
|  | 
| */␊ | 
| private function _resolveSelector($selector)␊ | 
| {␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate select %s",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo),␊ | 
| escapeshellarg($selector));␊ | 
| self::exec('IDF_Scm_Monotone::_resolveSelector',␊ | 
| $cmd, $out, $return);␊ | 
| return $out;␊ | 
| $out = $this->stdio->exec(array("select", $selector));␊ | 
| return preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY);␊ | 
| }␊ | 
| ␊ | 
| /**␊ | 
|  | 
| */␊ | 
| private static function _parseBasicIO($in)␊ | 
| {␊ | 
| if (substr($in, -1) != "\n")␊ | 
| $in .= "\n";␊ | 
| ␊ | 
| $pos = 0;␊ | 
| $stanzas = array();␊ | 
| ␊ | 
|  | 
| ␊ | 
| if (!array_key_exists($rev, $certCache))␊ | 
| {␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate certs %s",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo),␊ | 
| escapeshellarg($rev));␊ | 
| self::exec('IDF_Scm_Monotone::_getCerts',␊ | 
| $cmd, $out, $return);␊ | 
| $out = $this->stdio->exec(array("certs", $rev));␊ | 
| ␊ | 
| $stanzas = self::_parseBasicIO(implode("\n", $out));␊ | 
| $stanzas = self::_parseBasicIO($out);␊ | 
| $certs = array();␊ | 
| foreach ($stanzas as $stanza)␊ | 
| {␊ | 
|  | 
| ␊ | 
| private function _getLastChangeFor($file, $startrev)␊ | 
| {␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate get_content_changed %s %s",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo),␊ | 
| escapeshellarg($startrev),␊ | 
| escapeshellarg($file));␊ | 
| self::exec('IDF_Scm_Monotone::_getLastChangeFor',␊ | 
| $cmd, $out, $return);␊ | 
| $out = $this->stdio->exec(array(␊ | 
| "get_content_changed", $startrev, $file␊ | 
| ));␊ | 
| ␊ | 
| $stanzas = self::_parseBasicIO(implode("\n", $out));␊ | 
| $stanzas = self::_parseBasicIO($out);␊ | 
| ␊ | 
| // FIXME: we only care about the first returned content mark␊ | 
| // everything else seem to be very rare cases␊ | 
|  | 
| **/␊ | 
| public function getTags()␊ | 
| {␊ | 
| if (isset($this->cache['tags'])) {␊ | 
| if (isset($this->cache['tags']))␊ | 
| {␊ | 
| return $this->cache['tags'];␊ | 
| }␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate tags",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo));␊ | 
| self::exec('IDF_Scm_Monotone::getTags', $cmd, $out, $return);␊ | 
| ␊ | 
| $out = $this->stdio->exec(array("tags"));␊ | 
| ␊ | 
| $tags = array();␊ | 
| $stanzas = self::_parseBasicIO(implode("\n", $out));␊ | 
| $stanzas = self::_parseBasicIO($out);␊ | 
| foreach ($stanzas as $stanza)␊ | 
| {␊ | 
| $tagname = null;␊ | 
|  | 
| return array();␊ | 
| }␊ | 
| ␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate get_manifest_of %s",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo),␊ | 
| escapeshellarg($revs[0]));␊ | 
| self::exec('IDF_Scm_Monotone::getTree', $cmd, $out, $return);␊ | 
| $out = $this->stdio->exec(array(␊ | 
| "get_manifest_of", $revs[0]␊ | 
| ));␊ | 
| ␊ | 
| $files = array();␊ | 
| $stanzas = self::_parseBasicIO(implode("\n", $out));␊ | 
| $stanzas = self::_parseBasicIO($out);␊ | 
| $folder = $folder == '/' || empty($folder) ? '' : $folder.'/';␊ | 
| ␊ | 
| foreach ($stanzas as $stanza)␊ | 
|  | 
| if (count($revs) == 0)␊ | 
| return false;␊ | 
| ␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate get_manifest_of %s",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo),␊ | 
| escapeshellarg($revs[0]));␊ | 
| self::exec('IDF_Scm_Monotone::getPathInfo', $cmd, $out, $return);␊ | 
| $out = $this->stdio->exec(array(␊ | 
| "get_manifest_of", $revs[0]␊ | 
| ));␊ | 
| ␊ | 
| $files = array();␊ | 
| $stanzas = self::_parseBasicIO(implode("\n", $out));␊ | 
| $stanzas = self::_parseBasicIO($out);␊ | 
| ␊ | 
| foreach ($stanzas as $stanza)␊ | 
| {␊ | 
|  | 
| ␊ | 
| public function getFile($def, $cmd_only=false)␊ | 
| {␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate get_file %s",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo),␊ | 
| escapeshellarg($def->hash));␊ | 
| return ($cmd_only)␊ | 
| ? $cmd : self::shell_exec('IDF_Scm_Monotone::getFile', $cmd);␊ | 
| // this won't work with remote databases␊ | 
| if ($cmd_only)␊ | 
| {␊ | 
| throw new Pluf_Exception_NotImplemented();␊ | 
| }␊ | 
| ␊ | 
| return $this->stdio->exec(array("get_file", $def->hash));␊ | 
| }␊ | 
| ␊ | 
| private function _getDiff($target, $source = null)␊ | 
|  | 
| return "";␊ | 
| }␊ | 
| ␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate content_diff -r %s -r %s",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo),␊ | 
| escapeshellarg($sources[0]),␊ | 
| escapeshellarg($targets[0]));␊ | 
| self::exec('IDF_Scm_Monotone::_getDiff',␊ | 
| $cmd, $out, $return);␊ | 
| ␊ | 
| return implode("\n", $out);␊ | 
| return $this->stdio->exec(␊ | 
| array("content_diff"),␊ | 
| array("r" => $sources[0], "r" => $targets[0])␊ | 
| );␊ | 
| }␊ | 
| ␊ | 
| /**␊ | 
|  | 
| if (count($revs) == 0)␊ | 
| return false;␊ | 
| ␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '')␊ | 
| .sprintf("%s -d %s automate get_revision %s",␊ | 
| Pluf::f('mtn_path', 'mtn'),␊ | 
| escapeshellarg($this->repo),␊ | 
| escapeshellarg($revs[0]));␊ | 
| self::exec('IDF_Scm_Monotone::isCommitLarge',␊ | 
| $cmd, $out, $return);␊ | 
| $out = $this->stdio->exec(array(␊ | 
| "get_revision", $revs[0]␊ | 
| ));␊ | 
| ␊ | 
| $newAndPatchedFiles = 0;␊ | 
| $stanzas = self::_parseBasicIO(implode("\n", $out));␊ | 
| $stanzas = self::_parseBasicIO($out);␊ | 
| ␊ | 
| foreach ($stanzas as $stanza)␊ | 
| {␊ | 
|  | 
| * @param int Number of changes (10).␊ | 
| * @return array Changes.␊ | 
| */␊ | 
| public function getChangeLog($commit='HEAD', $n=10)␊ | 
| {␊ | 
| if ($n === null) $n = '';␊ | 
| else $n = ' -'.$n;␊ | 
| $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log%s --date=iso --pretty=format:\'%s\' %s',␊ | 
| escapeshellarg($this->repo), $n, $this->mediumtree_fmt,␊ | 
| escapeshellarg($commit));␊ | 
| $out = array();␊ | 
| $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;␊ | 
| self::exec('IDF_Scm_Monotone::getChangeLog', $cmd, $out);␊ | 
| return self::parseLog($out);␊ | 
| public function getChangeLog($commit=null, $n=10)␊ | 
| {␊ | 
| throw new Pluf_Exception_NotImplemented();␊ | 
| }␊ | 
| }␊ |