Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F995705
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
45 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/scripts/arcanist.php b/scripts/arcanist.php
index 0a7e76d1..f4ecf925 100755
--- a/scripts/arcanist.php
+++ b/scripts/arcanist.php
@@ -1,330 +1,282 @@
#!/usr/bin/env php
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once dirname(__FILE__).'/__init_script__.php';
phutil_require_module('phutil', 'conduit/client');
phutil_require_module('phutil', 'console');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'symbols');
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'configuration');
phutil_require_module('arcanist', 'workingcopyidentity');
phutil_require_module('arcanist', 'repository/api/base');
ini_set('memory_limit', -1);
$config_trace_mode = false;
$force_conduit = null;
$args = array_slice($argv, 1);
$load = array();
$matches = null;
foreach ($args as $key => $arg) {
if ($arg == '--') {
break;
} else if ($arg == '--trace') {
unset($args[$key]);
$config_trace_mode = true;
} else if ($arg == '--no-ansi') {
unset($args[$key]);
PhutilConsoleFormatter::disableANSI(true);
} else if (preg_match('/^--load-phutil-library=(.*)$/', $arg, $matches)) {
unset($args[$key]);
$load['?'] = $matches[1];
} else if (preg_match('/^--conduit-uri=(.*)$/', $arg, $matches)) {
unset($args[$key]);
$force_conduit = $matches[1];
}
}
// The POSIX extension is not available by default in some PHP installs.
if (function_exists('posix_isatty') && !posix_isatty(STDOUT)) {
PhutilConsoleFormatter::disableANSI(true);
}
$args = array_values($args);
$working_directory = $_SERVER['PWD'];
try {
if ($config_trace_mode) {
PhutilServiceProfiler::installEchoListener();
}
if (!$args) {
throw new ArcanistUsageException("No command provided. Try 'arc help'.");
}
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($working_directory);
if ($load) {
$libs = $load;
} else {
$libs = $working_copy->getConfig('phutil_libraries');
}
if ($libs) {
foreach ($libs as $name => $location) {
// Try to resolve the library location. We look in several places, in
// order:
//
// 1. Inside the working copy. This is for phutil libraries within the
// project. For instance "library/src" will resolve to
// "./library/src" if it exists.
// 2. In the same directory as the working copy. This allows you to
// check out a library alongside a working copy and reference it.
// If we haven't resolved yet, "library/src" will try to resolve to
// "../library/src" if it exists.
// 3. Using normal libphutil resolution rules. Generally, this means
// that it checks for libraries next to libphutil, then libraries
// in the PHP include_path.
$resolved = false;
// Check inside the working copy.
$resolved_location = Filesystem::resolvePath(
$location,
$working_copy->getProjectRoot());
if (Filesystem::pathExists($resolved_location)) {
$location = $resolved_location;
$resolved = true;
}
// If we didn't find anything, check alongside the working copy.
if (!$resolved) {
$resolved_location = Filesystem::resolvePath(
$location,
dirname($working_copy->getProjectRoot()));
if (Filesystem::pathExists($resolved_location)) {
$location = $resolved_location;
$resolved = true;
}
}
if ($config_trace_mode) {
echo "Loading phutil library '{$name}' from '{$location}'...\n";
}
try {
phutil_load_library($location);
} catch (PhutilBootloaderException $ex) {
$error_msg = sprintf(
'Failed to load library "%s" at location "%s". Please check the '.
'"phutil_libraries" setting in your .arcconfig file. Refer to page '.
'http://phabricator.com/docs/arcanist/article/'.
'Setting_Up_.arcconfig.html for more info.',
$name,
$location);
throw new ArcanistUsageException($error_msg);
} catch (PhutilLibraryConflictException $ex) {
if ($ex->getLibrary() != 'arcanist') {
throw $ex;
}
$arc_dir = dirname(dirname(__FILE__));
$error_msg =
"You are trying to run one copy of Arcanist on another copy of ".
"Arcanist. This operation is not supported. To execute Arcanist ".
"operations against this working copy, run './bin/arc' (from the ".
"current working copy) not some other copy of 'arc' (you ran one ".
"from '{$arc_dir}').";
throw new ArcanistUsageException($error_msg);
}
}
}
$user_config = ArcanistBaseWorkflow::readUserConfigurationFile();
$config = $working_copy->getConfig('arcanist_configuration');
if ($config) {
PhutilSymbolLoader::loadClass($config);
$config = new $config();
} else {
$config = new ArcanistConfiguration();
}
$command = strtolower($args[0]);
$workflow = $config->buildWorkflow($command);
if (!$workflow) {
throw new ArcanistUsageException(
"Unknown command '{$command}'. Try 'arc help'.");
}
$workflow->setArcanistConfiguration($config);
$workflow->setCommand($command);
$workflow->setWorkingDirectory($working_directory);
$workflow->parseArguments(array_slice($args, 1));
$need_working_copy = $workflow->requiresWorkingCopy();
$need_conduit = $workflow->requiresConduit();
$need_auth = $workflow->requiresAuthentication();
$need_repository_api = $workflow->requiresRepositoryAPI();
$need_conduit = $need_conduit ||
$need_auth;
$need_working_copy = $need_working_copy ||
$need_conduit ||
$need_repository_api;
if ($need_working_copy) {
if (!$working_copy->getProjectRoot()) {
throw new ArcanistUsageException(
"There is no '.arcconfig' file in this directory or any parent ".
"directory. Create a '.arcconfig' file to configure this project ".
"for use with Arcanist.");
}
$workflow->setWorkingCopy($working_copy);
}
- $set_guid = false;
- if ($need_conduit) {
- if ($force_conduit) {
- $conduit_uri = $force_conduit;
- } else {
- $conduit_uri = $working_copy->getConduitURI();
- }
+ if ($force_conduit) {
+ $conduit_uri = $force_conduit;
+ } else {
+ $conduit_uri = $working_copy->getConduitURI();
+ }
+ if ($conduit_uri) {
+ // Set the URI path to '/api/'. TODO: Originally, I contemplated letting
+ // you deploy Phabricator somewhere other than the domain root, but ended
+ // up never pursuing that. We should get rid of all "/api/" silliness
+ // in things users are expected to configure. This is already happening
+ // to some degree, e.g. "arc install-certificate" does it for you.
+ $conduit_uri = new PhutilURI($conduit_uri);
+ $conduit_uri->setPath('/api/');
+ $conduit_uri = (string)$conduit_uri;
+ }
+ $workflow->setConduitURI($conduit_uri);
+
+ if ($need_conduit) {
if (!$conduit_uri) {
throw new ArcanistUsageException(
"No Conduit URI is specified in the .arcconfig file for this project. ".
"Specify the Conduit URI for the host Differential is running on.");
- } else {
- // Set the URI path to '/api/'. TODO: Originally, I contemplated letting
- // you deploy Phabricator somewhere other than the domain root, but ended
- // up never pursuing that. We should get rid of all "/api/" silliness
- // in things users are expected to configure. This is already happening
- // to some degree, e.g. "arc install-certificate" does it for you.
- $conduit_uri = new PhutilURI($conduit_uri);
- $conduit_uri->setPath('/api/');
- $conduit_uri = (string)$conduit_uri;
}
- $conduit = new ConduitClient($conduit_uri);
- $workflow->setConduit($conduit);
+ $workflow->establishConduit();
+ }
- $hosts_config = idx($user_config, 'hosts', array());
- $host_config = idx($hosts_config, $conduit_uri, array());
- $user_name = idx($host_config, 'user');
- $certificate = idx($host_config, 'cert');
+ $hosts_config = idx($user_config, 'hosts', array());
+ $host_config = idx($hosts_config, $conduit_uri, array());
+ $user_name = idx($host_config, 'user');
+ $certificate = idx($host_config, 'cert');
+ $description = implode(' ', $argv);
+ $credentials = array(
+ 'user' => $user_name,
+ 'certificate' => $certificate,
+ 'description' => $description,
+ );
+ $workflow->setConduitCredentials($credentials);
+
+ if ($need_auth) {
if (!$user_name || !$certificate) {
throw new ArcanistUsageException(
phutil_console_format(
"YOU NEED TO __INSTALL A CERTIFICATE__ TO LOGIN TO PHABRICATOR\n\n".
"You are trying to connect to '{$conduit_uri}' but do not have ".
"a certificate installed for this host. Run:\n\n".
" $ **arc install-certificate**\n\n".
"...to install one."));
}
-
- $description = implode(' ', $argv);
-
- try {
- $connection = $conduit->callMethodSynchronous(
- 'conduit.connect',
- array(
- 'client' => 'arc',
- 'clientVersion' => 2,
- 'clientDescription' => php_uname('n').':'.$description,
- 'user' => $user_name,
- 'certificate' => $certificate,
- 'host' => $conduit_uri,
- ));
- } catch (ConduitClientException $ex) {
- if ($ex->getErrorCode() == 'ERR-NO-CERTIFICATE' ||
- $ex->getErrorCode() == 'ERR-INVALID-USER') {
- $message =
- "\n".
- phutil_console_format(
- "YOU NEED TO __INSTALL A CERTIFICATE__ TO LOGIN TO PHABRICATOR").
- "\n\n".
- phutil_console_format(
- " To do this, run: **arc install-certificate**").
- "\n\n".
- "The server '{$conduit_uri}' rejected your request:".
- "\n".
- $ex->getMessage();
- throw new ArcanistUsageException($message);
- } else {
- throw $ex;
- }
- }
-
- $workflow->setUserName($user_name);
- $user_phid = idx($connection, 'userPHID');
- if ($user_phid) {
- $set_guid = true;
- $workflow->setUserGUID($user_phid);
- }
+ $workflow->authenticateConduit();
}
if ($need_repository_api) {
$repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
$working_copy);
$workflow->setRepositoryAPI($repository_api);
}
- if ($need_auth && !$set_guid) {
- $user_name = getenv('USER');
- $user_find_future = $conduit->callMethod(
- 'user.find',
- array(
- 'aliases' => array(
- $user_name,
- ),
- ));
- $user_guids = $user_find_future->resolve();
- if (empty($user_guids[$user_name])) {
- throw new ArcanistUsageException(
- "Username '{$user_name}' is not recognized.");
- }
-
- $user_guid = $user_guids[$user_name];
- $workflow->setUserGUID($user_guid);
- $workflow->setUserName($user_name);
- }
-
$config->willRunWorkflow($command, $workflow);
$workflow->willRunWorkflow();
$err = $workflow->run();
$config->didRunWorkflow($command, $workflow, $err);
exit($err);
} catch (ArcanistUsageException $ex) {
echo phutil_console_format(
"**Usage Exception:** %s\n",
$ex->getMessage());
if ($config_trace_mode) {
echo "\n";
throw $ex;
}
exit(1);
} catch (Exception $ex) {
if ($config_trace_mode) {
throw $ex;
}
echo phutil_console_format(
"\n**Exception:**\n%s\n%s\n",
$ex->getMessage(),
"(Run with --trace for a full exception trace.)");
exit(1);
}
diff --git a/src/workflow/base/ArcanistBaseWorkflow.php b/src/workflow/base/ArcanistBaseWorkflow.php
index 4bf2c540..4d9999d7 100644
--- a/src/workflow/base/ArcanistBaseWorkflow.php
+++ b/src/workflow/base/ArcanistBaseWorkflow.php
@@ -1,672 +1,913 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Implements a runnable command, like "arc diff" or "arc help".
*
+ * = Managing Conduit =
+ *
+ * Workflows have the builtin ability to open a Conduit connection to a
+ * Phabricator installation, so methods can be invoked over the API. Workflows
+ * may either not need this (e.g., "help"), or may need a Conduit but not
+ * authentication (e.g., calling only public APIs), or may need a Conduit and
+ * authentication (e.g., "arc diff").
+ *
+ * To specify that you need an //unauthenticated// conduit, override
+ * @{method:requiresConduit} to return ##true##. To specify that you need an
+ * //authenticated// conduit, override @{method:requiresAuthentication} to
+ * return ##true##. You can also manually invoke @{method:establishConduit}
+ * and/or @{method:authenticateConduit} later in a workflow to upgrade it.
+ * Once a conduit is open, you can access the client by calling
+ * @{method:getConduit}, which allows you to invoke methods. You can get
+ * verified information about the user identity by calling @{method:getUserGUID}
+ * or @{method:getUserName} after authentication occurs.
+ *
+ * @task conduit Conduit
* @group workflow
*/
class ArcanistBaseWorkflow {
private $conduit;
+ private $conduitURI;
+ private $conduitCredentials;
+ private $conduitAuthenticated;
+
private $userGUID;
private $userName;
private $repositoryAPI;
private $workingCopy;
private $arguments;
private $command;
private $arcanistConfiguration;
private $parentWorkflow;
private $workingDirectory;
private $changeCache = array();
public function __construct() {
}
+
+/* -( Conduit )------------------------------------------------------------ */
+
+
+ /**
+ * Set the URI which the workflow will open a conduit connection to when
+ * @{method:establishConduit} is called. Arcanist makes an effort to set
+ * this by default for all workflows (by reading ##.arcconfig## and/or the
+ * value of ##--conduit-uri##) even if they don't need Conduit, so a workflow
+ * can generally upgrade into a conduit workflow later by just calling
+ * @{method:establishConduit}.
+ *
+ * You generally should not need to call this method unless you are
+ * specifically overriding the default URI. It is normally sufficient to
+ * just invoke @{method:establishConduit}.
+ *
+ * NOTE: You can not call this after a conduit has been established.
+ *
+ * @param string The URI to open a conduit to when @{method:establishConduit}
+ * is called.
+ * @return this
+ * @task conduit
+ */
+ final public function setConduitURI($conduit_uri) {
+ if ($this->conduit) {
+ throw new Exception(
+ "You can not change the Conduit URI after a conduit is already open.");
+ }
+ $this->conduitURI = $conduit_uri;
+ return $this;
+ }
+
+
+ /**
+ * Open a conduit channel to the server which was previously configured by
+ * calling @{method:setConduitURI}. Arcanist will do this automatically if
+ * the workflow returns ##true## from @{method:requiresConduit}, or you can
+ * later upgrade a workflow and build a conduit by invoking it manually.
+ *
+ * You must establish a conduit before you can make conduit calls.
+ *
+ * NOTE: You must call @{method:setConduitURI} before you can call this
+ * method.
+ *
+ * @return this
+ * @task conduit
+ */
+ final public function establishConduit() {
+ if ($this->conduit) {
+ return $this;
+ }
+
+ if (!$this->conduitURI) {
+ throw new Exception(
+ "You must specify a Conduit URI with setConduitURI() before you can ".
+ "establish a conduit.");
+ }
+
+ $this->conduit = new ConduitClient($this->conduitURI);
+
+ return $this;
+ }
+
+
+ /**
+ * Set credentials which will be used to authenticate against Conduit. These
+ * credentials can then be used to establish an authenticated connection to
+ * conduit by calling @{method:authenticateConduit}. Arcanist sets some
+ * defaults for all workflows regardless of whether or not they return true
+ * from @{method:requireAuthentication}, based on the ##~/.arcrc## and
+ * ##.arcconf## files if they are present. Thus, you can generally upgrade a
+ * workflow which does not require authentication into an authenticated
+ * workflow by later invoking @{method:requireAuthentication}. You should not
+ * normally need to call this method unless you are specifically overriding
+ * the defaults.
+ *
+ * NOTE: You can not call this method after calling
+ * @{method:authenticateConduit}.
+ *
+ * @param dict A credential dictionary, see @{method:authenticateConduit}.
+ * @return this
+ * @task conduit
+ */
+ final public function setConduitCredentials(array $credentials) {
+ if ($this->conduitAuthenticated) {
+ throw new Exception(
+ "You may not set new credentials after authenticating conduit.");
+ }
+
+ $this->conduitCredentials = $credentials;
+ return $this;
+ }
+
+
+ /**
+ * Open and authenticate a conduit connection to a Phabricator server using
+ * provided credentials. Normally, Arcanist does this for you automatically
+ * when you return true from @{method:requiresAuthentication}, but you can
+ * also upgrade an existing workflow to one with an authenticated conduit
+ * by invoking this method manually.
+ *
+ * You must authenticate the conduit before you can make authenticated conduit
+ * calls (almost all calls require authentication).
+ *
+ * This method uses credentials provided via @{method:setConduitCredentials}
+ * to authenticate to the server:
+ *
+ * - ##user## (required) The username to authenticate with.
+ * - ##certificate## (required) The Conduit certificate to use.
+ * - ##description## (optional) Description of the invoking command.
+ *
+ * Successful authentication allows you to call @{method:getUserGUID} and
+ * @{method:getUserName}, as well as use the client you access with
+ * @{method:getConduit} to make authenticated calls.
+ *
+ * NOTE: You must call @{method:setConduitURI} and
+ * @{method:setConduitCredentials} before you invoke this method.
+ *
+ * @return this
+ * @task conduit
+ */
+ final public function authenticateConduit() {
+ if ($this->conduitAuthenticated) {
+ return $this;
+ }
+
+ $this->establishConduit();
+
+ $credentials = $this->conduitCredentials;
+ if (!$credentials) {
+ throw new Exception(
+ "Set conduit credentials with setConduitCredentials() before ".
+ "authenticating conduit!");
+ }
+
+ if (empty($credentials['user']) || empty($credentials['certificate'])) {
+ throw new Exception(
+ "Credentials must include a 'user' and a 'certificate'.");
+ }
+
+ $description = idx($credentials, 'description', '');
+ $user = $credentials['user'];
+ $certificate = $credentials['certificate'];
+
+ try {
+ $connection = $this->getConduit()->callMethodSynchronous(
+ 'conduit.connect',
+ array(
+ 'client' => 'arc',
+ 'clientVersion' => 2,
+ 'clientDescription' => php_uname('n').':'.$description,
+ 'user' => $user,
+ 'certificate' => $certificate,
+ 'host' => $this->conduitURI,
+ ));
+ } catch (ConduitClientException $ex) {
+ if ($ex->getErrorCode() == 'ERR-NO-CERTIFICATE' ||
+ $ex->getErrorCode() == 'ERR-INVALID-USER') {
+ $message =
+ "\n".
+ phutil_console_format(
+ "YOU NEED TO __INSTALL A CERTIFICATE__ TO LOGIN TO PHABRICATOR").
+ "\n\n".
+ phutil_console_format(
+ " To do this, run: **arc install-certificate**").
+ "\n\n".
+ "The server '{$conduit_uri}' rejected your request:".
+ "\n".
+ $ex->getMessage();
+ throw new ArcanistUsageException($message);
+ } else {
+ throw $ex;
+ }
+ }
+
+ $this->userName = $user;
+ $this->userGUID = $connection['userPHID'];
+
+ $this->conduitAuthenticated = true;
+
+ return $this;
+ }
+
+
+ /**
+ * Override this to return true if your workflow requires a conduit channel.
+ * Arc will build the channel for you before your workflow executes. This
+ * implies that you only need an unauthenticated channel; if you need
+ * authentication, override @{method:requiresAuthentication}.
+ *
+ * @return bool True if arc should build a conduit channel before running
+ * the workflow.
+ * @task conduit
+ */
+ public function requiresConduit() {
+ return false;
+ }
+
+
+ /**
+ * Override this to return true if your workflow requires an authenticated
+ * conduit channel. This implies that it requires a conduit. Arc will build
+ * and authenticate the channel for you before the workflow executes.
+ *
+ * @return bool True if arc should build an authenticated conduit channel
+ * before running the workflow.
+ * @task conduit
+ */
+ public function requiresAuthentication() {
+ return false;
+ }
+
+
+ /**
+ * Returns the PHID for the user once they've authenticated via Conduit.
+ *
+ * NOTE: This method will be deprecated and renamed to ##getUserPHID()## at
+ * some point.
+ *
+ * @return phid Authenticated user PHID.
+ * @task conduit
+ */
+ final public function getUserGUID() {
+ if (!$this->userGUID) {
+ $workflow = get_class($this);
+ throw new Exception(
+ "This workflow ('{$workflow}') requires authentication, override ".
+ "requiresAuthentication() to return true.");
+ }
+ return $this->userGUID;
+ }
+
+ /**
+ * Return the username for the user once they've authenticated via Conduit.
+ *
+ * @return string Authenticated username.
+ * @task conduit
+ */
+ final public function getUserName() {
+ return $this->userName;
+ }
+
+
+ /**
+ * Get the established @{class@libphutil:ConduitClient} in order to make
+ * Conduit method calls. Before the client is available it must be connected,
+ * either implicitly by making @{method:requireConduit} or
+ * @{method:requireAuthentication} return true, or explicitly by calling
+ * @{method:establishConduit} or @{method:authenticateConduit}.
+ *
+ * @return @{class@libphutil:ConduitClient} Live conduit client.
+ * @task conduit
+ */
+ final public function getConduit() {
+ if (!$this->conduit) {
+ $workflow = get_class($this);
+ throw new Exception(
+ "This workflow ('{$workflow}') requires a Conduit, override ".
+ "requiresConduit() to return true.");
+ }
+ return $this->conduit;
+ }
+
+
public function setArcanistConfiguration($arcanist_configuration) {
$this->arcanistConfiguration = $arcanist_configuration;
return $this;
}
public function getArcanistConfiguration() {
return $this->arcanistConfiguration;
}
public function getCommandHelp() {
return get_class($this).": Undocumented";
}
public function requiresWorkingCopy() {
return false;
}
- public function requiresConduit() {
- return false;
- }
-
- public function requiresAuthentication() {
- return false;
- }
public function requiresRepositoryAPI() {
return false;
}
public function setCommand($command) {
$this->command = $command;
return $this;
}
public function getCommand() {
return $this->command;
}
- public function setUserName($user_name) {
- $this->userName = $user_name;
- return $this;
- }
-
- public function getUserName() {
- return $this->userName;
- }
-
public function getArguments() {
return array();
}
public function setWorkingDirectory($working_directory) {
$this->workingDirectory = $working_directory;
return $this;
}
public function getWorkingDirectory() {
return $this->workingDirectory;
}
private function setParentWorkflow($parent_workflow) {
$this->parentWorkflow = $parent_workflow;
return $this;
}
protected function getParentWorkflow() {
return $this->parentWorkflow;
}
public function buildChildWorkflow($command, array $argv) {
$arc_config = $this->getArcanistConfiguration();
$workflow = $arc_config->buildWorkflow($command);
$workflow->setParentWorkflow($this);
$workflow->setCommand($command);
if ($this->repositoryAPI) {
$workflow->setRepositoryAPI($this->repositoryAPI);
}
if ($this->userGUID) {
- $workflow->setUserGUID($this->getUserGUID());
- $workflow->setUserName($this->getUserName());
+ $workflow->userGUID = $this->getUserGUID();
+ $workflow->userName = $this->getUserName();
}
if ($this->conduit) {
- $workflow->setConduit($this->conduit);
+ $workflow->conduit = $this->conduit;
}
if ($this->workingCopy) {
$workflow->setWorkingCopy($this->workingCopy);
}
$workflow->setArcanistConfiguration($arc_config);
$workflow->parseArguments(array_values($argv));
return $workflow;
}
public function getArgument($key, $default = null) {
$args = $this->arguments;
if (!array_key_exists($key, $args)) {
return $default;
}
return $args[$key];
}
final public function getCompleteArgumentSpecification() {
$spec = $this->getArguments();
$arc_config = $this->getArcanistConfiguration();
$command = $this->getCommand();
$spec += $arc_config->getCustomArgumentsForCommand($command);
return $spec;
}
public function parseArguments(array $args) {
$spec = $this->getCompleteArgumentSpecification();
$dict = array();
$more_key = null;
if (!empty($spec['*'])) {
$more_key = $spec['*'];
unset($spec['*']);
$dict[$more_key] = array();
}
$short_to_long_map = array();
foreach ($spec as $long => $options) {
if (!empty($options['short'])) {
$short_to_long_map[$options['short']] = $long;
}
}
$more = array();
for ($ii = 0; $ii < count($args); $ii++) {
$arg = $args[$ii];
$arg_name = null;
$arg_key = null;
if ($arg == '--') {
$more = array_merge(
$more,
array_slice($args, $ii + 1));
break;
} else if (!strncmp($arg, '--', 2)) {
$arg_key = substr($arg, 2);
if (!array_key_exists($arg_key, $spec)) {
throw new ArcanistUsageException(
"Unknown argument '{$arg_key}'. Try 'arc help'.");
}
} else if (!strncmp($arg, '-', 1)) {
$arg_key = substr($arg, 1);
if (empty($short_to_long_map[$arg_key])) {
throw new ArcanistUsageException(
"Unknown argument '{$arg_key}'. Try 'arc help'.");
}
$arg_key = $short_to_long_map[$arg_key];
} else {
$more[] = $arg;
continue;
}
$options = $spec[$arg_key];
if (empty($options['param'])) {
$dict[$arg_key] = true;
} else {
if ($ii == count($args) - 1) {
throw new ArcanistUsageException(
"Option '{$arg}' requires a parameter.");
}
$dict[$arg_key] = $args[$ii + 1];
$ii++;
}
}
if ($more) {
if ($more_key) {
$dict[$more_key] = $more;
} else {
$example = reset($more);
throw new ArcanistUsageException(
"Unrecognized argument '{$example}'. Try 'arc help'.");
}
}
foreach ($dict as $key => $value) {
if (empty($spec[$key]['conflicts'])) {
continue;
}
foreach ($spec[$key]['conflicts'] as $conflict => $more) {
if (isset($dict[$conflict])) {
if ($more) {
$more = ': '.$more;
} else {
$more = '.';
}
// TODO: We'll always display these as long-form, when the user might
// have typed them as short form.
throw new ArcanistUsageException(
"Arguments '--{$key}' and '--{$conflict}' are mutually exclusive".
$more);
}
}
}
$this->arguments = $dict;
$this->didParseArguments();
return $this;
}
protected function didParseArguments() {
// Override this to customize workflow argument behavior.
}
public function getWorkingCopy() {
if (!$this->workingCopy) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a working copy, override ".
"requiresWorkingCopy() to return true.");
}
return $this->workingCopy;
}
public function setWorkingCopy(
ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
- public function getConduit() {
- if (!$this->conduit) {
- $workflow = get_class($this);
- throw new Exception(
- "This workflow ('{$workflow}') requires a Conduit, override ".
- "requiresConduit() to return true.");
- }
- return $this->conduit;
- }
-
- public function setConduit(ConduitClient $conduit) {
- $this->conduit = $conduit;
- return $this;
- }
-
- public function getUserGUID() {
- if (!$this->userGUID) {
- $workflow = get_class($this);
- throw new Exception(
- "This workflow ('{$workflow}') requires authentication, override ".
- "requiresAuthentication() to return true.");
- }
- return $this->userGUID;
- }
-
- public function setUserGUID($guid) {
- $this->userGUID = $guid;
- return $this;
- }
-
public function setRepositoryAPI($api) {
$this->repositoryAPI = $api;
return $this;
}
public function getRepositoryAPI() {
if (!$this->repositoryAPI) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a Repository API, override ".
"requiresRepositoryAPI() to return true.");
}
return $this->repositoryAPI;
}
protected function shouldRequireCleanUntrackedFiles() {
return empty($this->arguments['allow-untracked']);
}
protected function requireCleanWorkingCopy() {
$api = $this->getRepositoryAPI();
$working_copy_desc = phutil_console_format(
" Working copy: __%s__\n\n",
$api->getPath());
$untracked = $api->getUntrackedChanges();
if ($this->shouldRequireCleanUntrackedFiles()) {
if (!empty($untracked)) {
echo "You have untracked files in this working copy.\n\n".
$working_copy_desc.
" Untracked files in working copy:\n".
" ".implode("\n ", $untracked)."\n\n";
if ($api instanceof ArcanistGitAPI) {
echo phutil_console_wrap(
"Since you don't have '.gitignore' rules for these files and have ".
"not listed them in '.git/info/exclude', you may have forgotten ".
"to 'git add' them to your commit.");
} else if ($api instanceof ArcanistSubversionAPI) {
echo phutil_console_wrap(
"Since you don't have 'svn:ignore' rules for these files, you may ".
"have forgotten to 'svn add' them.");
}
$prompt = "Do you want to continue without adding these files?";
if (!phutil_console_confirm($prompt, $default_no = false)) {
throw new ArcanistUserAbortException();
}
}
}
$incomplete = $api->getIncompleteChanges();
if ($incomplete) {
throw new ArcanistUsageException(
"You have incompletely checked out directories in this working copy. ".
"Fix them before proceeding.\n\n".
$working_copy_desc.
" Incomplete directories in working copy:\n".
" ".implode("\n ", $incomplete)."\n\n".
"You can fix these paths by running 'svn update' on them.");
}
$conflicts = $api->getMergeConflicts();
if ($conflicts) {
throw new ArcanistUsageException(
"You have merge conflicts in this working copy. Resolve merge ".
"conflicts before proceeding.\n\n".
$working_copy_desc.
" Conflicts in working copy:\n".
" ".implode("\n ", $conflicts)."\n");
}
$unstaged = $api->getUnstagedChanges();
if ($unstaged) {
throw new ArcanistUsageException(
"You have unstaged changes in this working copy. Stage and commit (or ".
"revert) them before proceeding.\n\n".
$working_copy_desc.
" Unstaged changes in working copy:\n".
" ".implode("\n ", $unstaged)."\n");
}
$uncommitted = $api->getUncommittedChanges();
if ($uncommitted) {
throw new ArcanistUsageException(
"You have uncommitted changes in this branch. Commit (or revert) them ".
"before proceeding.\n\n".
$working_copy_desc.
" Uncommitted changes in working copy\n".
" ".implode("\n ", $uncommitted)."\n");
}
}
protected function chooseRevision(
array $revision_data,
$revision_id,
$prompt = null) {
$revisions = array();
foreach ($revision_data as $data) {
$ref = ArcanistDifferentialRevisionRef::newFromDictionary($data);
$revisions[$ref->getID()] = $ref;
}
if ($revision_id) {
$revision_id = $this->normalizeRevisionID($revision_id);
if (empty($revisions[$revision_id])) {
throw new ArcanistChooseInvalidRevisionException();
}
return $revisions[$revision_id];
}
if (!count($revisions)) {
throw new ArcanistChooseNoRevisionsException();
}
$repository_api = $this->getRepositoryAPI();
$candidates = array();
$cur_path = $repository_api->getPath();
foreach ($revisions as $revision) {
$source_path = $revision->getSourcePath();
if ($source_path == $cur_path) {
$candidates[] = $revision;
}
}
if (count($candidates) == 1) {
$candidate = reset($candidates);
$revision_id = $candidate->getID();
}
if ($revision_id) {
return $revisions[$revision_id];
}
$revision_indexes = array_keys($revisions);
echo "\n";
$ii = 1;
foreach ($revisions as $revision) {
echo ' ['.$ii++.'] D'.$revision->getID().' '.$revision->getName()."\n";
}
while (true) {
$id = phutil_console_prompt($prompt);
$id = trim(strtoupper($id), 'D');
if (isset($revisions[$id])) {
return $revisions[$id];
}
if (isset($revision_indexes[$id - 1])) {
return $revisions[$revision_indexes[$id - 1]];
}
}
}
protected function loadDiffBundleFromConduit(
ConduitClient $conduit,
$diff_id) {
return $this->loadBundleFromConduit(
$conduit,
array(
'diff_id' => $diff_id,
));
}
protected function loadRevisionBundleFromConduit(
ConduitClient $conduit,
$revision_id) {
return $this->loadBundleFromConduit(
$conduit,
array(
'revision_id' => $revision_id,
));
}
private function loadBundleFromConduit(
ConduitClient $conduit,
$params) {
$future = $conduit->callMethod('differential.getdiff', $params);
$diff = $future->resolve();
$changes = array();
foreach ($diff['changes'] as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setConduit($conduit);
return $bundle;
}
protected function getChangedLines($path, $mode) {
if (is_dir($path)) {
return array();
}
$change = $this->getChange($path);
$lines = $change->getChangedLines($mode);
return array_keys($lines);
}
private function getChange($path) {
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) {
if (empty($this->changeCache[$path])) {
$diff = $repository_api->getRawDiffText($path);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
if (count($changes) != 1) {
throw new Exception("Expected exactly one change.");
}
$this->changeCache[$path] = reset($changes);
}
} else {
if (empty($this->changeCache)) {
$diff = $repository_api->getFullGitDiff();
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
foreach ($changes as $change) {
$this->changeCache[$change->getCurrentPath()] = $change;
}
}
}
if (empty($this->changeCache[$path])) {
if ($repository_api instanceof ArcanistGitAPI) {
// This can legitimately occur under git if you make a change, "git
// commit" it, and then revert the change in the working copy and run
// "arc lint".
$change = new ArcanistDiffChange();
$change->setCurrentPath($path);
return $change;
} else {
throw new Exception(
"Trying to get change for unchanged path '{$path}'!");
}
}
return $this->changeCache[$path];
}
final public function willRunWorkflow() {
$spec = $this->getCompleteArgumentSpecification();
foreach ($this->arguments as $arg => $value) {
if (empty($spec[$arg])) {
continue;
}
$options = $spec[$arg];
if (!empty($options['supports'])) {
$system_name = $this->getRepositoryAPI()->getSourceControlSystemName();
if (!in_array($system_name, $options['supports'])) {
$extended_info = null;
if (!empty($options['nosupport'][$system_name])) {
$extended_info = ' '.$options['nosupport'][$system_name];
}
throw new ArcanistUsageException(
"Option '--{$arg}' is not supported under {$system_name}.".
$extended_info);
}
}
}
}
protected function parseGitRelativeCommit(ArcanistGitAPI $api, array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException(
"Specify exactly one commit.");
}
$base = reset($argv);
if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
$merge_base = $base;
} else {
list($err, $merge_base) = exec_manual(
'(cd %s; git merge-base %s HEAD)',
$api->getPath(),
$base);
if ($err) {
throw new ArcanistUsageException(
"Unable to parse git commit name '{$base}'.");
}
}
$api->setRelativeCommit(trim($merge_base));
}
protected function normalizeRevisionID($revision_id) {
return ltrim(strtoupper($revision_id), 'D');
}
protected function shouldShellComplete() {
return true;
}
protected function getShellCompletions(array $argv) {
return array();
}
protected function getSupportedRevisionControlSystems() {
return array('any');
}
protected function getPassthruArgumentsAsMap($command) {
$map = array();
foreach ($this->getCompleteArgumentSpecification() as $key => $spec) {
if (!empty($spec['passthru'][$command])) {
if (isset($this->arguments[$key])) {
$map[$key] = $this->arguments[$key];
}
}
}
return $map;
}
protected function getPassthruArgumentsAsArgv($command) {
$spec = $this->getCompleteArgumentSpecification();
$map = $this->getPassthruArgumentsAsMap($command);
$argv = array();
foreach ($map as $key => $value) {
$argv[] = '--'.$key;
if (!empty($spec[$key]['param'])) {
$argv[] = $value;
}
}
return $argv;
}
public static function getUserConfigurationFileLocation() {
return getenv('HOME').'/.arcrc';
}
public static function readUserConfigurationFile() {
$user_config = array();
$user_config_path = self::getUserConfigurationFileLocation();
if (Filesystem::pathExists($user_config_path)) {
$mode = fileperms($user_config_path);
if (!$mode) {
throw new Exception("Unable to get perms of '{$user_config_path}'!");
}
if ($mode & 0177) {
// Mode should allow only owner access.
$prompt = "File permissions on your ~/.arcrc are too open. ".
"Fix them by chmod'ing to 600?";
if (!phutil_console_confirm($prompt, $default_no = false)) {
throw new ArcanistUsageException("Set ~/.arcrc to file mode 600.");
}
execx('chmod 600 %s', $user_config_path);
}
$user_config_data = Filesystem::readFile($user_config_path);
$user_config = json_decode($user_config_data, true);
if (!is_array($user_config)) {
throw new ArcanistUsageException(
"Your '~/.arcrc' file is not a valid JSON file.");
}
}
return $user_config;
}
}
diff --git a/src/workflow/base/__init__.php b/src/workflow/base/__init__.php
index aba8ed30..139552c7 100644
--- a/src/workflow/base/__init__.php
+++ b/src/workflow/base/__init__.php
@@ -1,23 +1,25 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'differential/revision');
phutil_require_module('arcanist', 'exception');
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'exception/usage/userabort');
phutil_require_module('arcanist', 'parser/bundle');
phutil_require_module('arcanist', 'parser/diff');
phutil_require_module('arcanist', 'parser/diff/change');
phutil_require_module('arcanist', 'repository/api/git');
+phutil_require_module('phutil', 'conduit/client');
phutil_require_module('phutil', 'console');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'future/exec');
+phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistBaseWorkflow.php');
diff --git a/src/workflow/call-conduit/ArcanistCallConduitWorkflow.php b/src/workflow/call-conduit/ArcanistCallConduitWorkflow.php
index c836f181..7a225dd3 100644
--- a/src/workflow/call-conduit/ArcanistCallConduitWorkflow.php
+++ b/src/workflow/call-conduit/ArcanistCallConduitWorkflow.php
@@ -1,95 +1,99 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Provides command-line access to the Conduit API.
*
* @group workflow
*/
class ArcanistCallConduitWorkflow extends ArcanistBaseWorkflow {
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**call-conduit** __method__
Supports: http, https
Allows you to make a raw Conduit method call:
- Run this command from a working directory.
- Call parameters are REQUIRED and read as a JSON blob from stdin.
- Results are written to stdout as a JSON blob.
This workflow is primarily useful for writing scripts which integrate
with Phabricator. Examples:
$ echo '{}' | arc call-conduit conduit.ping
$ echo '{"phid":"PHID-FILE-xxxx"}' | arc call-conduit file.download
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'method',
);
}
public function shouldShellComplete() {
return false;
}
public function requiresConduit() {
return true;
}
+ public function requiresAuthentication() {
+ return true;
+ }
+
public function run() {
$method = $this->getArgument('method', array());
if (count($method) !== 1) {
throw new ArcanistUsageException(
"Provide exactly one Conduit method name.");
}
$method = reset($method);
$params = @file_get_contents('php://stdin');
$params = json_decode($params, true);
if (!is_array($params)) {
throw new ArcanistUsageException(
"Provide method parameters on stdin as a JSON blob.");
}
$error = null;
$error_message = null;
try {
$result = $this->getConduit()->callMethodSynchronous(
$method,
$params);
} catch (ConduitClientException $ex) {
$error = $ex->getErrorCode();
$error_message = $ex->getMessage();
$result = null;
}
echo json_encode(array(
'error' => $error,
'errorMessage' => $error_message,
'response' => $result,
))."\n";
return 0;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 13:21 (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
556928
Default Alt Text
(45 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment