diff --git a/src/configuration/ArcanistConfigurationManager.php b/src/configuration/ArcanistConfigurationManager.php index 3473a0bd..0d0a92e7 100644 --- a/src/configuration/ArcanistConfigurationManager.php +++ b/src/configuration/ArcanistConfigurationManager.php @@ -1,346 +1,356 @@ workingCopy = $working_copy; return $this; } /* -( Get config )--------------------------------------------------------- */ const CONFIG_SOURCE_RUNTIME = 'runtime'; const CONFIG_SOURCE_LOCAL = 'local'; const CONFIG_SOURCE_PROJECT = 'project'; const CONFIG_SOURCE_USER = 'user'; const CONFIG_SOURCE_SYSTEM = 'system'; const CONFIG_SOURCE_DEFAULT = 'default'; public function getProjectConfig($key) { if ($this->workingCopy) { return $this->workingCopy->getProjectConfig($key); } return null; } public function getLocalConfig($key) { if ($this->workingCopy) { return $this->workingCopy->getLocalConfig($key); } return null; } public function getWorkingCopyIdentity() { return $this->workingCopy; } /** * Read a configuration directive from any available configuration source. * This includes the directive in local, user and system configuration in * addition to project configuration, and configuration provided as command * arguments ("runtime"). * The precedence is runtime > local > project > user > system * * @param key Key to read. * @param wild Default value if key is not found. * @return wild Value, or default value if not found. * * @task config */ public function getConfigFromAnySource($key, $default = null) { $all = $this->getConfigFromAllSources($key); return empty($all) ? $default : head($all); } /** * For the advanced case where you want customized configuration handling. * * Reads the configuration from all available sources, returning a map (array) * of results, with the source as key. Missing values will not be in the map, * so an empty array will be returned if no results are found. * * The map is ordered by the canonical sources precedence, which is: * runtime > local > project > user > system * * @param key Key to read * @return array Mapping of source => value read. Sources with no value are * not in the array. * * @task config */ public function getConfigFromAllSources($key) { $results = array(); $settings = new ArcanistSettings(); $pval = idx($this->runtimeConfig, $key); if ($pval !== null) { $results[self::CONFIG_SOURCE_RUNTIME] = $settings->willReadValue($key, $pval); } $pval = $this->getLocalConfig($key); if ($pval !== null) { $results[self::CONFIG_SOURCE_LOCAL] = $settings->willReadValue($key, $pval); } $pval = $this->getProjectConfig($key); if ($pval !== null) { $results[self::CONFIG_SOURCE_PROJECT] = $settings->willReadValue($key, $pval); } $user_config = $this->readUserArcConfig(); - $pval = idx($user_config, $key); + + // For "aliases" coming from the user config file specifically, read the + // top level "aliases" key instead of the "aliases" key inside the "config" + // setting. Aliases were originally user-specific but later became standard + // configuration, which is why this works oddly. + if ($key === 'aliases') { + $pval = idx($this->readUserConfigurationFile(), $key); + } else { + $pval = idx($user_config, $key); + } + if ($pval !== null) { $results[self::CONFIG_SOURCE_USER] = $settings->willReadValue($key, $pval); } $system_config = $this->readSystemArcConfig(); $pval = idx($system_config, $key); if ($pval !== null) { $results[self::CONFIG_SOURCE_SYSTEM] = $settings->willReadValue($key, $pval); } $default_config = $this->readDefaultConfig(); if (array_key_exists($key, $default_config)) { $results[self::CONFIG_SOURCE_DEFAULT] = $default_config[$key]; } return $results; } /** * Sets a runtime config value that takes precedence over any static * config values. * * @param key Key to set. * @param value The value of the key. * * @task config */ public function setRuntimeConfig($key, $value) { $this->runtimeConfig[$key] = $value; return $this; } /* -( Read/write config )--------------------------------------------------- */ public function readLocalArcConfig() { if ($this->workingCopy) { return $this->workingCopy->readLocalArcConfig(); } return array(); } public function writeLocalArcConfig(array $config) { if ($this->workingCopy) { return $this->workingCopy->writeLocalArcConfig($config); } throw new Exception(pht('No working copy to write config to!')); } /** * This is probably not the method you're looking for; try * @{method:readUserArcConfig}. */ public function readUserConfigurationFile() { if ($this->userConfigCache === null) { $user_config = array(); $user_config_path = $this->getUserConfigurationFileLocation(); $console = PhutilConsole::getConsole(); if (Filesystem::pathExists($user_config_path)) { $console->writeLog( "%s\n", pht( 'Config: Reading user configuration file "%s"...', $user_config_path)); if (!phutil_is_windows()) { $mode = fileperms($user_config_path); if (!$mode) { throw new Exception( pht( 'Unable to read file permissions for "%s"!', $user_config_path)); } if ($mode & 0177) { // Mode should allow only owner access. $prompt = pht( "File permissions on your %s are too open. ". "Fix them by chmod'ing to 600?", '~/.arcrc'); if (!phutil_console_confirm($prompt, $default_no = false)) { throw new ArcanistUsageException( pht('Set %s to file mode 600.', '~/.arcrc')); } execx('chmod 600 %s', $user_config_path); // Drop the stat cache so we don't read the old permissions if // we end up here again. If we don't do this, we may prompt the user // to fix permissions multiple times. clearstatcache(); } } $user_config_data = Filesystem::readFile($user_config_path); try { $user_config = phutil_json_decode($user_config_data); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht("Your '%s' file is not a valid JSON file.", '~/.arcrc'), $ex); } } else { $console->writeLog( "%s\n", pht( 'Config: Did not find user configuration at "%s".', $user_config_path)); } $this->userConfigCache = $user_config; } return $this->userConfigCache; } /** * This is probably not the method you're looking for; try * @{method:writeUserArcConfig}. */ public function writeUserConfigurationFile($config) { $json_encoder = new PhutilJSON(); $json = $json_encoder->encodeFormatted($config); $path = $this->getUserConfigurationFileLocation(); Filesystem::writeFile($path, $json); if (!phutil_is_windows()) { execx('chmod 600 %s', $path); } } public function setUserConfigurationFileLocation($custom_arcrc) { if (!Filesystem::pathExists($custom_arcrc)) { throw new Exception( pht('Custom %s file was specified, but it was not found!', 'arcrc')); } $this->customArcrcFilename = $custom_arcrc; $this->userConfigCache = null; return $this; } public function getUserConfigurationFileLocation() { if (strlen($this->customArcrcFilename)) { return $this->customArcrcFilename; } if (phutil_is_windows()) { return getenv('APPDATA').'/.arcrc'; } else { return getenv('HOME').'/.arcrc'; } } public function readUserArcConfig() { return idx($this->readUserConfigurationFile(), 'config', array()); } public function writeUserArcConfig(array $options) { $config = $this->readUserConfigurationFile(); $config['config'] = $options; $this->writeUserConfigurationFile($config); } public function getSystemArcConfigLocation() { if (phutil_is_windows()) { return Filesystem::resolvePath( 'Phabricator/Arcanist/config', getenv('ProgramData')); } else { return '/etc/arcconfig'; } } public function readSystemArcConfig() { static $system_config; if ($system_config === null) { $system_config = array(); $system_config_path = $this->getSystemArcConfigLocation(); $console = PhutilConsole::getConsole(); if (Filesystem::pathExists($system_config_path)) { $console->writeLog( "%s\n", pht( 'Config: Reading system configuration file "%s"...', $system_config_path)); $file = Filesystem::readFile($system_config_path); try { $system_config = phutil_json_decode($file); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht( "Your '%s' file is not a valid JSON file.", $system_config_path), $ex); } } else { $console->writeLog( "%s\n", pht( 'Config: Did not find system configuration at "%s".', $system_config_path)); } } return $system_config; } public function applyRuntimeArcConfig($args) { $arcanist_settings = new ArcanistSettings(); $options = $args->getArg('config'); foreach ($options as $opt) { $opt_config = preg_split('/=/', $opt, 2); if (count($opt_config) !== 2) { throw new ArcanistUsageException( pht( "Argument was '%s', but must be '%s'. For example, %s", $opt, 'name=value', 'history.immutable=true')); } list($key, $value) = $opt_config; $value = $arcanist_settings->willWriteValue($key, $value); $this->setRuntimeConfig($key, $value); } return $this->runtimeConfig; } public function readDefaultConfig() { $settings = new ArcanistSettings(); return $settings->getDefaultSettings(); } } diff --git a/src/workflow/ArcanistAliasWorkflow.php b/src/workflow/ArcanistAliasWorkflow.php index 7fc9dfe7..b27b3ebf 100644 --- a/src/workflow/ArcanistAliasWorkflow.php +++ b/src/workflow/ArcanistAliasWorkflow.php @@ -1,243 +1,240 @@ 'argv', ); } public static function getAliases( ArcanistConfigurationManager $configuration_manager) { + $sources = $configuration_manager->getConfigFromAllSources('aliases'); - $working_copy_config_aliases = - $configuration_manager->getProjectConfig('aliases'); - if (!$working_copy_config_aliases) { - $working_copy_config_aliases = array(); + $aliases = array(); + foreach ($sources as $source) { + $aliases += $source; } - $user_config_aliases = idx( - $configuration_manager->readUserConfigurationFile(), - 'aliases', - array()); - return $user_config_aliases + $working_copy_config_aliases; + + return $aliases; } private function writeAliases(array $aliases) { $config = $this->getConfigurationManager()->readUserConfigurationFile(); $config['aliases'] = $aliases; $this->getConfigurationManager()->writeUserConfigurationFile($config); } public function run() { $aliases = self::getAliases($this->getConfigurationManager()); $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)) { throw new ArcanistUsageException( pht( 'You can not create an alias for "%s" because it is a '. 'builtin command. "%s" can only create new commands.', "arc {$alias}", 'arc alias')); } $new_alias = array_slice($argv, 1); $command = implode(' ', $new_alias); if (self::isShellCommandAlias($command)) { echo tsprintf( "%s\n", pht( 'Aliased "%s" to shell command "%s".', "arc {$alias}", substr($command, 1))); } else { echo tsprintf( "%s\n", pht( 'Aliased "%s" to "%s".', "arc {$alias}", "arc {$command}")); } $aliases[$alias] = $new_alias; $this->writeAliases($aliases); } return 0; } public static function isShellCommandAlias($command) { return preg_match('/^!/', $command); } public static function resolveAliases( $command, ArcanistConfiguration $config, array $argv, ArcanistConfigurationManager $configuration_manager) { $aliases = self::getAliases($configuration_manager); if (!isset($aliases[$command])) { return array(null, $argv); } $new_command = head($aliases[$command]); if (self::isShellCommandAlias($new_command)) { return array($new_command, $argv); } $workflow = $config->buildWorkflow($new_command); if (!$workflow) { return array(null, $argv); } $alias_argv = array_slice($aliases[$command], 1); foreach (array_reverse($alias_argv) as $alias_arg) { if (!in_array($alias_arg, $argv)) { array_unshift($argv, $alias_arg); } } return array($new_command, $argv); } private function printAliases(array $aliases) { if (!$aliases) { echo tsprintf( "%s\n", pht('You have not defined any aliases yet.')); return; } $table = id(new PhutilConsoleTable()) ->addColumn('input', array('title' => pht('Alias'))) ->addColumn('command', array('title' => pht('Command'))) ->addColumn('type', array('title' => pht('Type'))); ksort($aliases); foreach ($aliases as $alias => $binding) { $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( 'input' => "arc {$alias}", 'type' => $type, 'command' => $command, ); $table->addRow($row); } $table->draw(); } private function removeAlias(array $aliases, $alias) { if (empty($aliases[$alias])) { echo tsprintf( "%s\n", pht('No alias "%s" to remove.', $alias)); return; } $command = implode(' ', $aliases[$alias]); if (self::isShellCommandAlias($command)) { 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}")); } $ok = phutil_console_confirm(pht('Delete this alias?')); if (!$ok) { throw new ArcanistUserAbortException(); } unset($aliases[$alias]); $this->writeAliases($aliases); echo tsprintf( "%s\n", pht( 'Removed alias "%s".', "arc {$alias}")); } }