diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 97fb2cdd..defdbb03 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -154,6 +154,8 @@ phutil_register_library_map(array( 'ArcanistDiffParserTestCase' => 'parser/__tests__/ArcanistDiffParserTestCase.php', 'ArcanistDiffUtils' => 'difference/ArcanistDiffUtils.php', 'ArcanistDiffUtilsTestCase' => 'difference/__tests__/ArcanistDiffUtilsTestCase.php', + 'ArcanistDiffVectorNode' => 'difference/ArcanistDiffVectorNode.php', + 'ArcanistDiffVectorTree' => 'difference/ArcanistDiffVectorTree.php', 'ArcanistDiffWorkflow' => 'workflow/ArcanistDiffWorkflow.php', 'ArcanistDifferentialCommitMessage' => 'differential/ArcanistDifferentialCommitMessage.php', 'ArcanistDifferentialCommitMessageParserException' => 'differential/ArcanistDifferentialCommitMessageParserException.php', @@ -691,7 +693,6 @@ phutil_register_library_map(array( 'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php', 'PhutilFileLock' => 'filesystem/PhutilFileLock.php', 'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php', - 'PhutilFileTree' => 'filesystem/PhutilFileTree.php', 'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php', 'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php', 'PhutilGitBinaryAnalyzer' => 'filesystem/binary/PhutilGitBinaryAnalyzer.php', @@ -1134,6 +1135,8 @@ phutil_register_library_map(array( 'ArcanistDiffParserTestCase' => 'PhutilTestCase', 'ArcanistDiffUtils' => 'Phobject', 'ArcanistDiffUtilsTestCase' => 'PhutilTestCase', + 'ArcanistDiffVectorNode' => 'Phobject', + 'ArcanistDiffVectorTree' => 'Phobject', 'ArcanistDiffWorkflow' => 'ArcanistWorkflow', 'ArcanistDifferentialCommitMessage' => 'Phobject', 'ArcanistDifferentialCommitMessageParserException' => 'Exception', @@ -1702,7 +1705,6 @@ phutil_register_library_map(array( 'PhutilExecutionEnvironment' => 'Phobject', 'PhutilFileLock' => 'PhutilLock', 'PhutilFileLockTestCase' => 'PhutilTestCase', - 'PhutilFileTree' => 'Phobject', 'PhutilFrenchLocale' => 'PhutilLocale', 'PhutilGermanLocale' => 'PhutilLocale', 'PhutilGitBinaryAnalyzer' => 'PhutilBinaryAnalyzer', diff --git a/src/browse/workflow/ArcanistBrowseWorkflow.php b/src/browse/workflow/ArcanistBrowseWorkflow.php index 85143353..ffc41654 100644 --- a/src/browse/workflow/ArcanistBrowseWorkflow.php +++ b/src/browse/workflow/ArcanistBrowseWorkflow.php @@ -234,21 +234,8 @@ EOTEXT $ref_uri = head($ref_uris); - // TODO: "ArcanistRevisionRef", at least, may return a relative URI. - // If we get a relative URI, guess the correct absolute URI based on - // the Conduit URI. This might not be correct for Conduit over SSH. - $raw_uri = $ref_uri->getURI(); - - $raw_uri = new PhutilURI($raw_uri); - if (!strlen($raw_uri->getDomain())) { - $base_uri = $this->getConduitEngine() - ->getConduitURI(); - - $raw_uri = id(new PhutilURI($base_uri)) - ->setPath($raw_uri->getPath()); - } - $raw_uri = phutil_string_cast($raw_uri); + $raw_uri = $this->getAbsoluteURI($raw_uri); $uris[] = $raw_uri; } diff --git a/src/difference/ArcanistDiffVectorNode.php b/src/difference/ArcanistDiffVectorNode.php new file mode 100644 index 00000000..2a4b8d9c --- /dev/null +++ b/src/difference/ArcanistDiffVectorNode.php @@ -0,0 +1,113 @@ +vector = $vector; + return $this; + } + + public function getVector() { + return $this->vector; + } + + public function getChildren() { + return $this->children; + } + + public function setParentNode(ArcanistDiffVectorNode $parent) { + $this->parentNode = $parent; + return $this; + } + + public function getParentNode() { + return $this->parentNode; + } + + public function addChild(array $vector, $length, $idx) { + $is_node = ($idx === ($length - 1)); + $element = $vector[$idx]; + + if (!isset($this->children[$element])) { + $this->children[$element] = id(new self()) + ->setParentNode($this) + ->setVector(array_slice($vector, 0, $idx + 1)); + } + + $child = $this->children[$element]; + + if ($is_node) { + $child->setValueNode($child); + return; + } + + $child->addChild($vector, $length, $idx + 1); + } + + public function getDisplayVector() { + return $this->displayVector; + } + + public function appendDisplayElement($element) { + if ($this->displayVector === null) { + $this->displayVector = array(); + } + + $this->displayVector[] = $element; + + return $this; + } + + public function setDisplayNode(ArcanistDiffVectorNode $display_node) { + $this->displayNode = $display_node; + return $this; + } + + public function getDisplayNode() { + return $this->displayNode; + } + + public function setDisplayDepth($display_depth) { + $this->displayDepth = $display_depth; + return $this; + } + + public function getDisplayDepth() { + return $this->displayDepth; + } + + public function setValueNode($value_node) { + $this->valueNode = $value_node; + return $this; + } + + public function getValueNode() { + return $this->valueNode; + } + + public function setAncestralAttribute($key, $value) { + $this->attributes[$key] = $value; + + $parent = $this->getParentNode(); + if ($parent) { + $parent->setAncestralAttribute($key, $value); + } + + return $this; + } + + public function getAttribute($key, $default = null) { + return idx($this->attributes, $key, $default); + } + +} diff --git a/src/difference/ArcanistDiffVectorTree.php b/src/difference/ArcanistDiffVectorTree.php new file mode 100644 index 00000000..f0a3b72d --- /dev/null +++ b/src/difference/ArcanistDiffVectorTree.php @@ -0,0 +1,95 @@ +vectors[] = $vector; + return $this; + } + + public function newDisplayList() { + $root = new ArcanistDiffVectorNode(); + + foreach ($this->vectors as $vector) { + $root->addChild($vector, count($vector), 0); + } + + foreach ($root->getChildren() as $child) { + $this->compressTree($child); + } + + $root->setDisplayDepth(-1); + foreach ($root->getChildren() as $child) { + $this->updateDisplayDepth($child); + } + + return $this->getDisplayList($root); + } + + private function compressTree(ArcanistDiffVectorNode $node) { + $display_node = $node; + + $children = $node->getChildren(); + if ($children) { + $parent = $node->getParentNode(); + if ($parent) { + $siblings = $parent->getChildren(); + if (count($siblings) === 1) { + if (!$parent->getValueNode()) { + $parent_display = $parent->getDisplayNode(); + if ($parent_display) { + $display_node = $parent_display; + if ($node->getValueNode()) { + $parent->setValueNode($node->getValueNode()); + } + } + } + } + } + } + + $node->setDisplayNode($display_node); + + $display_element = last($node->getVector()); + $display_node->appendDisplayElement($display_element); + + foreach ($children as $child) { + $this->compressTree($child); + } + } + + private function updateDisplayDepth(ArcanistDiffVectorNode $node) { + $parent_depth = $node->getParentNode()->getDisplayDepth(); + + if ($node->getDisplayVector() === null) { + $display_depth = $parent_depth; + } else { + $display_depth = $parent_depth + 1; + } + + $node->setDisplayDepth($display_depth); + + foreach ($node->getChildren() as $child) { + $this->updateDisplayDepth($child); + } + } + + private function getDisplayList(ArcanistDiffVectorNode $node) { + $result = array(); + + foreach ($node->getChildren() as $child) { + if ($child->getDisplayVector() !== null) { + $result[] = $child; + } + foreach ($this->getDisplayList($child) as $item) { + $result[] = $item; + } + } + + return $result; + } + +} diff --git a/src/filesystem/PhutilFileTree.php b/src/filesystem/PhutilFileTree.php deleted file mode 100644 index a2486e36..00000000 --- a/src/filesystem/PhutilFileTree.php +++ /dev/null @@ -1,112 +0,0 @@ -splitPath($path); - $parts = array_reverse($parts); - $this->insertPath($parts, $data); - return $this; - } - - public function destroy() { - $this->parentNode = null; - foreach ($this->children as $child) { - $child->destroy(); - } - $this->children = array(); - return $this; - } - - /** - * Get the next node, iterating in depth-first order. - */ - public function getNextNode() { - if ($this->children) { - return head($this->children); - } - $cursor = $this; - while ($cursor) { - if ($cursor->getNextSibling()) { - return $cursor->getNextSibling(); - } - $cursor = $cursor->parentNode; - } - return null; - } - - public function getName() { - return $this->name; - } - - public function getFullPath() { - return $this->fullPath; - } - - public function getDepth() { - return $this->depth; - } - - public function getData() { - return $this->data; - } - - protected function insertPath(array $parts, $data) { - $part = array_pop($parts); - if ($part === null) { - if ($this->data) { - $full_path = $this->getFullPath(); - throw new Exception( - pht("Duplicate insertion for path '%s'.", $full_path)); - } - $this->data = $data; - return; - } - - if (empty($this->children[$part])) { - $node = new PhutilFileTree(); - $node->parentNode = $this; - $node->depth = $this->depth + 1; - $node->name = $part; - $node->fullPath = $this->parentNode ? ($this->fullPath.'/'.$part) : $part; - $this->children[$part] = $node; - } - - $this->children[$part]->insertPath($parts, $data); - } - - protected function splitPath($path) { - $path = trim($path, '/'); - $parts = preg_split('@/+@', $path); - return $parts; - } - - protected function getNextSibling() { - if (!$this->parentNode) { - return null; - } - - $found = false; - foreach ($this->parentNode->children as $node) { - if ($found) { - return $node; - } - if ($this->name === $node->name) { - $found = true; - } - } - - return null; - } - -} diff --git a/src/future/Future.php b/src/future/Future.php index 7ac13258..3c371bc4 100644 --- a/src/future/Future.php +++ b/src/future/Future.php @@ -41,15 +41,15 @@ abstract class Future extends Phobject { 'timeout.')); } - if ($this->hasException()) { - throw $this->getException(); - } - - if (!$this->hasResult()) { + if (!$this->hasResult() && !$this->hasException()) { $graph = new FutureIterator(array($this)); $graph->resolveAll(); } + if ($this->hasException()) { + throw $this->getException(); + } + return $this->getResult(); } diff --git a/src/future/exec/ExecFuture.php b/src/future/exec/ExecFuture.php index 48911930..0264d4d3 100644 --- a/src/future/exec/ExecFuture.php +++ b/src/future/exec/ExecFuture.php @@ -713,7 +713,17 @@ final class ExecFuture extends PhutilExecutableFuture { while (isset($this->stdin) && $this->stdin->getByteLength()) { $write_segment = $this->stdin->getAnyPrefix(); - $bytes = fwrite($stdin, $write_segment); + try { + $bytes = fwrite($stdin, $write_segment); + } catch (RuntimeException $ex) { + // If the subprocess has exited, we may get a broken pipe error here + // in recent versions of PHP. There does not seem to be any way to + // get the actual error code other than reading the exception string. + + // For now, treat this as if writes are blocked. + break; + } + if ($bytes === false) { throw new Exception(pht('Unable to write to stdin!')); } else if ($bytes) { diff --git a/src/ref/file/ArcanistFileRef.php b/src/ref/file/ArcanistFileRef.php index de588bd0..37fcf520 100644 --- a/src/ref/file/ArcanistFileRef.php +++ b/src/ref/file/ArcanistFileRef.php @@ -37,6 +37,16 @@ final class ArcanistFileRef return idxv($this->parameters, array('fields', 'size')); } + public function getURI() { + $uri = idxv($this->parameters, array('fields', 'uri')); + + if ($uri === null) { + $uri = '/'.$this->getMonogram(); + } + + return $uri; + } + public function getMonogram() { return 'F'.$this->getID(); } diff --git a/src/ref/paste/ArcanistPasteRef.php b/src/ref/paste/ArcanistPasteRef.php index 04c8630d..2c4bb58d 100644 --- a/src/ref/paste/ArcanistPasteRef.php +++ b/src/ref/paste/ArcanistPasteRef.php @@ -30,7 +30,13 @@ final class ArcanistPasteRef } public function getURI() { - return idxv($this->parameters, array('fields', 'uri')); + $uri = idxv($this->parameters, array('fields', 'uri')); + + if ($uri === null) { + $uri = '/'.$this->getMonogram(); + } + + return $uri; } public function getContent() { diff --git a/src/workflow/ArcanistPasteWorkflow.php b/src/workflow/ArcanistPasteWorkflow.php index 42df05b2..24468e7a 100644 --- a/src/workflow/ArcanistPasteWorkflow.php +++ b/src/workflow/ArcanistPasteWorkflow.php @@ -9,10 +9,11 @@ final class ArcanistPasteWorkflow public function getWorkflowInformation() { $help = pht(<<newWorkflowArgument('lang') ->setParameter('language') ->setHelp(pht('Language for the paste.')), - $this->newWorkflowArgument('json') - ->setHelp(pht('Output in JSON format.')), + $this->newWorkflowArgument('input') + ->setParameter('path') + ->setIsPathArgument(true) + ->setHelp(pht('Create a paste using the content in a file.')), + $this->newWorkflowArgument('browse') + ->setHelp(pht('After creating a paste, open it in a web browser.')), $this->newWorkflowArgument('argv') ->setWildcard(true), ); @@ -44,6 +49,8 @@ EOTEXT public function runWorkflow() { $set_language = $this->getArgument('lang'); $set_title = $this->getArgument('title'); + $is_browse = $this->getArgument('browse'); + $input_path = $this->getArgument('input'); $argv = $this->getArgument('argv'); if (count($argv) > 1) { @@ -52,6 +59,8 @@ EOTEXT 'Specify only one paste to retrieve.')); } + $is_read = (count($argv) === 1); + $symbols = $this->getSymbolEngine(); if (count($argv) === 1) { @@ -67,6 +76,19 @@ EOTEXT 'Flag "--title" is not supported when reading pastes.')); } + if ($is_browse) { + throw new PhutilArgumentUsageException( + pht( + 'Flag "--browse" is not supported when reading pastes. Use '. + '"arc browse" to browse known objects.')); + } + + if ($input_path !== null) { + throw new PhutilArgumentUsageException( + pht( + 'Flag "--input" is not supported when reading pastes.')); + } + $paste_symbol = $argv[0]; $paste_ref = $symbols->loadPasteForSymbol($paste_symbol); @@ -74,7 +96,8 @@ EOTEXT throw new PhutilArgumentUsageException( pht( 'Paste "%s" does not exist, or you do not have access '. - 'to see it.')); + 'to see it.', + $paste_symbol)); } echo $paste_ref->getContent(); @@ -82,7 +105,11 @@ EOTEXT return 0; } - $content = $this->readStdin(); + if ($input_path === null || $input_path === '-') { + $content = $this->readStdin(); + } else { + $content = Filesystem::readFile($input_path); + } $xactions = array(); @@ -121,6 +148,9 @@ EOTEXT $paste_phid = idxv($result, array('object', 'phid')); $paste_ref = $symbols->loadPasteForSymbol($paste_phid); + $uri = $paste_ref->getURI(); + $uri = $this->getAbsoluteURI($uri); + $log = $this->getLogEngine(); $log->writeSuccess( @@ -130,7 +160,11 @@ EOTEXT echo tsprintf( '%s', $paste_ref->newDisplayRef() - ->setURI($paste_ref->getURI())); + ->setURI($uri)); + + if ($is_browse) { + $this->openURIsInBrowser(array($uri)); + } return 0; } diff --git a/src/workflow/ArcanistUploadWorkflow.php b/src/workflow/ArcanistUploadWorkflow.php index e3506cb6..c9c9dd83 100644 --- a/src/workflow/ArcanistUploadWorkflow.php +++ b/src/workflow/ArcanistUploadWorkflow.php @@ -23,6 +23,10 @@ EOTEXT return array( $this->newWorkflowArgument('json') ->setHelp(pht('Output upload information in JSON format.')), + $this->newWorkflowArgument('browse') + ->setHelp( + pht( + 'After the upload completes, open the files in a web browser.')), $this->newWorkflowArgument('temporary') ->setHelp( pht( @@ -42,6 +46,7 @@ EOTEXT $is_temporary = $this->getArgument('temporary'); $is_json = $this->getArgument('json'); + $is_browse = $this->getArgument('browse'); $paths = $this->getArgument('paths'); $conduit = $this->getConduitEngine(); @@ -65,35 +70,68 @@ EOTEXT $files = $uploader->uploadFiles(); - $results = array(); + $phids = array(); foreach ($files as $file) { - // TODO: This could be handled more gracefully; just preserving behavior - // until we introduce `file.query` and modernize this. + // TODO: This could be handled more gracefully. if ($file->getErrors()) { throw new Exception(implode("\n", $file->getErrors())); } - $phid = $file->getPHID(); - $name = $file->getName(); + $phids[] = $file->getPHID(); + } - $info = $conduit->resolveCall( - 'file.info', - array( - 'phid' => $phid, - )); + $symbols = $this->getSymbolEngine(); + $symbol_refs = $symbols->loadFilesForSymbols($phids); - $results[$path] = $info; - - if (!$is_json) { - $id = $info['id']; - echo " F{$id} {$name}: ".$info['uri']."\n\n"; + $refs = array(); + foreach ($symbol_refs as $symbol_ref) { + $ref = $symbol_ref->getObject(); + if ($ref === null) { + throw new Exception( + pht( + 'Failed to resolve symbol ref "%s".', + $symbol_ref->getSymbol())); } + $refs[] = $ref; } if ($is_json) { - $output = id(new PhutilJSON())->encodeFormatted($results); - echo $output; + $json = array(); + + foreach ($refs as $key => $ref) { + $uri = $ref->getURI(); + $uri = $this->getAbsoluteURI($uri); + + $map = array( + 'argument' => $paths[$key], + 'id' => $ref->getID(), + 'phid' => $ref->getPHID(), + 'name' => $ref->getName(), + 'uri' => $uri, + ); + + $json[] = $map; + } + + echo id(new PhutilJSON())->encodeAsList($json); } else { - $this->writeStatus(pht('Done.')); + foreach ($refs as $ref) { + $uri = $ref->getURI(); + $uri = $this->getAbsoluteURI($uri); + echo tsprintf( + '%s', + $ref->newDisplayRef() + ->setURI($uri)); + } + } + + if ($is_browse) { + $uris = array(); + foreach ($refs as $ref) { + $uri = $ref->getURI(); + $uri = $this->getAbsoluteURI($uri); + $uris[] = $uri; + } + $this->openURIsInBrowser($uris); } return 0; diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php index 93e3e1ff..f4514698 100644 --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -2421,4 +2421,23 @@ abstract class ArcanistWorkflow extends Phobject { return $stdin->read(); } + protected function getAbsoluteURI($raw_uri) { + // TODO: "ArcanistRevisionRef", at least, may return a relative URI. + // If we get a relative URI, guess the correct absolute URI based on + // the Conduit URI. This might not be correct for Conduit over SSH. + + $raw_uri = new PhutilURI($raw_uri); + if (!strlen($raw_uri->getDomain())) { + $base_uri = $this->getConduitEngine() + ->getConduitURI(); + + $raw_uri = id(new PhutilURI($base_uri)) + ->setPath($raw_uri->getPath()); + } + + $raw_uri = phutil_string_cast($raw_uri); + + return $raw_uri; + } + }