Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F969539
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
425 KB
Referenced Files
None
Subscribers
None
View Options
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/scripts/arcanist.php b/scripts/arcanist.php
index 7b0088e9..2dc7611a 100755
--- a/scripts/arcanist.php
+++ b/scripts/arcanist.php
@@ -1,604 +1,616 @@
#!/usr/bin/env php
<?php
sanity_check_environment();
require_once dirname(__FILE__).'/__init_script__.php';
ini_set('memory_limit', -1);
$original_argv = $argv;
$args = new PhutilArgumentParser($argv);
$args->parseStandardArguments();
$args->parsePartial(
array(
array(
'name' => 'load-phutil-library',
'param' => 'path',
'help' => 'Load a libphutil library.',
'repeat' => true,
),
array(
'name' => 'skip-arcconfig',
),
array(
'name' => 'conduit-uri',
'param' => 'uri',
'help' => 'Connect to Phabricator install specified by __uri__.',
),
array(
'name' => 'conduit-version',
'param' => 'version',
'help' => '(Developers) Mock client version in protocol handshake.',
),
array(
'name' => 'conduit-timeout',
'param' => 'timeout',
'help' => 'Set Conduit timeout (in seconds).',
),
));
$config_trace_mode = $args->getArg('trace');
$force_conduit = $args->getArg('conduit-uri');
$force_conduit_version = $args->getArg('conduit-version');
$conduit_timeout = $args->getArg('conduit-timeout');
$skip_arcconfig = $args->getArg('skip-arcconfig');
$load = $args->getArg('load-phutil-library');
$help = $args->getArg('help');
$argv = $args->getUnconsumedArgumentVector();
$args = array_values($argv);
$working_directory = getcwd();
$console = PhutilConsole::getConsole();
$config = null;
$workflow = null;
try {
$console->writeLog(
"libphutil loaded from '%s'.\n",
phutil_get_library_root('phutil'));
$console->writeLog(
"arcanist loaded from '%s'.\n",
phutil_get_library_root('arcanist'));
if (!$args) {
if ($help) {
$args = array('help');
} else {
throw new ArcanistUsageException("No command provided. Try 'arc help'.");
}
} else if ($help) {
array_unshift($args, 'help');
}
- $global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
- $system_config = ArcanistBaseWorkflow::readSystemArcConfig();
+ $configuration_manager = new ArcanistConfigurationManager();
+
+ $global_config = $configuration_manager->readUserArcConfig();
+ $system_config = $configuration_manager->readSystemArcConfig();
if ($skip_arcconfig) {
$working_copy = ArcanistWorkingCopyIdentity::newDummyWorkingCopy();
} else {
$working_copy =
ArcanistWorkingCopyIdentity::newFromPath($working_directory);
}
+ $configuration_manager->setWorkingCopyIdentity($working_copy);
reenter_if_this_is_arcanist_or_libphutil(
$console,
$working_copy,
$original_argv);
// Load additional libraries, which can provide new classes like configuration
// overrides, linters and lint engines, unit test engines, etc.
// If the user specified "--load-phutil-library" one or more times from
// the command line, we load those libraries **instead** of whatever else
// is configured. This is basically a debugging feature to let you force
// specific libraries to load regardless of the state of the world.
if ($load) {
$console->writeLog(
"Using '--load-phutil-library' flag, configuration will be ignored ".
"and configured libraries will not be loaded."."\n");
// Load the flag libraries. These must load, since the user specified them
// explicitly.
arcanist_load_libraries(
$load,
$must_load = true,
$lib_source = 'a "--load-phutil-library" flag',
$working_copy);
} else {
// Load libraries in system 'load' config. In contrast to global config, we
// fail hard here because this file is edited manually, so if 'arc' breaks
// that doesn't make it any more difficult to correct.
arcanist_load_libraries(
idx($system_config, 'load', array()),
$must_load = true,
$lib_source = 'the "load" setting in system config',
$working_copy);
// Load libraries in global 'load' config, as per "arc set-config load". We
// need to fail softly if these break because errors would prevent the user
// from running "arc set-config" to correct them.
arcanist_load_libraries(
idx($global_config, 'load', array()),
$must_load = false,
$lib_source = 'the "load" setting in global config',
$working_copy);
// Load libraries in ".arcconfig". Libraries here must load.
arcanist_load_libraries(
$working_copy->getConfig('load'),
$must_load = true,
$lib_source = 'the "load" setting in ".arcconfig"',
$working_copy);
}
- $user_config = ArcanistBaseWorkflow::readUserConfigurationFile();
+ $user_config = $configuration_manager->readUserConfigurationFile();
$config_class = $working_copy->getConfig('arcanist_configuration');
if ($config_class) {
$config = new $config_class();
} else {
$config = new ArcanistConfiguration();
}
$command = strtolower($args[0]);
$args = array_slice($args, 1);
- $workflow = $config->selectWorkflow($command, $args, $working_copy, $console);
+ $workflow = $config->selectWorkflow(
+ $command,
+ $args,
+ $configuration_manager,
+ $console);
+ $workflow->setConfigurationManager($configuration_manager);
$workflow->setArcanistConfiguration($config);
$workflow->setCommand($command);
$workflow->setWorkingDirectory($working_directory);
$workflow->parseArguments($args);
// Write the command into the environment so that scripts (for example, local
// Git commit hooks) can detect that they're being run via `arc` and change
// their behaviors.
putenv('ARCANIST='.$command);
if ($force_conduit_version) {
$workflow->forceConduitVersion($force_conduit_version);
}
if ($conduit_timeout) {
$workflow->setConduitTimeout($conduit_timeout);
}
$need_working_copy = $workflow->requiresWorkingCopy();
$need_conduit = $workflow->requiresConduit();
$need_auth = $workflow->requiresAuthentication();
$need_repository_api = $workflow->requiresRepositoryAPI();
$want_repository_api = $workflow->desiresRepositoryAPI();
$want_working_copy = $workflow->desiresWorkingCopy() ||
$want_repository_api;
$need_conduit = $need_conduit ||
$need_auth;
$need_working_copy = $need_working_copy ||
$need_repository_api;
if ($need_working_copy || $want_working_copy) {
if ($need_working_copy && !$working_copy->getProjectRoot()) {
throw new ArcanistUsageException(
"This command must be run in a Git, Mercurial or Subversion working ".
"copy.");
}
- $workflow->setWorkingCopy($working_copy);
+ $configuration_manager->setWorkingCopyIdentity($working_copy);
}
if ($force_conduit) {
$conduit_uri = $force_conduit;
} else {
- if ($working_copy->getConduitURI()) {
- $conduit_uri = $working_copy->getConduitURI();
+ $project_conduit_uri =
+ $configuration_manager->getProjectConfig('conduit_uri');
+ if ($project_conduit_uri) {
+ $conduit_uri = $project_conduit_uri;
} else {
$conduit_uri = idx($global_config, 'default');
}
}
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);
// Apply global CA bundle from configs.
- if ($ca_bundle = $working_copy->getConfigFromAnySource('https.cabundle')) {
+ $ca_bundle = $configuration_manager->getConfigFromAnySource('https.cabundle');
+ if ($ca_bundle) {
$ca_bundle = Filesystem::resolvePath(
$ca_bundle, $working_copy->getProjectRoot());
HTTPSFuture::setGlobalCABundleFromPath($ca_bundle);
}
$blind_key = 'https.blindly-trust-domains';
- $blind_trust = $working_copy->getConfigFromAnySource($blind_key);
+ $blind_trust = $configuration_manager->getConfigFromAnySource($blind_key);
if ($blind_trust) {
HTTPSFuture::setBlindlyTrustDomains($blind_trust);
}
if ($need_conduit) {
if (!$conduit_uri) {
$message = phutil_console_format(
"This command requires arc to connect to a Phabricator install, but ".
"no Phabricator installation is configured. To configure a ".
"Phabricator URI:\n\n".
" - set a default location with `arc set-config default <uri>`; or\n".
" - specify '--conduit-uri=uri' explicitly; or\n".
" - run 'arc' in a working copy with an '.arcconfig'.\n");
$message = phutil_console_wrap($message);
throw new ArcanistUsageException($message);
}
$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');
$description = implode(' ', $original_argv);
$credentials = array(
'user' => $user_name,
'certificate' => $certificate,
'description' => $description,
);
$workflow->setConduitCredentials($credentials);
if ($need_auth) {
if (!$user_name || !$certificate) {
$arc = 'arc';
if ($force_conduit) {
$arc .= csprintf(' --conduit-uri=%s', $conduit_uri);
}
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."));
}
$workflow->authenticateConduit();
}
if ($need_repository_api || ($want_repository_api && $working_copy)) {
- $repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
- $working_copy);
+ $repository_api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
+ $configuration_manager);
$workflow->setRepositoryAPI($repository_api);
}
- $listeners = $working_copy->getConfigFromAnySource('events.listeners');
+ $listeners = $configuration_manager->getConfigFromAnySource(
+ 'events.listeners');
if ($listeners) {
foreach ($listeners as $listener) {
$console->writeLog(
"Registering event listener '%s'.\n",
$listener);
try {
id(new $listener())->register();
} catch (PhutilMissingSymbolException $ex) {
// Continue anwyay, since you may otherwise be unable to run commands
// like `arc set-config events.listeners` in order to repair the damage
// you've caused.
$console->writeErr(
"ERROR: Failed to load event listener '%s'!\n",
$listener);
}
}
}
$config->willRunWorkflow($command, $workflow);
$workflow->willRunWorkflow();
try {
$err = $workflow->run();
$config->didRunWorkflow($command, $workflow, $err);
} catch (Exception $e) {
$workflow->finalize();
throw $e;
}
$workflow->finalize();
exit((int)$err);
} catch (ArcanistNoEffectException $ex) {
echo $ex->getMessage()."\n";
} catch (Exception $ex) {
$is_usage = ($ex instanceof ArcanistUsageException);
if ($is_usage) {
echo phutil_console_format(
"**Usage Exception:** %s\n",
$ex->getMessage());
}
if ($config) {
$config->didAbortWorkflow($command, $workflow, $ex);
}
if ($config_trace_mode) {
echo "\n";
throw $ex;
}
if (!$is_usage) {
echo phutil_console_format(
"**Exception**\n%s\n%s\n",
$ex->getMessage(),
"(Run with --trace for a full exception trace.)");
}
exit(1);
}
/**
* Perform some sanity checks against the possible diversity of PHP builds in
* the wild, like very old versions and builds that were compiled with flags
* that exclude core functionality.
*/
function sanity_check_environment() {
// NOTE: We don't have phutil_is_windows() yet here.
$is_windows = (DIRECTORY_SEPARATOR != '/');
// We use stream_socket_pair() which is not available on Windows earlier.
$min_version = ($is_windows ? '5.3.0' : '5.2.3');
$cur_version = phpversion();
if (version_compare($cur_version, $min_version, '<')) {
die_with_bad_php(
"You are running PHP version '{$cur_version}', which is older than ".
"the minimum version, '{$min_version}'. Update to at least ".
"'{$min_version}'.");
}
if ($is_windows) {
$need_functions = array(
'curl_init' => array('builtin-dll', 'php_curl.dll'),
);
} else {
$need_functions = array(
'curl_init' => array(
'text',
"You need to install the cURL PHP extension, maybe with ".
"'apt-get install php5-curl' or 'yum install php53-curl' or ".
"something similar."),
'json_decode' => array('flag', '--without-json'),
);
}
$problems = array();
$config = null;
$show_config = false;
foreach ($need_functions as $fname => $resolution) {
if (function_exists($fname)) {
continue;
}
static $info;
if ($info === null) {
ob_start();
phpinfo(INFO_GENERAL);
$info = ob_get_clean();
$matches = null;
if (preg_match('/^Configure Command =>\s*(.*?)$/m', $info, $matches)) {
$config = $matches[1];
}
}
$generic = true;
list($what, $which) = $resolution;
if ($what == 'flag' && strpos($config, $which) !== false) {
$show_config = true;
$generic = false;
$problems[] =
"This build of PHP was compiled with the configure flag '{$which}', ".
"which means it does not have the function '{$fname}()'. This ".
"function is required for arc to run. Rebuild PHP without this flag. ".
"You may also be able to build or install the relevant extension ".
"separately.";
}
if ($what == 'builtin-dll') {
$generic = false;
$problems[] =
"Your install of PHP does not have the '{$which}' extension enabled. ".
"Edit your php.ini file and uncomment the line which reads ".
"'extension={$which}'.";
}
if ($what == 'text') {
$generic = false;
$problems[] = $which;
}
if ($generic) {
$problems[] =
"This build of PHP is missing the required function '{$fname}()'. ".
"Rebuild PHP or install the extension which provides '{$fname}()'.";
}
}
if ($problems) {
if ($show_config) {
$problems[] = "PHP was built with this configure command:\n\n{$config}";
}
die_with_bad_php(implode("\n\n", $problems));
}
}
function die_with_bad_php($message) {
echo "\nPHP CONFIGURATION ERRORS\n\n";
echo $message;
echo "\n\n";
exit(1);
}
function arcanist_load_libraries(
$load,
$must_load,
$lib_source,
ArcanistWorkingCopyIdentity $working_copy) {
if (!$load) {
return;
}
if (!is_array($load)) {
$error = "Libraries specified by {$lib_source} are invalid; expected ".
"a list. Check your configuration.";
$console = PhutilConsole::getConsole();
$console->writeErr("WARNING: %s\n", $error);
return;
}
foreach ($load as $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.
//
// Note that absolute paths will just resolve absolutely through rule (1).
$resolved = false;
// Check inside the working copy. This also checks absolute paths, since
// they'll resolve absolute and just ignore the project root.
$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;
}
}
$console = PhutilConsole::getConsole();
$console->writeLog(
"Loading phutil library from '%s'...\n",
$location);
$error = null;
try {
phutil_load_library($location);
} catch (PhutilBootloaderException $ex) {
$error = "Failed to load phutil library at location '{$location}'. ".
"This library is specified by {$lib_source}. Check that the ".
"setting is correct and the library is located in the right ".
"place.";
if ($must_load) {
throw new ArcanistUsageException($error);
} else {
fwrite(STDERR, phutil_console_wrap('WARNING: '.$error."\n\n"));
}
} catch (PhutilLibraryConflictException $ex) {
if ($ex->getLibrary() != 'arcanist') {
throw $ex;
}
$arc_dir = dirname(dirname(__FILE__));
$error =
"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);
}
}
}
/**
* NOTE: SPOOKY BLACK MAGIC
*
* When arc is run in a copy of arcanist other than itself, or a copy of
* libphutil other than the one we loaded, reenter the script and force it
* to use the current working directory instead of the default.
*
* In the case of execution inside arcanist/, we force execution of the local
* arc binary.
*
* In the case of execution inside libphutil/, we force the local copy to load
* instead of the one selected by default rules.
*
* @param PhutilConsole Console.
* @param ArcanistWorkingCopyIdentity The current working copy.
* @param array Original arc arguments.
* @return void
*/
function reenter_if_this_is_arcanist_or_libphutil(
PhutilConsole $console,
ArcanistWorkingCopyIdentity $working_copy,
array $original_argv) {
$project_id = $working_copy->getProjectID();
if ($project_id != 'arcanist' && $project_id != 'libphutil') {
// We're not in a copy of arcanist or libphutil.
return;
}
$library_names = array(
'arcanist' => 'arcanist',
'libphutil' => 'phutil',
);
$library_root = phutil_get_library_root($library_names[$project_id]);
$project_root = $working_copy->getProjectRoot();
if (Filesystem::isDescendant($library_root, $project_root)) {
// We're in a copy of arcanist or libphutil, but already loaded the correct
// copy. Continue execution normally.
return;
}
if ($project_id == 'libphutil') {
$console->writeLog(
"This is libphutil! Forcing this copy to load...\n");
$original_argv[0] = dirname(phutil_get_library_root('arcanist')).'/bin/arc';
$libphutil_path = $project_root;
} else {
$console->writeLog(
"This is arcanist! Forcing this copy to run...\n");
$original_argv[0] = $project_root.'/bin/arc';
$libphutil_path = dirname(phutil_get_library_root('phutil'));
}
if (phutil_is_windows()) {
$err = phutil_passthru(
'set ARC_PHUTIL_PATH=%s & %Ls',
$libphutil_path,
$original_argv);
} else {
$err = phutil_passthru(
'ARC_PHUTIL_PATH=%s %Ls',
$libphutil_path,
$original_argv);
}
exit($err);
}
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index f3a8ae7f..0bcbce51 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,329 +1,330 @@
<?php
/**
* This file is automatically generated. Use 'arc liberate' to rebuild it.
* @generated
* @phutil-library-version 2
*/
phutil_register_library_map(array(
'__library_version__' => 2,
'class' =>
array(
'ArcanistAliasWorkflow' => 'workflow/ArcanistAliasWorkflow.php',
'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php',
'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php',
'ArcanistApacheLicenseLinter' => 'lint/linter/ArcanistApacheLicenseLinter.php',
'ArcanistArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistArcanistLinterTestCase.php',
'ArcanistBackoutWorkflow' => 'workflow/ArcanistBackoutWorkflow.php',
'ArcanistBaseCommitParser' => 'parser/ArcanistBaseCommitParser.php',
'ArcanistBaseCommitParserTestCase' => 'parser/__tests__/ArcanistBaseCommitParserTestCase.php',
'ArcanistBaseTestResultParser' => 'unit/engine/ArcanistBaseTestResultParser.php',
'ArcanistBaseUnitTestEngine' => 'unit/engine/ArcanistBaseUnitTestEngine.php',
'ArcanistBaseWorkflow' => 'workflow/ArcanistBaseWorkflow.php',
'ArcanistBaseXHPASTLinter' => 'lint/linter/ArcanistBaseXHPASTLinter.php',
'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php',
'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php',
'ArcanistBritishTestCase' => 'configuration/__tests__/ArcanistBritishTestCase.php',
'ArcanistBrowseWorkflow' => 'workflow/ArcanistBrowseWorkflow.php',
'ArcanistBundle' => 'parser/ArcanistBundle.php',
'ArcanistBundleTestCase' => 'parser/__tests__/ArcanistBundleTestCase.php',
'ArcanistCSSLintLinter' => 'lint/linter/ArcanistCSSLintLinter.php',
'ArcanistCSSLintLinterTestCase' => 'lint/linter/__tests__/ArcanistCSSLintLinterTestCase.php',
'ArcanistCSharpLinter' => 'lint/linter/ArcanistCSharpLinter.php',
'ArcanistCallConduitWorkflow' => 'workflow/ArcanistCallConduitWorkflow.php',
'ArcanistCapabilityNotSupportedException' => 'workflow/exception/ArcanistCapabilityNotSupportedException.php',
'ArcanistChooseInvalidRevisionException' => 'exception/ArcanistChooseInvalidRevisionException.php',
'ArcanistChooseNoRevisionsException' => 'exception/ArcanistChooseNoRevisionsException.php',
'ArcanistCloseRevisionWorkflow' => 'workflow/ArcanistCloseRevisionWorkflow.php',
'ArcanistCloseWorkflow' => 'workflow/ArcanistCloseWorkflow.php',
'ArcanistCommentRemover' => 'parser/ArcanistCommentRemover.php',
'ArcanistCommentRemoverTestCase' => 'parser/__tests__/ArcanistCommentRemoverTestCase.php',
'ArcanistCommitWorkflow' => 'workflow/ArcanistCommitWorkflow.php',
'ArcanistConduitLinter' => 'lint/linter/ArcanistConduitLinter.php',
'ArcanistConfiguration' => 'configuration/ArcanistConfiguration.php',
'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php',
+ 'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php',
'ArcanistCoverWorkflow' => 'workflow/ArcanistCoverWorkflow.php',
'ArcanistCppcheckLinter' => 'lint/linter/ArcanistCppcheckLinter.php',
'ArcanistCpplintLinter' => 'lint/linter/ArcanistCpplintLinter.php',
'ArcanistCpplintLinterTestCase' => 'lint/linter/__tests__/ArcanistCpplintLinterTestCase.php',
'ArcanistDiffChange' => 'parser/diff/ArcanistDiffChange.php',
'ArcanistDiffChangeType' => 'parser/diff/ArcanistDiffChangeType.php',
'ArcanistDiffHunk' => 'parser/diff/ArcanistDiffHunk.php',
'ArcanistDiffParser' => 'parser/ArcanistDiffParser.php',
'ArcanistDiffParserTestCase' => 'parser/__tests__/ArcanistDiffParserTestCase.php',
'ArcanistDiffUtils' => 'difference/ArcanistDiffUtils.php',
'ArcanistDiffUtilsTestCase' => 'difference/__tests__/ArcanistDiffUtilsTestCase.php',
'ArcanistDiffWorkflow' => 'workflow/ArcanistDiffWorkflow.php',
'ArcanistDifferentialCommitMessage' => 'differential/ArcanistDifferentialCommitMessage.php',
'ArcanistDifferentialCommitMessageParserException' => 'differential/ArcanistDifferentialCommitMessageParserException.php',
'ArcanistDifferentialDependencyGraph' => 'differential/ArcanistDifferentialDependencyGraph.php',
'ArcanistDifferentialRevisionHash' => 'differential/constants/ArcanistDifferentialRevisionHash.php',
'ArcanistDifferentialRevisionStatus' => 'differential/constants/ArcanistDifferentialRevisionStatus.php',
'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php',
'ArcanistEventType' => 'events/constant/ArcanistEventType.php',
'ArcanistExportWorkflow' => 'workflow/ArcanistExportWorkflow.php',
'ArcanistExternalLinter' => 'lint/linter/ArcanistExternalLinter.php',
'ArcanistFeatureWorkflow' => 'workflow/ArcanistFeatureWorkflow.php',
'ArcanistFilenameLinter' => 'lint/linter/ArcanistFilenameLinter.php',
'ArcanistFlagWorkflow' => 'workflow/ArcanistFlagWorkflow.php',
'ArcanistFlake8Linter' => 'lint/linter/ArcanistFlake8Linter.php',
'ArcanistFlake8LinterTestCase' => 'lint/linter/__tests__/ArcanistFlake8LinterTestCase.php',
'ArcanistFutureLinter' => 'lint/linter/ArcanistFutureLinter.php',
'ArcanistGeneratedLinter' => 'lint/linter/ArcanistGeneratedLinter.php',
'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php',
'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php',
'ArcanistGitHookPreReceiveWorkflow' => 'workflow/ArcanistGitHookPreReceiveWorkflow.php',
'ArcanistHelpWorkflow' => 'workflow/ArcanistHelpWorkflow.php',
'ArcanistHgClientChannel' => 'hgdaemon/ArcanistHgClientChannel.php',
'ArcanistHgProxyClient' => 'hgdaemon/ArcanistHgProxyClient.php',
'ArcanistHgProxyServer' => 'hgdaemon/ArcanistHgProxyServer.php',
'ArcanistHgServerChannel' => 'hgdaemon/ArcanistHgServerChannel.php',
'ArcanistHookAPI' => 'repository/hookapi/ArcanistHookAPI.php',
'ArcanistInlinesWorkflow' => 'workflow/ArcanistInlinesWorkflow.php',
'ArcanistInstallCertificateWorkflow' => 'workflow/ArcanistInstallCertificateWorkflow.php',
'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php',
'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php',
'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php',
'ArcanistLicenseLinter' => 'lint/linter/ArcanistLicenseLinter.php',
'ArcanistLintConsoleRenderer' => 'lint/renderer/ArcanistLintConsoleRenderer.php',
'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php',
'ArcanistLintJSONRenderer' => 'lint/renderer/ArcanistLintJSONRenderer.php',
'ArcanistLintLikeCompilerRenderer' => 'lint/renderer/ArcanistLintLikeCompilerRenderer.php',
'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php',
'ArcanistLintNoneRenderer' => 'lint/renderer/ArcanistLintNoneRenderer.php',
'ArcanistLintPatcher' => 'lint/ArcanistLintPatcher.php',
'ArcanistLintRenderer' => 'lint/renderer/ArcanistLintRenderer.php',
'ArcanistLintResult' => 'lint/ArcanistLintResult.php',
'ArcanistLintSeverity' => 'lint/ArcanistLintSeverity.php',
'ArcanistLintSummaryRenderer' => 'lint/renderer/ArcanistLintSummaryRenderer.php',
'ArcanistLintWorkflow' => 'workflow/ArcanistLintWorkflow.php',
'ArcanistLinter' => 'lint/linter/ArcanistLinter.php',
'ArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistLinterTestCase.php',
'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php',
'ArcanistMarkCommittedWorkflow' => 'workflow/ArcanistMarkCommittedWorkflow.php',
'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php',
'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php',
'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php',
'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php',
'ArcanistNoEffectException' => 'exception/usage/ArcanistNoEffectException.php',
'ArcanistNoEngineException' => 'exception/usage/ArcanistNoEngineException.php',
'ArcanistNoLintLinter' => 'lint/linter/ArcanistNoLintLinter.php',
'ArcanistNoLintTestCaseMisnamed' => 'lint/linter/__tests__/ArcanistNoLintTestCase.php',
'ArcanistPEP8Linter' => 'lint/linter/ArcanistPEP8Linter.php',
'ArcanistPEP8LinterTestCase' => 'lint/linter/__tests__/ArcanistPEP8LinterTestCase.php',
'ArcanistPHPCSLinterTestCase' => 'lint/linter/__tests__/ArcanistPHPCSLinterTestCase.php',
'ArcanistPasteWorkflow' => 'workflow/ArcanistPasteWorkflow.php',
'ArcanistPatchWorkflow' => 'workflow/ArcanistPatchWorkflow.php',
'ArcanistPhpcsLinter' => 'lint/linter/ArcanistPhpcsLinter.php',
'ArcanistPhutilLibraryLinter' => 'lint/linter/ArcanistPhutilLibraryLinter.php',
'ArcanistPhutilTestCase' => 'unit/engine/phutil/ArcanistPhutilTestCase.php',
'ArcanistPhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php',
'ArcanistPhutilTestSkippedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php',
'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestTerminatedException.php',
'ArcanistPhutilXHPASTLinter' => 'lint/linter/ArcanistPhutilXHPASTLinter.php',
'ArcanistPhutilXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistPhutilXHPASTLinterTestCase.php',
'ArcanistPyFlakesLinter' => 'lint/linter/ArcanistPyFlakesLinter.php',
'ArcanistPyLintLinter' => 'lint/linter/ArcanistPyLintLinter.php',
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php',
'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php',
'ArcanistRevertWorkflow' => 'workflow/ArcanistRevertWorkflow.php',
'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php',
'ArcanistRubyLinterTestCase' => 'lint/linter/__tests__/ArcanistRubyLinterTestCase.php',
'ArcanistScalaSBTLinter' => 'lint/linter/ArcanistScalaSBTLinter.php',
'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php',
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
'ArcanistSettings' => 'configuration/ArcanistSettings.php',
'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php',
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
'ArcanistSpellingDefaultData' => 'lint/linter/spelling/ArcanistSpellingDefaultData.php',
'ArcanistSpellingLinter' => 'lint/linter/ArcanistSpellingLinter.php',
'ArcanistSpellingLinterTestCase' => 'lint/linter/__tests__/ArcanistSpellingLinterTestCase.php',
'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php',
'ArcanistSubversionHookAPI' => 'repository/hookapi/ArcanistSubversionHookAPI.php',
'ArcanistSvnHookPreCommitWorkflow' => 'workflow/ArcanistSvnHookPreCommitWorkflow.php',
'ArcanistTasksWorkflow' => 'workflow/ArcanistTasksWorkflow.php',
'ArcanistTestCase' => 'infrastructure/testing/ArcanistTestCase.php',
'ArcanistTextLinter' => 'lint/linter/ArcanistTextLinter.php',
'ArcanistTextLinterTestCase' => 'lint/linter/__tests__/ArcanistTextLinterTestCase.php',
'ArcanistTodoWorkflow' => 'workflow/ArcanistTodoWorkflow.php',
'ArcanistUncommittedChangesException' => 'exception/usage/ArcanistUncommittedChangesException.php',
'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.php',
'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php',
'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php',
'ArcanistUnitWorkflow' => 'workflow/ArcanistUnitWorkflow.php',
'ArcanistUpgradeWorkflow' => 'workflow/ArcanistUpgradeWorkflow.php',
'ArcanistUploadWorkflow' => 'workflow/ArcanistUploadWorkflow.php',
'ArcanistUsageException' => 'exception/ArcanistUsageException.php',
'ArcanistUserAbortException' => 'exception/usage/ArcanistUserAbortException.php',
'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php',
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.php',
'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php',
'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php',
'ArcanistXHPASTLintSwitchHook' => 'lint/linter/xhpast/ArcanistXHPASTLintSwitchHook.php',
'ArcanistXHPASTLintTestSwitchHook' => 'lint/linter/__tests__/ArcanistXHPASTLintTestSwitchHook.php',
'ArcanistXHPASTLinter' => 'lint/linter/ArcanistXHPASTLinter.php',
'ArcanistXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php',
'ArcanistXUnitTestResultParser' => 'unit/engine/ArcanistXUnitTestResultParser.php',
'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php',
'ComprehensiveLintEngine' => 'lint/engine/ComprehensiveLintEngine.php',
'ExampleLintEngine' => 'lint/engine/ExampleLintEngine.php',
'GoTestResultParser' => 'unit/engine/GoTestResultParser.php',
'GoTestResultParserTestCase' => 'unit/engine/__tests__/GoTestResultParserTestCase.php',
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
'PHPUnitTestEngineTestCase' => 'unit/engine/__tests__/PHPUnitTestEngineTestCase.php',
'PhpunitResultParser' => 'unit/engine/PhpunitResultParser.php',
'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php',
'PhutilLintEngine' => 'lint/engine/PhutilLintEngine.php',
'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php',
'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php',
'PytestTestEngine' => 'unit/engine/PytestTestEngine.php',
'UnitTestableArcanistLintEngine' => 'lint/engine/UnitTestableArcanistLintEngine.php',
'XUnitTestEngine' => 'unit/engine/XUnitTestEngine.php',
'XUnitTestResultParserTestCase' => 'unit/engine/__tests__/XUnitTestResultParserTestCase.php',
),
'function' =>
array(
),
'xmap' =>
array(
'ArcanistAliasWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistAnoidWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistApacheLicenseLinter' => 'ArcanistLicenseLinter',
'ArcanistArcanistLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistBackoutWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistBaseCommitParserTestCase' => 'ArcanistTestCase',
'ArcanistBaseWorkflow' => 'Phobject',
'ArcanistBaseXHPASTLinter' => 'ArcanistFutureLinter',
'ArcanistBookmarkWorkflow' => 'ArcanistFeatureWorkflow',
'ArcanistBranchWorkflow' => 'ArcanistFeatureWorkflow',
'ArcanistBritishTestCase' => 'ArcanistTestCase',
'ArcanistBrowseWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistBundleTestCase' => 'ArcanistTestCase',
'ArcanistCSSLintLinter' => 'ArcanistExternalLinter',
'ArcanistCSSLintLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistCSharpLinter' => 'ArcanistFutureLinter',
'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCapabilityNotSupportedException' => 'Exception',
'ArcanistChooseInvalidRevisionException' => 'Exception',
'ArcanistChooseNoRevisionsException' => 'Exception',
'ArcanistCloseRevisionWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCloseWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCommentRemoverTestCase' => 'ArcanistTestCase',
'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistConduitLinter' => 'ArcanistLinter',
'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine',
'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCppcheckLinter' => 'ArcanistLinter',
'ArcanistCpplintLinter' => 'ArcanistLinter',
'ArcanistCpplintLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistDiffParserTestCase' => 'ArcanistTestCase',
'ArcanistDiffUtilsTestCase' => 'ArcanistTestCase',
'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDifferentialCommitMessageParserException' => 'Exception',
'ArcanistDifferentialDependencyGraph' => 'AbstractDirectedGraph',
'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistEventType' => 'PhutilEventType',
'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistExternalLinter' => 'ArcanistFutureLinter',
'ArcanistFeatureWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFilenameLinter' => 'ArcanistLinter',
'ArcanistFlagWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFlake8Linter' => 'ArcanistExternalLinter',
'ArcanistFlake8LinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistFutureLinter' => 'ArcanistLinter',
'ArcanistGeneratedLinter' => 'ArcanistLinter',
'ArcanistGetConfigWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
'ArcanistGitHookPreReceiveWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistHelpWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistHgClientChannel' => 'PhutilProtocolChannel',
'ArcanistHgServerChannel' => 'PhutilProtocolChannel',
'ArcanistInlinesWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistInstallCertificateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistJSHintLinter' => 'ArcanistLinter',
'ArcanistLandWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLiberateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLicenseLinter' => 'ArcanistLinter',
'ArcanistLintConsoleRenderer' => 'ArcanistLintRenderer',
'ArcanistLintJSONRenderer' => 'ArcanistLintRenderer',
'ArcanistLintLikeCompilerRenderer' => 'ArcanistLintRenderer',
'ArcanistLintNoneRenderer' => 'ArcanistLintRenderer',
'ArcanistLintSummaryRenderer' => 'ArcanistLintRenderer',
'ArcanistLintWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase',
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
'ArcanistMercurialParserTestCase' => 'ArcanistTestCase',
'ArcanistMergeConflictLinter' => 'ArcanistLinter',
'ArcanistNoEffectException' => 'ArcanistUsageException',
'ArcanistNoEngineException' => 'ArcanistUsageException',
'ArcanistNoLintLinter' => 'ArcanistLinter',
'ArcanistNoLintTestCaseMisnamed' => 'ArcanistLinterTestCase',
'ArcanistPEP8Linter' => 'ArcanistExternalLinter',
'ArcanistPEP8LinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistPHPCSLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistPasteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPhpcsLinter' => 'ArcanistExternalLinter',
'ArcanistPhutilLibraryLinter' => 'ArcanistLinter',
'ArcanistPhutilTestCaseTestCase' => 'ArcanistPhutilTestCase',
'ArcanistPhutilTestSkippedException' => 'Exception',
'ArcanistPhutilTestTerminatedException' => 'Exception',
'ArcanistPhutilXHPASTLinter' => 'ArcanistBaseXHPASTLinter',
'ArcanistPhutilXHPASTLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistPyFlakesLinter' => 'ArcanistLinter',
'ArcanistPyLintLinter' => 'ArcanistLinter',
'ArcanistRepositoryAPIMiscTestCase' => 'ArcanistTestCase',
'ArcanistRepositoryAPIStateTestCase' => 'ArcanistTestCase',
'ArcanistRevertWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistRubyLinter' => 'ArcanistExternalLinter',
'ArcanistRubyLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistScalaSBTLinter' => 'ArcanistLinter',
'ArcanistScriptAndRegexLinter' => 'ArcanistLinter',
'ArcanistSetConfigWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistShellCompleteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistSingleLintEngine' => 'ArcanistLintEngine',
'ArcanistSpellingLinter' => 'ArcanistLinter',
'ArcanistSpellingLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI',
'ArcanistSubversionHookAPI' => 'ArcanistHookAPI',
'ArcanistSvnHookPreCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistTasksWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistTestCase' => 'ArcanistPhutilTestCase',
'ArcanistTextLinter' => 'ArcanistLinter',
'ArcanistTextLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistTodoWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUncommittedChangesException' => 'ArcanistUsageException',
'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer',
'ArcanistUnitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUpgradeWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUploadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUsageException' => 'Exception',
'ArcanistUserAbortException' => 'ArcanistUsageException',
'ArcanistWhichWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistXHPASTLintNamingHookTestCase' => 'ArcanistTestCase',
'ArcanistXHPASTLintTestSwitchHook' => 'ArcanistXHPASTLintSwitchHook',
'ArcanistXHPASTLinter' => 'ArcanistBaseXHPASTLinter',
'ArcanistXHPASTLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'CSharpToolsTestEngine' => 'XUnitTestEngine',
'ComprehensiveLintEngine' => 'ArcanistLintEngine',
'ExampleLintEngine' => 'ArcanistLintEngine',
'GoTestResultParser' => 'ArcanistBaseTestResultParser',
'GoTestResultParserTestCase' => 'ArcanistTestCase',
'NoseTestEngine' => 'ArcanistBaseUnitTestEngine',
'PHPUnitTestEngineTestCase' => 'ArcanistTestCase',
'PhpunitResultParser' => 'ArcanistBaseTestResultParser',
'PhpunitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilLintEngine' => 'ArcanistLintEngine',
'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilUnitTestEngineTestCase' => 'ArcanistTestCase',
'PytestTestEngine' => 'ArcanistBaseUnitTestEngine',
'UnitTestableArcanistLintEngine' => 'ArcanistLintEngine',
'XUnitTestEngine' => 'ArcanistBaseUnitTestEngine',
'XUnitTestResultParserTestCase' => 'ArcanistTestCase',
),
));
diff --git a/src/configuration/ArcanistConfiguration.php b/src/configuration/ArcanistConfiguration.php
index fe4a79bb..373a9195 100644
--- a/src/configuration/ArcanistConfiguration.php
+++ b/src/configuration/ArcanistConfiguration.php
@@ -1,265 +1,265 @@
<?php
/**
* Runtime workflow configuration. In Arcanist, commands you type like
* "arc diff" or "arc lint" are called "workflows". This class allows you to add
* new workflows (and extend existing workflows) by subclassing it and then
* pointing to your subclass in your project configuration.
*
* When specified as the **arcanist_configuration** class in your project's
* ##.arcconfig##, your subclass will be instantiated (instead of this class)
* and be able to handle all the method calls. In particular, you can:
*
* - create, replace, or disable workflows by overriding buildWorkflow()
* and buildAllWorkflows();
* - add additional steps before or after workflows run by overriding
* willRunWorkflow() or didRunWorkflow() or didAbortWorkflow(); and
* - add new flags to existing workflows by overriding
* getCustomArgumentsForCommand().
*
* @group config
* @concrete-extensible
*/
class ArcanistConfiguration {
public function buildWorkflow($command) {
if ($command == '--help') {
// Special-case "arc --help" to behave like "arc help" instead of telling
// you to type "arc help" without being helpful.
$command = 'help';
}
return idx($this->buildAllWorkflows(), $command);
}
public function buildAllWorkflows() {
$workflows_by_name = array();
$workflows_by_class_name = id(new PhutilSymbolLoader())
->setAncestorClass('ArcanistBaseWorkflow')
->loadObjects();
foreach ($workflows_by_class_name as $class => $workflow) {
$name = $workflow->getWorkflowName();
if (isset($workflows_by_name[$name])) {
$other = get_class($workflows_by_name[$name]);
throw new Exception(
"Workflows {$class} and {$other} both implement workflows named ".
"{$name}.");
}
$workflows_by_name[$name] = $workflow;
}
return $workflows_by_name;
}
final public function isValidWorkflow($workflow) {
return (bool)$this->buildWorkflow($workflow);
}
public function willRunWorkflow($command, ArcanistBaseWorkflow $workflow) {
// This is a hook.
}
public function didRunWorkflow($command, ArcanistBaseWorkflow $workflow,
$err) {
// This is a hook.
}
public function didAbortWorkflow($command, $workflow, Exception $ex) {
// This is a hook.
}
public function getCustomArgumentsForCommand($command) {
return array();
}
final public function selectWorkflow(
&$command,
array &$args,
- ArcanistWorkingCopyIdentity $working_copy,
+ ArcanistConfigurationManager $configuration_manager,
PhutilConsole $console) {
// First, try to build a workflow with the exact name provided. We always
// pick an exact match, and do not allow aliases to override it.
$workflow = $this->buildWorkflow($command);
if ($workflow) {
return $workflow;
}
// If the user has an alias, like 'arc alias dhelp diff help', look it up
// and substitute it. We do this only after trying to resolve the workflow
// normally to prevent you from doing silly things like aliasing 'alias'
// to something else.
- $aliases = ArcanistAliasWorkflow::getAliases($working_copy);
+ $aliases = ArcanistAliasWorkflow::getAliases($configuration_manager);
list($new_command, $args) = ArcanistAliasWorkflow::resolveAliases(
$command,
$this,
$args,
- $working_copy);
+ $configuration_manager);
$full_alias = idx($aliases, $command, array());
$full_alias = implode(' ', $full_alias);
// Run shell command aliases.
if (ArcanistAliasWorkflow::isShellCommandAlias($new_command)) {
$shell_cmd = substr($full_alias, 1);
$console->writeLog(
"[alias: 'arc %s' -> $ %s]",
$command,
$shell_cmd);
if ($args) {
$err = phutil_passthru('%C %Ls', $shell_cmd, $args);
} else {
$err = phutil_passthru('%C', $shell_cmd);
}
exit($err);
}
// Run arc command aliases.
if ($new_command) {
$workflow = $this->buildWorkflow($new_command);
if ($workflow) {
$console->writeLog(
"[alias: 'arc %s' -> 'arc %s']\n",
$command,
$full_alias);
$command = $new_command;
return $workflow;
}
}
$all = array_keys($this->buildAllWorkflows());
// We haven't found a real command or an alias, so try to locate a command
// by unique prefix.
$prefixes = $this->expandCommandPrefix($command, $all);
if (count($prefixes) == 1) {
$command = head($prefixes);
return $this->buildWorkflow($command);
} else if (count($prefixes) > 1) {
$this->raiseUnknownCommand($command, $prefixes);
}
// We haven't found a real command, alias, or unique prefix. Try similar
// spellings.
$corrected = self::correctCommandSpelling($command, $all, 2);
if (count($corrected) == 1) {
$console->writeErr(
pht(
"(Assuming '%s' is the British spelling of '%s'.)",
$command,
head($corrected))."\n");
$command = head($corrected);
return $this->buildWorkflow($command);
} else if (count($corrected) > 1) {
$this->raiseUnknownCommand($command, $corrected);
}
$this->raiseUnknownCommand($command);
}
private function raiseUnknownCommand($command, array $maybe = array()) {
$message = pht("Unknown command '%s'. Try 'arc help'.", $command);
if ($maybe) {
$message .= "\n\n".pht("Did you mean:")."\n";
sort($maybe);
foreach ($maybe as $other) {
$message .= " ".$other."\n";
}
}
throw new ArcanistUsageException($message);
}
private function expandCommandPrefix($command, array $options) {
$is_prefix = array();
foreach ($options as $option) {
if (strncmp($option, $command, strlen($command)) == 0) {
$is_prefix[$option] = true;
}
}
return array_keys($is_prefix);
}
public static function correctCommandSpelling(
$command,
array $options,
$max_distance) {
// Adjust to the scaled edit costs we use below, so "2" roughly means
// "2 edits".
$max_distance = $max_distance * 3;
// These costs are somewhat made up, but the theory is that it is far more
// likely you will mis-strike a key ("lans" for "land") or press two keys
// out of order ("alnd" for "land") than omit keys or press extra keys.
$matrix = id(new PhutilEditDistanceMatrix())
->setInsertCost(4)
->setDeleteCost(4)
->setReplaceCost(3)
->setTransposeCost(2);
return self::correctSpelling($command, $options, $matrix, $max_distance);
}
public static function correctArgumentSpelling($command, array $options) {
$max_distance = 1;
// We are stricter with arguments - we allow only one inserted or deleted
// character. It is mainly to handle cases like --no-lint versus --nolint
// or --reviewer versus --reviewers.
$matrix = id(new PhutilEditDistanceMatrix())
->setInsertCost(1)
->setDeleteCost(1)
->setReplaceCost(10);
return self::correctSpelling($command, $options, $matrix, $max_distance);
}
public static function correctSpelling(
$input,
array $options,
PhutilEditDistanceMatrix $matrix,
$max_distance) {
$distances = array();
$inputv = str_split($input);
foreach ($options as $option) {
$optionv = str_split($option);
$matrix->setSequences($optionv, $inputv);
$distances[$option] = $matrix->getEditDistance();
}
asort($distances);
$best = min($max_distance, reset($distances));
foreach ($distances as $option => $distance) {
if ($distance > $best) {
unset($distances[$option]);
}
}
// Before filtering, check if we have multiple equidistant matches and
// return them if we do. This prevents us from, e.g., matching "alnd" with
// both "land" and "amend", then dropping "land" for being too short, and
// incorrectly completing to "amend".
if (count($distances) > 1) {
return array_keys($distances);
}
foreach ($distances as $option => $distance) {
if (strlen($option) < $distance) {
unset($distances[$option]);
}
}
return array_keys($distances);
}
}
diff --git a/src/configuration/ArcanistConfigurationManager.php b/src/configuration/ArcanistConfigurationManager.php
new file mode 100644
index 00000000..a9202de4
--- /dev/null
+++ b/src/configuration/ArcanistConfigurationManager.php
@@ -0,0 +1,248 @@
+<?php
+
+/**
+ * This class holds everything related to configuration and configuration files.
+ *
+ * @group config
+ */
+final class ArcanistConfigurationManager {
+
+ private $runtimeConfig = array();
+ private $workingCopy = null;
+
+ public function setWorkingCopyIdentity(
+ ArcanistWorkingCopyIdentity $working_copy) {
+ $this->workingCopy = $working_copy;
+ }
+
+/* -( 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';
+
+ public function getProjectConfig($key) {
+ if ($this->workingCopy) {
+ return $this->workingCopy->getConfig($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 configuratin 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 cannonical 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);
+ 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);
+ }
+
+ 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);
+ }
+
+ return false;
+ }
+
+
+ /**
+ * This is probably not the method you're looking for; try
+ * @{method:readUserArcConfig}.
+ */
+ public function readUserConfigurationFile() {
+ $user_config = array();
+ $user_config_path = self::getUserConfigurationFileLocation();
+ if (Filesystem::pathExists($user_config_path)) {
+
+ if (!phutil_is_windows()) {
+ $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);
+
+ // 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);
+ $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;
+ }
+
+ /**
+ * 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 = self::getUserConfigurationFileLocation();
+ Filesystem::writeFile($path, $json);
+
+ if (!phutil_is_windows()) {
+ execx('chmod 600 %s', $path);
+ }
+ }
+
+ public function getUserConfigurationFileLocation() {
+ if (phutil_is_windows()) {
+ return getenv('APPDATA').'/.arcrc';
+ } else {
+ return getenv('HOME').'/.arcrc';
+ }
+ }
+
+ public function readUserArcConfig() {
+ return idx(self::readUserConfigurationFile(), 'config', array());
+ }
+
+ public function writeUserArcConfig(array $options) {
+ $config = self::readUserConfigurationFile();
+ $config['config'] = $options;
+ self::writeUserConfigurationFile($config);
+ }
+
+
+ public function getSystemArcConfigLocation() {
+ if (phutil_is_windows()) {
+ return Filesystem::resolvePath(
+ 'Phabricator/Arcanist/config',
+ getenv('ProgramData'));
+ } else {
+ return '/etc/arcconfig';
+ }
+ }
+
+ public function readSystemArcConfig() {
+ $system_config = array();
+ $system_config_path = self::getSystemArcConfigLocation();
+ if (Filesystem::pathExists($system_config_path)) {
+ $file = Filesystem::readFile($system_config_path);
+ if ($file) {
+ $system_config = json_decode($file, true);
+ }
+ }
+ return $system_config;
+ }
+
+}
diff --git a/src/lint/linter/ArcanistScriptAndRegexLinter.php b/src/lint/linter/ArcanistScriptAndRegexLinter.php
index 662697e8..88ee7086 100644
--- a/src/lint/linter/ArcanistScriptAndRegexLinter.php
+++ b/src/lint/linter/ArcanistScriptAndRegexLinter.php
@@ -1,400 +1,398 @@
<?php
/**
* Simple glue linter which runs some script on each path, and then uses a
* regex to parse lint messages from the script's output. (This linter uses a
* script and a regex to interpret the results of some real linter, it does
* not itself lint both scripts and regexes).
*
* Configure this linter by setting these keys in your configuration:
*
* - `linter.scriptandregex.script` Script command to run. This can be
* the path to a linter script, but may also include flags or use shell
* features (see below for examples).
* - `linter.scriptandregex.regex` The regex to process output with. This
* regex uses named capturing groups (detailed below) to interpret output.
*
* The script will be invoked from the project root, so you can specify a
* relative path like `scripts/lint.sh` or an absolute path like
* `/opt/lint/lint.sh`.
*
* This linter is necessarily more limited in its capabilities than a normal
* linter which can perform custom processing, but may be somewhat simpler to
* configure.
*
* == Script... ==
*
* The script will be invoked once for each file that is to be linted, with
* the file passed as the first argument. The file may begin with a "-"; ensure
* your script will not interpret such files as flags (perhaps by ending your
* script configuration with "--", if its argument parser supports that).
*
* Note that when run via `arc diff`, the list of files to be linted includes
* deleted files and files that were moved away by the change. The linter should
* not assume the path it is given exists, and it is not an error for the
* linter to be invoked with paths which are no longer there. (Every affected
* path is subject to lint because some linters may raise errors in other files
* when a file is removed, or raise an error about its removal.)
*
* The script should emit lint messages to stdout, which will be parsed with
* the provided regex.
*
* For example, you might use a configuration like this:
*
* /opt/lint/lint.sh --flag value --other-flag --
*
* stderr is ignored. If you have a script which writes messages to stderr,
* you can redirect stderr to stdout by using a configuration like this:
*
* sh -c '/opt/lint/lint.sh "$0" 2>&1'
*
* The return code of the script must be 0, or an exception will be raised
* reporting that the linter failed. If you have a script which exits nonzero
* under normal circumstances, you can force it to always exit 0 by using a
* configuration like this:
*
* sh -c '/opt/lint/lint.sh "$0" || true'
*
* Multiple instances of the script will be run in parallel if there are
* multiple files to be linted, so they should not use any unique resources.
* For instance, this configuration would not work properly, because several
* processes may attempt to write to the file at the same time:
*
* COUNTEREXAMPLE
* sh -c '/opt/lint/lint.sh --output /tmp/lint.out "$0" && cat /tmp/lint.out'
*
* There are necessary limits to how gracefully this linter can deal with
* edge cases, because it is just a script and a regex. If you need to do
* things that this linter can't handle, you can write a phutil linter and move
* the logic to handle those cases into PHP. PHP is a better general-purpose
* programming language than regular expressions are, if only by a small margin.
*
* == ...and Regex ==
*
* The regex must be a valid PHP PCRE regex, including delimiters and flags.
*
* The regex will be matched against the entire output of the script, so it
* should generally be in this form if messages are one-per-line:
*
* /^...$/m
*
* The regex should capture these named patterns with `(?P<name>...)`:
*
* - `message` (required) Text describing the lint message. For example,
* "This is a syntax error.".
* - `name` (optional) Text summarizing the lint message. For example,
* "Syntax Error".
* - `severity` (optional) The word "error", "warning", "autofix", "advice",
* or "disabled", in any combination of upper and lower case. Instead, you
* may match groups called `error`, `warning`, `advice`, `autofix`, or
* `disabled`. These allow you to match output formats like "E123" and
* "W123" to indicate errors and warnings, even though the word "error" is
* not present in the output. If no severity capturing group is present,
* messages are raised with "error" severity. If multiple severity capturing
* groups are present, messages are raised with the highest captured
* serverity. Capturing groups like `error` supersede the `severity`
* capturing group.
* - `error` (optional) Match some nonempty substring to indicate that this
* message has "error" severity.
* - `warning` (optional) Match some nonempty substring to indicate that this
* message has "warning" severity.
* - `advice` (optional) Match some nonempty substring to indicate that this
* message has "advice" severity.
* - `autofix` (optional) Match some nonempty substring to indicate that this
* message has "autofix" severity.
* - `disabled` (optional) Match some nonempty substring to indicate that this
* message has "disabled" severity.
* - `file` (optional) The name of the file to raise the lint message in. If
* not specified, defaults to the linted file. It is generally not necessary
* to capture this unless the linter can raise messages in files other than
* the one it is linting.
* - `line` (optional) The line number of the message.
* - `char` (optional) The character offset of the message.
* - `offset` (optional) The byte offset of the message. If captured, this
* supersedes `line` and `char`.
* - `original` (optional) The text the message affects.
* - `replacement` (optional) The text that the range captured by `original`
* should be automatically replaced by to resolve the message.
* - `code` (optional) A short error type identifier which can be used
* elsewhere to configure handling of specific types of messages. For
* example, "EXAMPLE1", "EXAMPLE2", etc., where each code identifies a
* class of message like "syntax error", "missing whitespace", etc. This
* allows configuration to later change the severity of all whitespace
* messages, for example.
* - `ignore` (optional) Match some nonempty substring to ignore the match.
* You can use this if your linter sometimes emits text like "No lint
* errors".
* - `stop` (optional) Match some nonempty substring to stop processing input.
* Remaining matches for this file will be discarded, but linting will
* continue with other linters and other files.
* - `halt` (optional) Match some nonempty substring to halt all linting of
* this file by any linter. Linting will continue with other files.
* - `throw` (optional) Match some nonempty substring to throw an error, which
* will stop `arc` completely. You can use this to fail abruptly if you
* encounter unexpected output. All processing will abort.
*
* Numbered capturing groups are ignored.
*
* For example, if your lint script's output looks like this:
*
* error:13 Too many goats!
* warning:22 Not enough boats.
*
* ...you could use this regex to parse it:
*
* /^(?P<severity>warning|error):(?P<line>\d+) (?P<message>.*)$/m
*
* The simplest valid regex for line-oriented output is something like this:
*
* /^(?P<message>.*)$/m
*
* @task lint Linting
* @task linterinfo Linter Information
* @task parse Parsing Output
* @task config Validating Configuration
*
* @group linter
*/
final class ArcanistScriptAndRegexLinter extends ArcanistLinter {
private $output = array();
/* -( Linting )------------------------------------------------------------ */
/**
* Run the script on each file to be linted.
*
* @task lint
*/
public function willLintPaths(array $paths) {
$script = $this->getConfiguredScript();
$root = $this->getEngine()->getWorkingCopy()->getProjectRoot();
$futures = array();
foreach ($paths as $path) {
$future = new ExecFuture('%C %s', $script, $path);
$future->setCWD($root);
$futures[$path] = $future;
}
foreach (Futures($futures)->limit(4) as $path => $future) {
list($stdout) = $future->resolvex();
$this->output[$path] = $stdout;
}
}
/**
* Run the regex on the output of the script.
*
* @task lint
*/
public function lintPath($path) {
$regex = $this->getConfiguredRegex();
$output = idx($this->output, $path);
if (!strlen($output)) {
// No output, but it exited 0, so just move on.
return;
}
$matches = null;
if (!preg_match_all($regex, $output, $matches, PREG_SET_ORDER)) {
// Output with no matches. This might be a configuration error, but more
// likely it's something like "No lint errors." and the user just hasn't
// written a sufficiently powerful/ridiculous regexp to capture it into an
// 'ignore' group. Don't make them figure this out; advanced users can
// capture 'throw' to handle this case.
return;
}
foreach ($matches as $match) {
if (!empty($match['throw'])) {
$throw = $match['throw'];
throw new ArcanistUsageException(
"ArcanistScriptAndRegexLinter: ".
"configuration captured a 'throw' named capturing group, ".
"'{$throw}'. Script output:\n".
$output);
}
if (!empty($match['halt'])) {
$this->stopAllLinters();
break;
}
if (!empty($match['stop'])) {
break;
}
if (!empty($match['ignore'])) {
continue;
}
list($line, $char) = $this->getMatchLineAndChar($match, $path);
$dict = array(
'path' => idx($match, 'file', $path),
'line' => $line,
'char' => $char,
'code' => idx($match, 'code', $this->getLinterName()),
'severity' => $this->getMatchSeverity($match),
'name' => idx($match, 'name', 'Lint'),
'description' => idx($match, 'message', 'Undefined Lint Message'),
);
$original = idx($match, 'original');
if ($original !== null) {
$dict['original'] = $original;
}
$replacement = idx($match, 'replacement');
if ($replacement !== null) {
$dict['replacement'] = $replacement;
}
$lint = ArcanistLintMessage::newFromDictionary($dict);
$this->addLintMessage($lint);
}
}
/* -( Linter Information )------------------------------------------------- */
/**
* Return the short name of the linter.
*
* @return string Short linter identifier.
*
* @task linterinfo
*/
public function getLinterName() {
return 'S&RX';
}
/* -( Parsing Output )----------------------------------------------------- */
/**
* Get the line and character of the message from the regex match.
*
* @param dict Captured groups from regex.
* @return pair<int,int> Line and character of the message.
*
* @task parse
*/
private function getMatchLineAndChar(array $match, $path) {
if (!empty($match['offset'])) {
list($line, $char) = $this->getEngine()->getLineAndCharFromOffset(
idx($match, 'file', $path),
$match['offset']);
return array($line + 1, $char + 1);
}
$line = idx($match, 'line', 1);
$char = idx($match, 'char');
return array($line, $char);
}
/**
* Map the regex matching groups to a message severity. We look for either
* a nonempty severity name group like 'error', or a group called 'severity'
* with a valid name.
*
* @param dict Captured groups from regex.
* @return const @{class:ArcanistLintSeverity} constant.
*
* @task parse
*/
private function getMatchSeverity(array $match) {
$map = array(
'error' => ArcanistLintSeverity::SEVERITY_ERROR,
'warning' => ArcanistLintSeverity::SEVERITY_WARNING,
'autofix' => ArcanistLintSeverity::SEVERITY_AUTOFIX,
'advice' => ArcanistLintSeverity::SEVERITY_ADVICE,
'disabled' => ArcanistLintSeverity::SEVERITY_DISABLED,
);
$severity_name = strtolower(idx($match, 'severity'));
foreach ($map as $name => $severity) {
if (!empty($match[$name])) {
return $severity;
}
if ($severity_name == $name) {
return $severity;
}
}
return ArcanistLintSeverity::SEVERITY_ERROR;
}
/* -( Validating Configuration )------------------------------------------- */
/**
* Load, validate, and return the "script" configuration.
*
* @return string The shell command fragment to use to run the linter.
*
* @task config
*/
private function getConfiguredScript() {
- $working_copy = $this->getEngine()->getWorkingCopy();
$key = 'linter.scriptandregex.script';
- $config = $working_copy->getConfigFromAnySource($key);
+ $config = $this->getConfigFromAnySource($key);
if (!$config) {
throw new ArcanistUsageException(
"ArcanistScriptAndRegexLinter: ".
"You must configure '{$key}' to point to a script to execute.");
}
// NOTE: No additional validation since the "script" can be some random
// shell command and/or include flags, so it does not need to point to some
// file on disk.
return $config;
}
/**
* Load, validate, and return the "regex" configuration.
*
* @return string A valid PHP PCRE regular expression.
*
* @task config
*/
private function getConfiguredRegex() {
- $working_copy = $this->getEngine()->getWorkingCopy();
$key = 'linter.scriptandregex.regex';
- $config = $working_copy->getConfigFromAnySource($key);
+ $config = $this->getConfigFromAnySource($key);
if (!$config) {
throw new ArcanistUsageException(
"ArcanistScriptAndRegexLinter: ".
"You must configure '{$key}' with a valid PHP PCRE regex.");
}
// NOTE: preg_match() returns 0 for no matches and false for compile error;
// this won't match, but will validate the syntax of the regex.
$ok = preg_match($config, 'syntax-check');
if ($ok === false) {
throw new ArcanistUsageException(
"ArcanistScriptAndRegexLinter: ".
"Regex '{$config}' does not compile. You must configure '{$key}' with ".
"a valid PHP PCRE regex, including delimiters.");
}
return $config;
}
}
diff --git a/src/lint/linter/ArcanistXHPASTLinter.php b/src/lint/linter/ArcanistXHPASTLinter.php
index 896d60fd..9735e249 100644
--- a/src/lint/linter/ArcanistXHPASTLinter.php
+++ b/src/lint/linter/ArcanistXHPASTLinter.php
@@ -1,2341 +1,2341 @@
<?php
/**
* Uses XHPAST to apply lint rules to PHP.
*
* @group linter
*/
final class ArcanistXHPASTLinter extends ArcanistBaseXHPASTLinter {
private $futures = array();
private $trees = array();
const LINT_PHP_SYNTAX_ERROR = 1;
const LINT_UNABLE_TO_PARSE = 2;
const LINT_VARIABLE_VARIABLE = 3;
const LINT_EXTRACT_USE = 4;
const LINT_UNDECLARED_VARIABLE = 5;
const LINT_PHP_SHORT_TAG = 6;
const LINT_PHP_ECHO_TAG = 7;
const LINT_PHP_CLOSE_TAG = 8;
const LINT_NAMING_CONVENTIONS = 9;
const LINT_IMPLICIT_CONSTRUCTOR = 10;
const LINT_DYNAMIC_DEFINE = 12;
const LINT_STATIC_THIS = 13;
const LINT_PREG_QUOTE_MISUSE = 14;
const LINT_PHP_OPEN_TAG = 15;
const LINT_TODO_COMMENT = 16;
const LINT_EXIT_EXPRESSION = 17;
const LINT_COMMENT_STYLE = 18;
const LINT_CLASS_FILENAME_MISMATCH = 19;
const LINT_TAUTOLOGICAL_EXPRESSION = 20;
const LINT_PLUS_OPERATOR_ON_STRINGS = 21;
const LINT_DUPLICATE_KEYS_IN_ARRAY = 22;
const LINT_REUSED_ITERATORS = 23;
const LINT_BRACE_FORMATTING = 24;
const LINT_PARENTHESES_SPACING = 25;
const LINT_CONTROL_STATEMENT_SPACING = 26;
const LINT_BINARY_EXPRESSION_SPACING = 27;
const LINT_ARRAY_INDEX_SPACING = 28;
const LINT_RAGGED_CLASSTREE_EDGE = 29;
const LINT_IMPLICIT_FALLTHROUGH = 30;
const LINT_PHP_53_FEATURES = 31;
const LINT_REUSED_AS_ITERATOR = 32;
const LINT_COMMENT_SPACING = 34;
const LINT_PHP_54_FEATURES = 35;
const LINT_SLOWNESS = 36;
const LINT_CLOSING_CALL_PAREN = 37;
const LINT_CLOSING_DECL_PAREN = 38;
const LINT_REUSED_ITERATOR_REFERENCE = 39;
const LINT_KEYWORD_CASING = 40;
public function getLintNameMap() {
return array(
self::LINT_PHP_SYNTAX_ERROR => 'PHP Syntax Error!',
self::LINT_UNABLE_TO_PARSE => 'Unable to Parse',
self::LINT_VARIABLE_VARIABLE => 'Use of Variable Variable',
self::LINT_EXTRACT_USE => 'Use of extract()',
self::LINT_UNDECLARED_VARIABLE => 'Use of Undeclared Variable',
self::LINT_PHP_SHORT_TAG => 'Use of Short Tag "<?"',
self::LINT_PHP_ECHO_TAG => 'Use of Echo Tag "<?="',
self::LINT_PHP_CLOSE_TAG => 'Use of Close Tag "?>"',
self::LINT_NAMING_CONVENTIONS => 'Naming Conventions',
self::LINT_IMPLICIT_CONSTRUCTOR => 'Implicit Constructor',
self::LINT_DYNAMIC_DEFINE => 'Dynamic define()',
self::LINT_STATIC_THIS => 'Use of $this in Static Context',
self::LINT_PREG_QUOTE_MISUSE => 'Misuse of preg_quote()',
self::LINT_PHP_OPEN_TAG => 'Expected Open Tag',
self::LINT_TODO_COMMENT => 'TODO Comment',
self::LINT_EXIT_EXPRESSION => 'Exit Used as Expression',
self::LINT_COMMENT_STYLE => 'Comment Style',
self::LINT_CLASS_FILENAME_MISMATCH => 'Class-Filename Mismatch',
self::LINT_TAUTOLOGICAL_EXPRESSION => 'Tautological Expression',
self::LINT_PLUS_OPERATOR_ON_STRINGS => 'Not String Concatenation',
self::LINT_DUPLICATE_KEYS_IN_ARRAY => 'Duplicate Keys in Array',
self::LINT_REUSED_ITERATORS => 'Reuse of Iterator Variable',
self::LINT_BRACE_FORMATTING => 'Brace placement',
self::LINT_PARENTHESES_SPACING => 'Spaces Inside Parentheses',
self::LINT_CONTROL_STATEMENT_SPACING => 'Space After Control Statement',
self::LINT_BINARY_EXPRESSION_SPACING => 'Space Around Binary Operator',
self::LINT_ARRAY_INDEX_SPACING => 'Spacing Before Array Index',
self::LINT_RAGGED_CLASSTREE_EDGE => 'Class Not abstract Or final',
self::LINT_IMPLICIT_FALLTHROUGH => 'Implicit Fallthrough',
self::LINT_PHP_53_FEATURES => 'Use Of PHP 5.3 Features',
self::LINT_PHP_54_FEATURES => 'Use Of PHP 5.4 Features',
self::LINT_REUSED_AS_ITERATOR => 'Variable Reused As Iterator',
self::LINT_COMMENT_SPACING => 'Comment Spaces',
self::LINT_SLOWNESS => 'Slow Construct',
self::LINT_CLOSING_CALL_PAREN => 'Call Formatting',
self::LINT_CLOSING_DECL_PAREN => 'Declaration Formatting',
self::LINT_REUSED_ITERATOR_REFERENCE => 'Reuse of Iterator References',
self::LINT_KEYWORD_CASING => 'Keyword Conventions',
);
}
public function getLinterName() {
return 'XHP';
}
public function getLintSeverityMap() {
$disabled = ArcanistLintSeverity::SEVERITY_DISABLED;
$advice = ArcanistLintSeverity::SEVERITY_ADVICE;
$warning = ArcanistLintSeverity::SEVERITY_WARNING;
return array(
self::LINT_TODO_COMMENT => $disabled,
self::LINT_UNABLE_TO_PARSE => $warning,
self::LINT_NAMING_CONVENTIONS => $warning,
self::LINT_PREG_QUOTE_MISUSE => $advice,
self::LINT_BRACE_FORMATTING => $warning,
self::LINT_PARENTHESES_SPACING => $warning,
self::LINT_CONTROL_STATEMENT_SPACING => $warning,
self::LINT_BINARY_EXPRESSION_SPACING => $warning,
self::LINT_ARRAY_INDEX_SPACING => $warning,
self::LINT_IMPLICIT_FALLTHROUGH => $warning,
self::LINT_SLOWNESS => $warning,
self::LINT_COMMENT_SPACING => $advice,
self::LINT_CLOSING_CALL_PAREN => $warning,
self::LINT_CLOSING_DECL_PAREN => $warning,
self::LINT_REUSED_ITERATOR_REFERENCE => $warning,
self::LINT_KEYWORD_CASING => $warning,
// This is disabled by default because it implies a very strict policy
// which isn't necessary in the general case.
self::LINT_RAGGED_CLASSTREE_EDGE => $disabled,
// This is disabled by default because projects don't necessarily target
// a specific minimum version.
self::LINT_PHP_53_FEATURES => $disabled,
self::LINT_PHP_54_FEATURES => $disabled,
);
}
protected function buildFutures(array $paths) {
foreach ($paths as $path) {
if (!isset($this->futures[$path])) {
$this->futures[$path] = xhpast_get_parser_future($this->getData($path));
}
}
return array_select_keys($this->futures, $paths);
}
public function getXHPASTTreeForPath($path) {
if (!array_key_exists($path, $this->trees)) {
$this->trees[$path] = null;
try {
$this->trees[$path] = XHPASTTree::newFromDataAndResolvedExecFuture(
$this->getData($path),
$this->futures[$path]->resolve());
$root = $this->trees[$path]->getRootNode();
$root->buildSelectCache();
$root->buildTokenCache();
} catch (XHPASTSyntaxErrorException $ex) {
$this->raiseLintAtLine(
$ex->getErrorLine(),
1,
self::LINT_PHP_SYNTAX_ERROR,
'This file contains a syntax error: '.$ex->getMessage());
} catch (Exception $ex) {
$this->raiseLintAtPath(self::LINT_UNABLE_TO_PARSE, $ex->getMessage());
}
}
return $this->trees[$path];
}
public function getCacheVersion() {
$version = '4';
$path = xhpast_get_binary_path();
if (Filesystem::pathExists($path)) {
$version .= '-'.md5_file($path);
}
return $version;
}
protected function resolveFuture($path, Future $future) {
$tree = $this->getXHPASTTreeForPath($path);
if (!$tree) {
return;
}
$root = $tree->getRootNode();
$method_codes = array(
'lintStrstrUsedForCheck' => self::LINT_SLOWNESS,
'lintStrposUsedForStart' => self::LINT_SLOWNESS,
'lintPHP53Features' => self::LINT_PHP_53_FEATURES,
'lintPHP54Features' => self::LINT_PHP_54_FEATURES,
'lintImplicitFallthrough' => self::LINT_IMPLICIT_FALLTHROUGH,
'lintBraceFormatting' => self::LINT_BRACE_FORMATTING,
'lintTautologicalExpressions' => self::LINT_TAUTOLOGICAL_EXPRESSION,
'lintCommentSpaces' => self::LINT_COMMENT_SPACING,
'lintHashComments' => self::LINT_COMMENT_STYLE,
'lintReusedIterators' => self::LINT_REUSED_ITERATORS,
'lintReusedIteratorReferences' => self::LINT_REUSED_ITERATOR_REFERENCE,
'lintVariableVariables' => self::LINT_VARIABLE_VARIABLE,
'lintUndeclaredVariables' => array(
self::LINT_EXTRACT_USE,
self::LINT_REUSED_AS_ITERATOR,
self::LINT_UNDECLARED_VARIABLE,
),
'lintPHPTagUse' => array(
self::LINT_PHP_SHORT_TAG,
self::LINT_PHP_ECHO_TAG,
self::LINT_PHP_OPEN_TAG,
self::LINT_PHP_CLOSE_TAG,
),
'lintNamingConventions' => self::LINT_NAMING_CONVENTIONS,
'lintSurpriseConstructors' => self::LINT_IMPLICIT_CONSTRUCTOR,
'lintParenthesesShouldHugExpressions' => self::LINT_PARENTHESES_SPACING,
'lintSpaceAfterControlStatementKeywords' =>
self::LINT_CONTROL_STATEMENT_SPACING,
'lintSpaceAroundBinaryOperators' => self::LINT_BINARY_EXPRESSION_SPACING,
'lintDynamicDefines' => self::LINT_DYNAMIC_DEFINE,
'lintUseOfThisInStaticMethods' => self::LINT_STATIC_THIS,
'lintPregQuote' => self::LINT_PREG_QUOTE_MISUSE,
'lintExitExpressions' => self::LINT_EXIT_EXPRESSION,
'lintArrayIndexWhitespace' => self::LINT_ARRAY_INDEX_SPACING,
'lintTODOComments' => self::LINT_TODO_COMMENT,
'lintPrimaryDeclarationFilenameMatch' =>
self::LINT_CLASS_FILENAME_MISMATCH,
'lintPlusOperatorOnStrings' => self::LINT_PLUS_OPERATOR_ON_STRINGS,
'lintDuplicateKeysInArray' => self::LINT_DUPLICATE_KEYS_IN_ARRAY,
'lintRaggedClasstreeEdges' => self::LINT_RAGGED_CLASSTREE_EDGE,
'lintClosingCallParen' => self::LINT_CLOSING_CALL_PAREN,
'lintClosingDeclarationParen' => self::LINT_CLOSING_DECL_PAREN,
'lintKeywordCasing' => self::LINT_KEYWORD_CASING,
);
foreach ($method_codes as $method => $codes) {
foreach ((array)$codes as $code) {
if ($this->isCodeEnabled($code)) {
call_user_func(array($this, $method), $root);
break;
}
}
}
}
public function lintStrstrUsedForCheck($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildOfType(1, 'n_OPERATOR');
$operator = $operator->getConcreteString();
if ($operator != '===' && $operator != '!==') {
continue;
}
$false = $expression->getChildByIndex(0);
if ($false->getTypeName() == 'n_SYMBOL_NAME' &&
$false->getConcreteString() == 'false') {
$strstr = $expression->getChildByIndex(2);
} else {
$strstr = $false;
$false = $expression->getChildByIndex(2);
if ($false->getTypeName() != 'n_SYMBOL_NAME' ||
$false->getConcreteString() != 'false') {
continue;
}
}
if ($strstr->getTypeName() != 'n_FUNCTION_CALL') {
continue;
}
$name = strtolower($strstr->getChildByIndex(0)->getConcreteString());
if ($name == 'strstr' || $name == 'strchr') {
$this->raiseLintAtNode(
$strstr,
self::LINT_SLOWNESS,
"Use strpos() for checking if the string contains something.");
} else if ($name == 'stristr') {
$this->raiseLintAtNode(
$strstr,
self::LINT_SLOWNESS,
"Use stripos() for checking if the string contains something.");
}
}
}
public function lintStrposUsedForStart($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildOfType(1, 'n_OPERATOR');
$operator = $operator->getConcreteString();
if ($operator != '===' && $operator != '!==') {
continue;
}
$zero = $expression->getChildByIndex(0);
if ($zero->getTypeName() == 'n_NUMERIC_SCALAR' &&
$zero->getConcreteString() == '0') {
$strpos = $expression->getChildByIndex(2);
} else {
$strpos = $zero;
$zero = $expression->getChildByIndex(2);
if ($zero->getTypeName() != 'n_NUMERIC_SCALAR' ||
$zero->getConcreteString() != '0') {
continue;
}
}
if ($strpos->getTypeName() != 'n_FUNCTION_CALL') {
continue;
}
$name = strtolower($strpos->getChildByIndex(0)->getConcreteString());
if ($name == 'strpos') {
$this->raiseLintAtNode(
$strpos,
self::LINT_SLOWNESS,
"Use strncmp() for checking if the string starts with something.");
} else if ($name == 'stripos') {
$this->raiseLintAtNode(
$strpos,
self::LINT_SLOWNESS,
"Use strncasecmp() for checking if the string starts with ".
"something.");
}
}
}
public function lintPHP53Features($root) {
$functions = $root->selectTokensOfType('T_FUNCTION');
foreach ($functions as $function) {
$next = $function->getNextToken();
while ($next) {
if ($next->isSemantic()) {
break;
}
$next = $next->getNextToken();
}
if ($next) {
if ($next->getTypeName() == '(') {
$this->raiseLintAtToken(
$function,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but anonymous functions were '.
'not introduced until PHP 5.3.');
}
}
}
$namespaces = $root->selectTokensOfType('T_NAMESPACE');
foreach ($namespaces as $namespace) {
$this->raiseLintAtToken(
$namespace,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but namespaces were not introduced '.
'until PHP 5.3.');
}
// NOTE: This is only "use x;", in anonymous functions the node type is
// n_LEXICAL_VARIABLE_LIST even though both tokens are T_USE.
// TODO: We parse n_USE in a slightly crazy way right now; that would be
// a better selector once it's fixed.
$uses = $root->selectDescendantsOfType('n_USE_LIST');
foreach ($uses as $use) {
$this->raiseLintAtNode(
$use,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but namespaces were not introduced '.
'until PHP 5.3.');
}
$statics = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($statics as $static) {
$name = $static->getChildByIndex(0);
if ($name->getTypeName() != 'n_CLASS_NAME') {
continue;
}
if ($name->getConcreteString() == 'static') {
$this->raiseLintAtNode(
$name,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but `static::` was not introduced '.
'until PHP 5.3.');
}
}
$ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION');
foreach ($ternaries as $ternary) {
$yes = $ternary->getChildByIndex(1);
if ($yes->getTypeName() == 'n_EMPTY') {
$this->raiseLintAtNode(
$ternary,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but short ternary was not '.
'introduced until PHP 5.3.');
}
}
$heredocs = $root->selectDescendantsOfType('n_HEREDOC');
foreach ($heredocs as $heredoc) {
if (preg_match('/^<<<[\'"]/', $heredoc->getConcreteString())) {
$this->raiseLintAtNode(
$heredoc,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but nowdoc was not introduced until '.
'PHP 5.3.');
}
}
$this->lintPHP53Functions($root);
}
private function lintPHP53Functions($root) {
$target = phutil_get_library_root('arcanist').
'/../resources/php_compat_info.json';
$compat_info = json_decode(file_get_contents($target), true);
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$node = $call->getChildByIndex(0);
$name = strtolower($node->getConcreteString());
$version = idx($compat_info['functions'], $name);
$windows = idx($compat_info['functions_windows'], $name);
if ($version) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but `{$name}()` was not ".
"introduced until PHP {$version}.");
} else if (array_key_exists($name, $compat_info['params'])) {
$params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
foreach (array_values($params->getChildren()) as $i => $param) {
$version = idx($compat_info['params'][$name], $i);
if ($version) {
$this->raiseLintAtNode(
$param,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but parameter ".($i + 1)." ".
"of `{$name}()` was not introduced until PHP {$version}.");
}
}
} else if ($windows === '' || version_compare($windows, '5.3.0') > 0) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.3.0 on Windows, but `{$name}()` is not ".
"available there".
($windows ? " until PHP {$windows}" : "").".");
}
}
$classes = $root->selectDescendantsOfType('n_CLASS_NAME');
foreach ($classes as $node) {
$name = strtolower($node->getConcreteString());
$version = idx($compat_info['interfaces'], $name);
$version = idx($compat_info['classes'], $name, $version);
if ($version) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but `{$name}` was not ".
"introduced until PHP {$version}.");
}
}
}
public function lintPHP54Features($root) {
$indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS');
foreach ($indexes as $index) {
$left = $index->getChildByIndex(0);
switch ($left->getTypeName()) {
case 'n_FUNCTION_CALL':
case 'n_METHOD_CALL':
$this->raiseLintAtNode(
$index->getChildByIndex(1),
self::LINT_PHP_54_FEATURES,
"The f()[...] syntax was not introduced until PHP 5.4, but this ".
"codebase targets an earlier version of PHP. You can rewrite ".
"this expression using idx().");
break;
}
}
}
private function lintImplicitFallthrough($root) {
$hook_obj = null;
$working_copy = $this->getEngine()->getWorkingCopy();
if ($working_copy) {
$hook_class = $working_copy->getConfig('lint.xhpast.switchhook');
$hook_class = $this->getConfig('switchhook', $hook_class);
if ($hook_class) {
$hook_obj = newv($hook_class, array());
assert_instances_of(array($hook_obj), 'ArcanistXHPASTLintSwitchHook');
}
}
$switches = $root->selectDescendantsOfType('n_SWITCH');
foreach ($switches as $switch) {
$blocks = array();
$cases = $switch->selectDescendantsOfType('n_CASE');
foreach ($cases as $case) {
$blocks[] = $case;
}
$defaults = $switch->selectDescendantsOfType('n_DEFAULT');
foreach ($defaults as $default) {
$blocks[] = $default;
}
foreach ($blocks as $key => $block) {
// Collect all the tokens in this block which aren't at top level.
// We want to ignore "break", and "continue" in these blocks.
$lower_level = $block->selectDescendantsOfType('n_WHILE');
$lower_level->add($block->selectDescendantsOfType('n_DO_WHILE'));
$lower_level->add($block->selectDescendantsOfType('n_FOR'));
$lower_level->add($block->selectDescendantsOfType('n_FOREACH'));
$lower_level->add($block->selectDescendantsOfType('n_SWITCH'));
$lower_level_tokens = array();
foreach ($lower_level as $lower_level_block) {
$lower_level_tokens += $lower_level_block->getTokens();
}
// Collect all the tokens in this block which aren't in this scope
// (because they're inside class, function or interface declarations).
// We want to ignore all of these tokens.
$decls = $block->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$decls->add($block->selectDescendantsOfType('n_CLASS_DECLARATION'));
// For completeness; these can't actually have anything.
$decls->add($block->selectDescendantsOfType('n_INTERFACE_DECLARATION'));
$different_scope_tokens = array();
foreach ($decls as $decl) {
$different_scope_tokens += $decl->getTokens();
}
$lower_level_tokens += $different_scope_tokens;
// Get all the trailing nonsemantic tokens, since we need to look for
// "fallthrough" comments past the end of the semantic block.
$tokens = $block->getTokens();
$last = end($tokens);
while ($last && $last = $last->getNextToken()) {
if ($last->isSemantic()) {
break;
}
$tokens[$last->getTokenID()] = $last;
}
$blocks[$key] = array(
$tokens,
$lower_level_tokens,
$different_scope_tokens,
);
}
foreach ($blocks as $token_lists) {
list(
$tokens,
$lower_level_tokens,
$different_scope_tokens) = $token_lists;
// Test each block (case or default statement) to see if it's OK. It's
// OK if:
//
// - it is empty; or
// - it ends in break, return, throw, continue or exit at top level; or
// - it has a comment with "fallthrough" in its text.
// Empty blocks are OK, so we start this at `true` and only set it to
// false if we find a statement.
$block_ok = true;
// Keeps track of whether the current statement is one that validates
// the block (break, return, throw, continue) or something else.
$statement_ok = false;
foreach ($tokens as $token_id => $token) {
if (!$token->isSemantic()) {
// Liberally match "fall" in the comment text so that comments like
// "fallthru", "fall through", "fallthrough", etc., are accepted.
if (preg_match('/fall/i', $token->getValue())) {
$block_ok = true;
break;
}
continue;
}
$tok_type = $token->getTypeName();
if ($tok_type == 'T_FUNCTION' ||
$tok_type == 'T_CLASS' ||
$tok_type == 'T_INTERFACE') {
// These aren't statements, but mark the block as nonempty anyway.
$block_ok = false;
continue;
}
if ($tok_type == ';') {
if ($statement_ok) {
$statment_ok = false;
} else {
$block_ok = false;
}
continue;
}
if ($tok_type == 'T_BREAK' ||
$tok_type == 'T_CONTINUE') {
if (empty($lower_level_tokens[$token_id])) {
$statement_ok = true;
$block_ok = true;
}
continue;
}
if ($tok_type == 'T_RETURN' ||
$tok_type == 'T_THROW' ||
$tok_type == 'T_EXIT' ||
($hook_obj && $hook_obj->checkSwitchToken($token))) {
if (empty($different_scope_tokens[$token_id])) {
$statement_ok = true;
$block_ok = true;
}
continue;
}
}
if (!$block_ok) {
$this->raiseLintAtToken(
head($tokens),
self::LINT_IMPLICIT_FALLTHROUGH,
"This 'case' or 'default' has a nonempty block which does not ".
"end with 'break', 'continue', 'return', 'throw' or 'exit'. Did ".
"you forget to add one of those? If you intend to fall through, ".
"add a '// fallthrough' comment to silence this warning.");
}
}
}
}
private function lintBraceFormatting($root) {
foreach ($root->selectDescendantsOfType('n_STATEMENT_LIST') as $list) {
$tokens = $list->getTokens();
if (!$tokens || head($tokens)->getValue() != '{') {
continue;
}
list($before, $after) = $list->getSurroundingNonsemanticTokens();
if (!$before) {
$first = head($tokens);
// Only insert the space if we're after a closing parenthesis. If
// we're in a construct like "else{}", other rules will insert space
// after the 'else' correctly.
$prev = $first->getPrevToken();
if (!$prev || $prev->getValue() != ')') {
continue;
}
$this->raiseLintAtToken(
$first,
self::LINT_BRACE_FORMATTING,
'Put opening braces on the same line as control statements and '.
'declarations, with a single space before them.',
' '.$first->getValue());
} else if (count($before) == 1) {
$before = reset($before);
if ($before->getValue() != ' ') {
$this->raiseLintAtToken(
$before,
self::LINT_BRACE_FORMATTING,
'Put opening braces on the same line as control statements and '.
'declarations, with a single space before them.',
' ');
}
}
}
}
private function lintTautologicalExpressions($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
static $operators = array(
'-' => true,
'/' => true,
'-=' => true,
'/=' => true,
'<=' => true,
'<' => true,
'==' => true,
'===' => true,
'!=' => true,
'!==' => true,
'>=' => true,
'>' => true,
);
static $logical = array(
'||' => true,
'&&' => true,
);
foreach ($expressions as $expr) {
$operator = $expr->getChildByIndex(1)->getConcreteString();
if (!empty($operators[$operator])) {
$left = $expr->getChildByIndex(0)->getSemanticString();
$right = $expr->getChildByIndex(2)->getSemanticString();
if ($left == $right) {
$this->raiseLintAtNode(
$expr,
self::LINT_TAUTOLOGICAL_EXPRESSION,
'Both sides of this expression are identical, so it always '.
'evaluates to a constant.');
}
}
if (!empty($logical[$operator])) {
$left = $expr->getChildByIndex(0)->getSemanticString();
$right = $expr->getChildByIndex(2)->getSemanticString();
// NOTE: These will be null to indicate "could not evaluate".
$left = $this->evaluateStaticBoolean($left);
$right = $this->evaluateStaticBoolean($right);
if (($operator == '||' && ($left === true || $right === true)) ||
($operator == '&&' && ($left === false || $right === false))) {
$this->raiseLintAtNode(
$expr,
self::LINT_TAUTOLOGICAL_EXPRESSION,
'The logical value of this expression is static. Did you forget '.
'to remove some debugging code?');
}
}
}
}
/**
* Statically evaluate a boolean value from an XHP tree.
*
* TODO: Improve this and move it to XHPAST proper?
*
* @param string The "semantic string" of a single value.
* @return mixed ##true## or ##false## if the value could be evaluated
* statically; ##null## if static evaluation was not possible.
*/
private function evaluateStaticBoolean($string) {
switch (strtolower($string)) {
case '0':
case 'null':
case 'false':
return false;
case '1':
case 'true':
return true;
}
return null;
}
protected function lintCommentSpaces($root) {
foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
$value = $comment->getValue();
if ($value[0] != '#') {
$match = null;
if (preg_match('@^(/[/*]+)[^/*\s]@', $value, $match)) {
$this->raiseLintAtOffset(
$comment->getOffset(),
self::LINT_COMMENT_SPACING,
'Put space after comment start.',
$match[1],
$match[1].' ');
}
}
}
}
protected function lintHashComments($root) {
foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
$value = $comment->getValue();
if ($value[0] != '#') {
continue;
}
$this->raiseLintAtOffset(
$comment->getOffset(),
self::LINT_COMMENT_STYLE,
'Use "//" single-line comments, not "#".',
'#',
(preg_match('/^#\S/', $value) ? '// ' : '//'));
}
}
/**
* Find cases where loops get nested inside each other but use the same
* iterator variable. For example:
*
* COUNTEREXAMPLE
* foreach ($list as $thing) {
* foreach ($stuff as $thing) { // <-- Raises an error for reuse of $thing
* // ...
* }
* }
*
*/
private function lintReusedIterators($root) {
$used_vars = array();
$for_loops = $root->selectDescendantsOfType('n_FOR');
foreach ($for_loops as $for_loop) {
$var_map = array();
// Find all the variables that are assigned to in the for() expression.
$for_expr = $for_loop->getChildOfType(0, 'n_FOR_EXPRESSION');
$bin_exprs = $for_expr->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($bin_exprs as $bin_expr) {
if ($bin_expr->getChildByIndex(1)->getConcreteString() == '=') {
$var = $bin_expr->getChildByIndex(0);
$var_map[$var->getConcreteString()] = $var;
}
}
$used_vars[$for_loop->getID()] = $var_map;
}
$foreach_loops = $root->selectDescendantsOfType('n_FOREACH');
foreach ($foreach_loops as $foreach_loop) {
$var_map = array();
$foreach_expr = $foreach_loop->getChildOftype(0, 'n_FOREACH_EXPRESSION');
// We might use one or two vars, i.e. "foreach ($x as $y => $z)" or
// "foreach ($x as $y)".
$possible_used_vars = array(
$foreach_expr->getChildByIndex(1),
$foreach_expr->getChildByIndex(2),
);
foreach ($possible_used_vars as $var) {
if ($var->getTypeName() == 'n_EMPTY') {
continue;
}
$name = $var->getConcreteString();
$name = trim($name, '&'); // Get rid of ref silliness.
$var_map[$name] = $var;
}
$used_vars[$foreach_loop->getID()] = $var_map;
}
$all_loops = $for_loops->add($foreach_loops);
foreach ($all_loops as $loop) {
$child_for_loops = $loop->selectDescendantsOfType('n_FOR');
$child_foreach_loops = $loop->selectDescendantsOfType('n_FOREACH');
$child_loops = $child_for_loops->add($child_foreach_loops);
$outer_vars = $used_vars[$loop->getID()];
foreach ($child_loops as $inner_loop) {
$inner_vars = $used_vars[$inner_loop->getID()];
$shared = array_intersect_key($outer_vars, $inner_vars);
if ($shared) {
$shared_desc = implode(', ', array_keys($shared));
$message = $this->raiseLintAtNode(
$inner_loop->getChildByIndex(0),
self::LINT_REUSED_ITERATORS,
"This loop reuses iterator variables ({$shared_desc}) from an ".
"outer loop. You might be clobbering the outer iterator. Change ".
"the inner loop to use a different iterator name.");
$locations = array();
foreach ($shared as $var) {
$locations[] = $this->getOtherLocation($var->getOffset());
}
$message->setOtherLocations($locations);
}
}
}
}
/**
* Find cases where a foreach loop is being iterated using a variable
* reference and the same variable is used outside of the loop without
* calling unset() or reassigning the variable to another variable
* reference.
*
* COUNTEREXAMPLE
* foreach ($ar as &$a) {
* // ...
* }
* $a = 1; // <-- Raises an error for using $a
*
*/
protected function lintReusedIteratorReferences($root) {
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
$defs = $fdefs->add($mdefs);
foreach ($defs as $def) {
$body = $def->getChildByIndex(5);
if ($body->getTypeName() == 'n_EMPTY') {
// Abstract method declaration.
continue;
}
$exclude = array();
// Exclude uses of variables, unsets, and foreach loops
// within closures - they are checked on their own
$func_defs = $body->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($func_defs as $func_def) {
$vars = $func_def->selectDescendantsOfType('n_VARIABLE');
foreach ($vars as $var) {
$exclude[$var->getID()] = true;
}
$unset_lists = $func_def->selectDescendantsOfType('n_UNSET_LIST');
foreach ($unset_lists as $unset_list) {
$exclude[$unset_list->getID()] = true;
}
$foreaches = $func_def->selectDescendantsOfType('n_FOREACH');
foreach ($foreaches as $foreach) {
$exclude[$foreach->getID()] = true;
}
}
// Find all variables that are unset within the scope
$unset_vars = array();
$unset_lists = $body->selectDescendantsOfType('n_UNSET_LIST');
foreach ($unset_lists as $unset_list) {
if (isset($exclude[$unset_list->getID()])) {
continue;
}
$unset_list_vars = $unset_list->selectDescendantsOfType('n_VARIABLE');
foreach ($unset_list_vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$unset_vars[$concrete][] = $var->getOffset();
$exclude[$var->getID()] = true;
}
}
// Find all reference variables in foreach expressions
$reference_vars = array();
$foreaches = $body->selectDescendantsOfType('n_FOREACH');
foreach ($foreaches as $foreach) {
if (isset($exclude[$foreach->getID()])) {
continue;
}
$foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION');
$var = $foreach_expr->getChildByIndex(2);
if ($var->getTypeName() != 'n_VARIABLE_REFERENCE') {
continue;
}
$reference = $var->getChildByIndex(0);
if ($reference->getTypeName() != 'n_VARIABLE') {
continue;
}
$reference_name = $this->getConcreteVariableString($reference);
$reference_vars[$reference_name][] = $reference->getOffset();
$exclude[$reference->getID()] = true;
// Exclude uses of the reference variable within the foreach loop
$foreach_vars = $foreach->selectDescendantsOfType('n_VARIABLE');
foreach ($foreach_vars as $var) {
$name = $this->getConcreteVariableString($var);
if ($name == $reference_name) {
$exclude[$var->getID()] = true;
}
}
}
// Allow usage if the reference variable is assigned to another
// reference variable
$binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($binary as $expr) {
if ($expr->getChildByIndex(1)->getConcreteString() != '=') {
continue;
}
$lval = $expr->getChildByIndex(0);
if ($lval->getTypeName() != 'n_VARIABLE') {
continue;
}
$rval = $expr->getChildByIndex(2);
if ($rval->getTypeName() != 'n_VARIABLE_REFERENCE') {
continue;
}
// Counts as unsetting a variable
$concrete = $this->getConcreteVariableString($lval);
$unset_vars[$concrete][] = $lval->getOffset();
$exclude[$lval->getID()] = true;
}
$all_vars = array();
$all = $body->selectDescendantsOfType('n_VARIABLE');
foreach ($all as $var) {
if (isset($exclude[$var->getID()])) {
continue;
}
$name = $this->getConcreteVariableString($var);
if (!isset($reference_vars[$name])) {
continue;
}
// Find the closest reference offset to this variable
$reference_offset = null;
foreach ($reference_vars[$name] as $offset) {
if ($offset < $var->getOffset()) {
$reference_offset = $offset;
} else {
break;
}
}
if (!$reference_offset) {
continue;
}
// Check if an unset exists between reference and usage of this
// variable
$warn = true;
if (isset($unset_vars[$name])) {
foreach ($unset_vars[$name] as $unset_offset) {
if ($unset_offset > $reference_offset &&
$unset_offset < $var->getOffset()) {
$warn = false;
break;
}
}
}
if ($warn) {
$this->raiseLintAtNode(
$var,
self::LINT_REUSED_ITERATOR_REFERENCE,
'This variable was used already as a by-reference iterator ' .
'variable. Such variables survive outside the foreach loop, ' .
'do not reuse.');
}
}
}
}
protected function lintVariableVariables($root) {
$vvars = $root->selectDescendantsOfType('n_VARIABLE_VARIABLE');
foreach ($vvars as $vvar) {
$this->raiseLintAtNode(
$vvar,
self::LINT_VARIABLE_VARIABLE,
'Rewrite this code to use an array. Variable variables are unclear '.
'and hinder static analysis.');
}
}
protected function lintUndeclaredVariables($root) {
// These things declare variables in a function:
// Explicit parameters
// Assignment
// Assignment via list()
// Static
// Global
// Lexical vars
// Builtins ($this)
// foreach()
// catch
//
// These things make lexical scope unknowable:
// Use of extract()
// Assignment to variable variables ($$x)
// Global with variable variables
//
// These things don't count as "using" a variable:
// isset()
// empty()
// Static class variables
//
// The general approach here is to find each function/method declaration,
// then:
//
// 1. Identify all the variable declarations, and where they first occur
// in the function/method declaration.
// 2. Identify all the uses that don't really count (as above).
// 3. Everything else must be a use of a variable.
// 4. For each variable, check if any uses occur before the declaration
// and warn about them.
//
// We also keep track of where lexical scope becomes unknowable (e.g.,
// because the function calls extract() or uses dynamic variables,
// preventing us from keeping track of which variables are defined) so we
// can stop issuing warnings after that.
//
// TODO: Support functions defined inside other functions which is commonly
// used with anonymous functions.
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
$defs = $fdefs->add($mdefs);
foreach ($defs as $def) {
// We keep track of the first offset where scope becomes unknowable, and
// silence any warnings after that. Default it to INT_MAX so we can min()
// it later to keep track of the first problem we encounter.
$scope_destroyed_at = PHP_INT_MAX;
$declarations = array(
'$this' => 0,
) + array_fill_keys($this->getSuperGlobalNames(), 0);
$declaration_tokens = array();
$exclude_tokens = array();
$vars = array();
// First up, find all the different kinds of declarations, as explained
// above. Put the tokens into the $vars array.
$param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
$param_vars = $param_list->selectDescendantsOfType('n_VARIABLE');
foreach ($param_vars as $var) {
$vars[] = $var;
}
// This is PHP5.3 closure syntax: function () use ($x) {};
$lexical_vars = $def
->getChildByIndex(4)
->selectDescendantsOfType('n_VARIABLE');
foreach ($lexical_vars as $var) {
$vars[] = $var;
}
$body = $def->getChildByIndex(5);
if ($body->getTypeName() == 'n_EMPTY') {
// Abstract method declaration.
continue;
}
$static_vars = $body
->selectDescendantsOfType('n_STATIC_DECLARATION')
->selectDescendantsOfType('n_VARIABLE');
foreach ($static_vars as $var) {
$vars[] = $var;
}
$global_vars = $body
->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
foreach ($global_vars as $var_list) {
foreach ($var_list->getChildren() as $var) {
if ($var->getTypeName() == 'n_VARIABLE') {
$vars[] = $var;
} else {
// Dynamic global variable, i.e. "global $$x;".
$scope_destroyed_at = min($scope_destroyed_at, $var->getOffset());
// An error is raised elsewhere, no need to raise here.
}
}
}
$catches = $body
->selectDescendantsOfType('n_CATCH')
->selectDescendantsOfType('n_VARIABLE');
foreach ($catches as $var) {
$vars[] = $var;
}
$binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($binary as $expr) {
if ($expr->getChildByIndex(1)->getConcreteString() != '=') {
continue;
}
$lval = $expr->getChildByIndex(0);
if ($lval->getTypeName() == 'n_VARIABLE') {
$vars[] = $lval;
} else if ($lval->getTypeName() == 'n_LIST') {
// Recursivey grab everything out of list(), since the grammar
// permits list() to be nested. Also note that list() is ONLY valid
// as an lval assignments, so we could safely lift this out of the
// n_BINARY_EXPRESSION branch.
$assign_vars = $lval->selectDescendantsOfType('n_VARIABLE');
foreach ($assign_vars as $var) {
$vars[] = $var;
}
}
if ($lval->getTypeName() == 'n_VARIABLE_VARIABLE') {
$scope_destroyed_at = min($scope_destroyed_at, $lval->getOffset());
// No need to raise here since we raise an error elsewhere.
}
}
$calls = $body->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = strtolower($call->getChildByIndex(0)->getConcreteString());
if ($name == 'empty' || $name == 'isset') {
$params = $call
->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
->selectDescendantsOfType('n_VARIABLE');
foreach ($params as $var) {
$exclude_tokens[$var->getID()] = true;
}
continue;
}
if ($name != 'extract') {
continue;
}
$scope_destroyed_at = min($scope_destroyed_at, $call->getOffset());
$this->raiseLintAtNode(
$call,
self::LINT_EXTRACT_USE,
'Avoid extract(). It is confusing and hinders static analysis.');
}
// Now we have every declaration except foreach(), handled below. Build
// two maps, one which just keeps track of which tokens are part of
// declarations ($declaration_tokens) and one which has the first offset
// where a variable is declared ($declarations).
foreach ($vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$declarations[$concrete] = min(
idx($declarations, $concrete, PHP_INT_MAX),
$var->getOffset());
$declaration_tokens[$var->getID()] = true;
}
// Excluded tokens are ones we don't "count" as being used, described
// above. Put them into $exclude_tokens.
$class_statics = $body
->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
$class_static_vars = $class_statics
->selectDescendantsOfType('n_VARIABLE');
foreach ($class_static_vars as $var) {
$exclude_tokens[$var->getID()] = true;
}
// Find all the variables in scope, and figure out where they are used.
// We want to find foreach() iterators which are both declared before and
// used after the foreach() loop.
$uses = array();
$all_vars = $body->selectDescendantsOfType('n_VARIABLE');
$all = array();
// NOTE: $all_vars is not a real array so we can't unset() it.
foreach ($all_vars as $var) {
// Be strict since it's easier; we don't let you reuse an iterator you
// declared before a loop after the loop, even if you're just assigning
// to it.
$concrete = $this->getConcreteVariableString($var);
$uses[$concrete][$var->getID()] = $var->getOffset();
if (isset($declaration_tokens[$var->getID()])) {
// We know this is part of a declaration, so it's fine.
continue;
}
if (isset($exclude_tokens[$var->getID()])) {
// We know this is part of isset() or similar, so it's fine.
continue;
}
$all[$var->getOffset()] = $concrete;
}
// Do foreach() last, we want to handle implicit redeclaration of a
// variable already in scope since this probably means we're ovewriting a
// local.
// NOTE: Processing foreach expressions in order allows programs which
// reuse iterator variables in other foreach() loops -- this is fine. We
// have a separate warning to prevent nested loops from reusing the same
// iterators.
$foreaches = $body->selectDescendantsOfType('n_FOREACH');
$all_foreach_vars = array();
foreach ($foreaches as $foreach) {
$foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION');
$foreach_vars = array();
// Determine the end of the foreach() loop.
$foreach_tokens = $foreach->getTokens();
$last_token = end($foreach_tokens);
$foreach_end = $last_token->getOffset();
$key_var = $foreach_expr->getChildByIndex(1);
if ($key_var->getTypeName() == 'n_VARIABLE') {
$foreach_vars[] = $key_var;
}
$value_var = $foreach_expr->getChildByIndex(2);
if ($value_var->getTypeName() == 'n_VARIABLE') {
$foreach_vars[] = $value_var;
} else {
// The root-level token may be a reference, as in:
// foreach ($a as $b => &$c) { ... }
// Reach into the n_VARIABLE_REFERENCE node to grab the n_VARIABLE
// node.
$var = $value_var->getChildByIndex(0);
if ($var->getTypeName() == 'n_VARIABLE_VARIABLE') {
$var = $var->getChildByIndex(0);
}
$foreach_vars[] = $var;
}
// Remove all uses of the iterators inside of the foreach() loop from
// the $uses map.
foreach ($foreach_vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$offset = $var->getOffset();
foreach ($uses[$concrete] as $id => $use_offset) {
if (($use_offset >= $offset) && ($use_offset < $foreach_end)) {
unset($uses[$concrete][$id]);
}
}
$all_foreach_vars[] = $var;
}
}
foreach ($all_foreach_vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$offset = $var->getOffset();
// If a variable was declared before a foreach() and is used after
// it, raise a message.
if (isset($declarations[$concrete])) {
if ($declarations[$concrete] < $offset) {
if (!empty($uses[$concrete]) &&
max($uses[$concrete]) > $offset) {
$message = $this->raiseLintAtNode(
$var,
self::LINT_REUSED_AS_ITERATOR,
'This iterator variable is a previously declared local '.
'variable. To avoid overwriting locals, do not reuse them '.
'as iterator variables.');
$message->setOtherLocations(array(
$this->getOtherLocation($declarations[$concrete]),
$this->getOtherLocation(max($uses[$concrete])),
));
}
}
}
// This is a declaration, exclude it from the "declare variables prior
// to use" check below.
unset($all[$var->getOffset()]);
$vars[] = $var;
}
// Now rebuild declarations to include foreach().
foreach ($vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$declarations[$concrete] = min(
idx($declarations, $concrete, PHP_INT_MAX),
$var->getOffset());
$declaration_tokens[$var->getID()] = true;
}
foreach (array('n_STRING_SCALAR', 'n_HEREDOC') as $type) {
foreach ($body->selectDescendantsOfType($type) as $string) {
foreach ($string->getStringVariables() as $offset => $var) {
$all[$string->getOffset() + $offset - 1] = '$'.$var;
}
}
}
// Issue a warning for every variable token, unless it appears in a
// declaration, we know about a prior declaration, we have explicitly
// exlcuded it, or scope has been made unknowable before it appears.
$issued_warnings = array();
foreach ($all as $offset => $concrete) {
if ($offset >= $scope_destroyed_at) {
// This appears after an extract() or $$var so we have no idea
// whether it's legitimate or not. We raised a harshly-worded warning
// when scope was made unknowable, so just ignore anything we can't
// figure out.
continue;
}
if ($offset >= idx($declarations, $concrete, PHP_INT_MAX)) {
// The use appears after the variable is declared, so it's fine.
continue;
}
if (!empty($issued_warnings[$concrete])) {
// We've already issued a warning for this variable so we don't need
// to issue another one.
continue;
}
$this->raiseLintAtOffset(
$offset,
self::LINT_UNDECLARED_VARIABLE,
'Declare variables prior to use (even if you are passing them '.
'as reference parameters). You may have misspelled this '.
'variable name.',
$concrete);
$issued_warnings[$concrete] = true;
}
}
}
private function getConcreteVariableString($var) {
$concrete = $var->getConcreteString();
// Strip off curly braces as in $obj->{$property}.
$concrete = trim($concrete, '{}');
return $concrete;
}
protected function lintPHPTagUse($root) {
$tokens = $root->getTokens();
foreach ($tokens as $token) {
if ($token->getTypeName() == 'T_OPEN_TAG') {
if (trim($token->getValue()) == '<?') {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_SHORT_TAG,
'Use the full form of the PHP open tag, "<?php".',
"<?php\n");
}
break;
} else if ($token->getTypeName() == 'T_OPEN_TAG_WITH_ECHO') {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_ECHO_TAG,
'Avoid the PHP echo short form, "<?=".');
break;
} else {
if (!preg_match('/^#!/', $token->getValue())) {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_OPEN_TAG,
'PHP files should start with "<?php", which may be preceded by '.
'a "#!" line for scripts.');
}
break;
}
}
foreach ($root->selectTokensOfType('T_CLOSE_TAG') as $token) {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_CLOSE_TAG,
'Do not use the PHP closing tag, "?>".');
}
}
protected function lintNamingConventions($root) {
// We're going to build up a list of <type, name, token, error> tuples
// and then try to instantiate a hook class which has the opportunity to
// override us.
$names = array();
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$name_token = $class->getChildByIndex(1);
$name_string = $name_token->getConcreteString();
$names[] = array(
'class',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
? null
: 'Follow naming conventions: classes should be named using '.
'UpperCamelCase.',
);
}
$ifaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
foreach ($ifaces as $iface) {
$name_token = $iface->getChildByIndex(1);
$name_string = $name_token->getConcreteString();
$names[] = array(
'interface',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
? null
: 'Follow naming conventions: interfaces should be named using '.
'UpperCamelCase.',
);
}
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($functions as $function) {
$name_token = $function->getChildByIndex(2);
if ($name_token->getTypeName() == 'n_EMPTY') {
// Unnamed closure.
continue;
}
$name_string = $name_token->getConcreteString();
$names[] = array(
'function',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
? null
: 'Follow naming conventions: functions should be named using '.
'lowercase_with_underscores.',
);
}
$methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$name_token = $method->getChildByIndex(2);
$name_string = $name_token->getConcreteString();
$names[] = array(
'method',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowerCamelCase(
ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
? null
: 'Follow naming conventions: methods should be named using '.
'lowerCamelCase.',
);
}
$param_tokens = array();
$params = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST');
foreach ($params as $param_list) {
foreach ($param_list->getChildren() as $param) {
$name_token = $param->getChildByIndex(1);
if ($name_token->getTypeName() == 'n_VARIABLE_REFERENCE') {
$name_token = $name_token->getChildOfType(0, 'n_VARIABLE');
}
$param_tokens[$name_token->getID()] = true;
$name_string = $name_token->getConcreteString();
$names[] = array(
'parameter',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
? null
: 'Follow naming conventions: parameters should be named using '.
'lowercase_with_underscores.',
);
}
}
$constants = $root->selectDescendantsOfType(
'n_CLASS_CONSTANT_DECLARATION_LIST');
foreach ($constants as $constant_list) {
foreach ($constant_list->getChildren() as $constant) {
$name_token = $constant->getChildByIndex(0);
$name_string = $name_token->getConcreteString();
$names[] = array(
'constant',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($name_string)
? null
: 'Follow naming conventions: class constants should be named '.
'using UPPERCASE_WITH_UNDERSCORES.',
);
}
}
$member_tokens = array();
$props = $root->selectDescendantsOfType('n_CLASS_MEMBER_DECLARATION_LIST');
foreach ($props as $prop_list) {
foreach ($prop_list->getChildren() as $token_id => $prop) {
if ($prop->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') {
continue;
}
$name_token = $prop->getChildByIndex(0);
$member_tokens[$name_token->getID()] = true;
$name_string = $name_token->getConcreteString();
$names[] = array(
'member',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowerCamelCase(
ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
? null
: 'Follow naming conventions: class properties should be named '.
'using lowerCamelCase.',
);
}
}
$superglobal_map = array_fill_keys(
$this->getSuperGlobalNames(),
true);
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
$defs = $fdefs->add($mdefs);
foreach ($defs as $def) {
$globals = $def->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
$globals = $globals->selectDescendantsOfType('n_VARIABLE');
$globals_map = array();
foreach ($globals as $global) {
$global_string = $global->getConcreteString();
$globals_map[$global_string] = true;
$names[] = array(
- 'global',
+ 'user',
$global_string,
$global,
// No advice for globals, but hooks have an option to provide some.
null);
}
// Exclude access of static properties, since lint will be raised at
// their declaration if they're invalid and they may not conform to
// variable rules. This is slightly overbroad (includes the entire
// rhs of a "Class::..." token) to cover cases like "Class:$x[0]". These
// varaibles are simply made exempt from naming conventions.
$exclude_tokens = array();
$statics = $def->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($statics as $static) {
$rhs = $static->getChildByIndex(1);
$rhs_vars = $def->selectDescendantsOfType('n_VARIABLE');
foreach ($rhs_vars as $var) {
$exclude_tokens[$var->getID()] = true;
}
}
$vars = $def->selectDescendantsOfType('n_VARIABLE');
foreach ($vars as $token_id => $var) {
if (isset($member_tokens[$token_id])) {
continue;
}
if (isset($param_tokens[$token_id])) {
continue;
}
if (isset($exclude_tokens[$token_id])) {
continue;
}
$var_string = $var->getConcreteString();
// Awkward artifact of "$o->{$x}".
$var_string = trim($var_string, '{}');
if (isset($superglobal_map[$var_string])) {
continue;
}
if (isset($globals_map[$var_string])) {
continue;
}
$names[] = array(
'variable',
$var_string,
$var,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPVariable($var_string))
? null
: 'Follow naming conventions: variables should be named using '.
'lowercase_with_underscores.',
);
}
}
$engine = $this->getEngine();
$working_copy = $engine->getWorkingCopy();
if ($working_copy) {
// If a naming hook is configured, give it a chance to override the
// default results for all the symbol names.
$hook_class = $working_copy->getConfig('lint.xhpast.naminghook');
if ($hook_class) {
$hook_obj = newv($hook_class, array());
foreach ($names as $k => $name_attrs) {
list($type, $name, $token, $default) = $name_attrs;
$result = $hook_obj->lintSymbolName($type, $name, $default);
$names[$k][3] = $result;
}
}
}
// Raise anything we're left with.
foreach ($names as $k => $name_attrs) {
list($type, $name, $token, $result) = $name_attrs;
if ($result) {
$this->raiseLintAtNode(
$token,
self::LINT_NAMING_CONVENTIONS,
$result);
}
}
}
protected function lintSurpriseConstructors($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1)->getConcreteString();
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$method_name_token = $method->getChildByIndex(2);
$method_name = $method_name_token->getConcreteString();
if (strtolower($class_name) == strtolower($method_name)) {
$this->raiseLintAtNode(
$method_name_token,
self::LINT_IMPLICIT_CONSTRUCTOR,
'Name constructors __construct() explicitly. This method is a '.
'constructor because it has the same name as the class it is '.
'defined in.');
}
}
}
}
protected function lintParenthesesShouldHugExpressions($root) {
$calls = $root->selectDescendantsOfType('n_CALL_PARAMETER_LIST');
$controls = $root->selectDescendantsOfType('n_CONTROL_CONDITION');
$fors = $root->selectDescendantsOfType('n_FOR_EXPRESSION');
$foreach = $root->selectDescendantsOfType('n_FOREACH_EXPRESSION');
$decl = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST');
$all_paren_groups = $calls
->add($controls)
->add($fors)
->add($foreach)
->add($decl);
foreach ($all_paren_groups as $group) {
$tokens = $group->getTokens();
$token_o = array_shift($tokens);
$token_c = array_pop($tokens);
if ($token_o->getTypeName() != '(') {
throw new Exception('Expected open paren!');
}
if ($token_c->getTypeName() != ')') {
throw new Exception('Expected close paren!');
}
$nonsem_o = $token_o->getNonsemanticTokensAfter();
$nonsem_c = $token_c->getNonsemanticTokensBefore();
if (!$nonsem_o) {
continue;
}
$raise = array();
$string_o = implode('', mpull($nonsem_o, 'getValue'));
if (preg_match('/^[ ]+$/', $string_o)) {
$raise[] = array($nonsem_o, $string_o);
}
if ($nonsem_o !== $nonsem_c) {
$string_c = implode('', mpull($nonsem_c, 'getValue'));
if (preg_match('/^[ ]+$/', $string_c)) {
$raise[] = array($nonsem_c, $string_c);
}
}
foreach ($raise as $warning) {
list($tokens, $string) = $warning;
$this->raiseLintAtOffset(
reset($tokens)->getOffset(),
self::LINT_PARENTHESES_SPACING,
'Parentheses should hug their contents.',
$string,
'');
}
}
}
protected function lintSpaceAfterControlStatementKeywords($root) {
foreach ($root->getTokens() as $id => $token) {
switch ($token->getTypeName()) {
case 'T_IF':
case 'T_ELSE':
case 'T_FOR':
case 'T_FOREACH':
case 'T_WHILE':
case 'T_DO':
case 'T_SWITCH':
$after = $token->getNonsemanticTokensAfter();
if (empty($after)) {
$this->raiseLintAtToken(
$token,
self::LINT_CONTROL_STATEMENT_SPACING,
'Convention: put a space after control statements.',
$token->getValue().' ');
} else if (count($after) == 1) {
$space = head($after);
// If we have an else clause with braces, $space may not be
// a single white space. e.g.,
//
// if ($x)
// echo 'foo'
// else // <- $space is not " " but "\n ".
// echo 'bar'
//
// We just require it starts with either a whitespace or a newline.
if ($token->getTypeName() == 'T_ELSE' ||
$token->getTypeName() == 'T_DO') {
break;
}
if ($space->isAnyWhitespace() && $space->getValue() != ' ') {
$this->raiseLintAtToken(
$space,
self::LINT_CONTROL_STATEMENT_SPACING,
'Convention: put a single space after control statements.',
' ');
}
}
break;
}
}
}
protected function lintSpaceAroundBinaryOperators($root) {
// NOTE: '.' is parsed as n_CONCATENATION_LIST, not n_BINARY_EXPRESSION,
// so we don't select it here.
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildByIndex(1);
$operator_value = $operator->getConcreteString();
list($before, $after) = $operator->getSurroundingNonsemanticTokens();
$replace = null;
if (empty($before) && empty($after)) {
$replace = " {$operator_value} ";
} else if (empty($before)) {
$replace = " {$operator_value}";
} else if (empty($after)) {
$replace = "{$operator_value} ";
}
if ($replace !== null) {
$this->raiseLintAtNode(
$operator,
self::LINT_BINARY_EXPRESSION_SPACING,
'Convention: logical and arithmetic operators should be '.
'surrounded by whitespace.',
$replace);
}
}
$tokens = $root->selectTokensOfType(',');
foreach ($tokens as $token) {
$next = $token->getNextToken();
switch ($next->getTypeName()) {
case ')':
case 'T_WHITESPACE':
break;
break;
default:
$this->raiseLintAtToken(
$token,
self::LINT_BINARY_EXPRESSION_SPACING,
'Convention: comma should be followed by space.',
', ');
break;
}
}
// TODO: Spacing around ".".
// TODO: Spacing around default parameter assignment in function/method
// declarations (which is not n_BINARY_EXPRESSION).
}
protected function lintDynamicDefines($root) {
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = $call->getChildByIndex(0)->getConcreteString();
if (strtolower($name) == 'define') {
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
$defined = $parameter_list->getChildByIndex(0);
if (!$defined->isStaticScalar()) {
$this->raiseLintAtNode(
$defined,
self::LINT_DYNAMIC_DEFINE,
'First argument to define() must be a string literal.');
}
}
}
}
protected function lintUseOfThisInStaticMethods($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$attributes = $method
->getChildByIndex(0, 'n_METHOD_MODIFIER_LIST')
->selectDescendantsOfType('n_STRING');
$method_is_static = false;
$method_is_abstract = false;
foreach ($attributes as $attribute) {
if (strtolower($attribute->getConcreteString()) == 'static') {
$method_is_static = true;
}
if (strtolower($attribute->getConcreteString()) == 'abstract') {
$method_is_abstract = true;
}
}
if ($method_is_abstract) {
continue;
}
if (!$method_is_static) {
continue;
}
$body = $method->getChildOfType(5, 'n_STATEMENT_LIST');
$variables = $body->selectDescendantsOfType('n_VARIABLE');
foreach ($variables as $variable) {
if ($method_is_static &&
strtolower($variable->getConcreteString()) == '$this') {
$this->raiseLintAtNode(
$variable,
self::LINT_STATIC_THIS,
'You can not reference "$this" inside a static method.');
}
}
}
}
}
/**
* preg_quote() takes two arguments, but the second one is optional because
* it is possible to use (), [] or {} as regular expression delimiters. If
* you don't pass a second argument, you're probably going to get something
* wrong.
*/
protected function lintPregQuote($root) {
$function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($function_calls as $call) {
$name = $call->getChildByIndex(0)->getConcreteString();
if (strtolower($name) === 'preg_quote') {
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
if (count($parameter_list->getChildren()) !== 2) {
$this->raiseLintAtNode(
$call,
self::LINT_PREG_QUOTE_MISUSE,
'If you use pattern delimiters that require escaping (such as //, '.
'but not ()) then you should pass two arguments to preg_quote(), '.
'so that preg_quote() knows which delimiter to escape.');
}
}
}
}
/**
* Exit is parsed as an expression, but using it as such is almost always
* wrong. That is, this is valid:
*
* strtoupper(33 * exit - 6);
*
* When exit is used as an expression, it causes the program to terminate with
* exit code 0. This is likely not what is intended; these statements have
* different effects:
*
* exit(-1);
* exit -1;
*
* The former exits with a failure code, the latter with a success code!
*/
protected function lintExitExpressions($root) {
$unaries = $root->selectDescendantsOfType('n_UNARY_PREFIX_EXPRESSION');
foreach ($unaries as $unary) {
$operator = $unary->getChildByIndex(0)->getConcreteString();
if (strtolower($operator) == 'exit') {
if ($unary->getParentNode()->getTypeName() != 'n_STATEMENT') {
$this->raiseLintAtNode(
$unary,
self::LINT_EXIT_EXPRESSION,
"Use exit as a statement, not an expression.");
}
}
}
}
private function lintArrayIndexWhitespace($root) {
$indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS');
foreach ($indexes as $index) {
$tokens = $index->getChildByIndex(0)->getTokens();
$last = array_pop($tokens);
$trailing = $last->getNonsemanticTokensAfter();
$trailing_text = implode('', mpull($trailing, 'getValue'));
if (preg_match('/^ +$/', $trailing_text)) {
$this->raiseLintAtOffset(
$last->getOffset() + strlen($last->getValue()),
self::LINT_ARRAY_INDEX_SPACING,
'Convention: no spaces before index access.',
$trailing_text,
'');
}
}
}
protected function lintTODOComments($root) {
$comments = $root->selectTokensOfType('T_COMMENT') +
$root->selectTokensOfType('T_DOC_COMMENT');
foreach ($comments as $token) {
$value = $token->getValue();
$matches = null;
$preg = preg_match_all(
'/TODO/',
$value,
$matches,
PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $match) {
list($string, $offset) = $match;
$this->raiseLintAtOffset(
$token->getOffset() + $offset,
self::LINT_TODO_COMMENT,
'This comment has a TODO.',
$string);
}
}
}
/**
* Lint that if the file declares exactly one interface or class,
* the name of the file matches the name of the class,
* unless the classname is funky like an XHP element.
*/
private function lintPrimaryDeclarationFilenameMatch($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
if (count($classes) + count($interfaces) != 1) {
return;
}
$declarations = count($classes) ? $classes : $interfaces;
$declarations->rewind();
$declaration = $declarations->current();
$decl_name = $declaration->getChildByIndex(1);
$decl_string = $decl_name->getConcreteString();
// Exclude strangely named classes, e.g. XHP tags.
if (!preg_match('/^\w+$/', $decl_string)) {
return;
}
$rename = $decl_string.'.php';
$path = $this->getActivePath();
$filename = basename($path);
if ($rename == $filename) {
return;
}
$this->raiseLintAtNode(
$decl_name,
self::LINT_CLASS_FILENAME_MISMATCH,
"The name of this file differs from the name of the class or interface ".
"it declares. Rename the file to '{$rename}'.");
}
private function lintPlusOperatorOnStrings($root) {
$binops = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($binops as $binop) {
$op = $binop->getChildByIndex(1);
if ($op->getConcreteString() != '+') {
continue;
}
$left = $binop->getChildByIndex(0);
$right = $binop->getChildByIndex(2);
if (($left->getTypeName() == 'n_STRING_SCALAR') ||
($right->getTypeName() == 'n_STRING_SCALAR')) {
$this->raiseLintAtNode(
$binop,
self::LINT_PLUS_OPERATOR_ON_STRINGS,
"In PHP, '.' is the string concatenation operator, not '+'. This ".
"expression uses '+' with a string literal as an operand.");
}
}
}
/**
* Finds duplicate keys in array initializers, as in
* array(1 => 'anything', 1 => 'foo'). Since the first entry is ignored,
* this is almost certainly an error.
*/
private function lintDuplicateKeysInArray($root) {
$array_literals = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
foreach ($array_literals as $array_literal) {
$nodes_by_key = array();
$keys_warn = array();
$list_node = $array_literal->getChildByIndex(0);
foreach ($list_node->getChildren() as $array_entry) {
$key_node = $array_entry->getChildByIndex(0);
switch ($key_node->getTypeName()) {
case 'n_STRING_SCALAR':
case 'n_NUMERIC_SCALAR':
// Scalars: array(1 => 'v1', '1' => 'v2');
$key = 'scalar:'.(string)$key_node->evalStatic();
break;
case 'n_SYMBOL_NAME':
case 'n_VARIABLE':
case 'n_CLASS_STATIC_ACCESS':
// Constants: array(CONST => 'v1', CONST => 'v2');
// Variables: array($a => 'v1', $a => 'v2');
// Class constants and vars: array(C::A => 'v1', C::A => 'v2');
$key = $key_node->getTypeName().':'.$key_node->getConcreteString();
break;
default:
$key = null;
break;
}
if ($key !== null) {
if (isset($nodes_by_key[$key])) {
$keys_warn[$key] = true;
}
$nodes_by_key[$key][] = $key_node;
}
}
foreach ($keys_warn as $key => $_) {
$node = array_pop($nodes_by_key[$key]);
$message = $this->raiseLintAtNode(
$node,
self::LINT_DUPLICATE_KEYS_IN_ARRAY,
"Duplicate key in array initializer. PHP will ignore all ".
"but the last entry.");
$locations = array();
foreach ($nodes_by_key[$key] as $node) {
$locations[] = $this->getOtherLocation($node->getOffset());
}
$message->setOtherLocations($locations);
}
}
}
private function lintRaggedClasstreeEdges($root) {
$parser = new PhutilDocblockParser();
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$is_final = false;
$is_abstract = false;
$is_concrete_extensible = false;
$attributes = $class->getChildOfType(0, 'n_CLASS_ATTRIBUTES');
foreach ($attributes->getChildren() as $child) {
if ($child->getConcreteString() == 'final') {
$is_final = true;
}
if ($child->getConcreteString() == 'abstract') {
$is_abstract = true;
}
}
$docblock = $class->getDocblockToken();
if ($docblock) {
list($text, $specials) = $parser->parse($docblock->getValue());
$is_concrete_extensible = idx($specials, 'concrete-extensible');
}
if (!$is_final && !$is_abstract && !$is_concrete_extensible) {
$this->raiseLintAtNode(
$class->getChildOfType(1, 'n_CLASS_NAME'),
self::LINT_RAGGED_CLASSTREE_EDGE,
"This class is neither 'final' nor 'abstract', and does not have ".
"a docblock marking it '@concrete-extensible'.");
}
}
}
private function lintClosingCallParen($root) {
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
$calls = $calls->add($root->selectDescendantsOfType('n_METHOD_CALL'));
foreach ($calls as $call) {
// If the last parameter of a call is a HEREDOC, don't apply this rule.
$params = $call
->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
->getChildren();
if ($params) {
$last_param = last($params);
if ($last_param->getTypeName() == 'n_HEREDOC') {
continue;
}
}
$tokens = $call->getTokens();
$last = array_pop($tokens);
$trailing = $last->getNonsemanticTokensBefore();
$trailing_text = implode('', mpull($trailing, 'getValue'));
if (preg_match('/^\s+$/', $trailing_text)) {
$this->raiseLintAtOffset(
$last->getOffset() - strlen($trailing_text),
self::LINT_CLOSING_CALL_PAREN,
'Convention: no spaces before closing parenthesis in calls.',
$trailing_text,
'');
}
}
}
private function lintClosingDeclarationParen($root) {
$decs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$decs = $decs->add($root->selectDescendantsOfType('n_METHOD_DECLARATION'));
foreach ($decs as $dec) {
$params = $dec->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
$tokens = $params->getTokens();
$last = array_pop($tokens);
$trailing = $last->getNonsemanticTokensBefore();
$trailing_text = implode('', mpull($trailing, 'getValue'));
if (preg_match('/^\s+$/', $trailing_text)) {
$this->raiseLintAtOffset(
$last->getOffset() - strlen($trailing_text),
self::LINT_CLOSING_DECL_PAREN,
'Convention: no spaces before closing parenthesis in function and '.
'method declarations.',
$trailing_text,
'');
}
}
}
private function lintKeywordCasing($root) {
$keywords = array();
$symbols = $root->selectDescendantsOfType('n_SYMBOL_NAME');
foreach ($symbols as $symbol) {
$keywords[] = head($symbol->getTokens());
}
$arrays = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
foreach ($arrays as $array) {
$keywords[] = head($array->getTokens());
}
$typehints = $root->selectDescendantsOfType('n_TYPE_NAME');
foreach ($typehints as $typehint) {
$keywords[] = head($typehint->getTokens());
}
$new_invocations = $root->selectDescendantsOfType('n_NEW');
foreach ($new_invocations as $invocation) {
$keywords[] = head($invocation->getTokens());
}
// NOTE: Although PHP generally allows arbitrary casing for all language
// keywords, it's exceedingly rare for anyone to type, e.g., "CLASS" or
// "cLaSs" in the wild. This list just attempts to cover unconventional
// spellings which see some level of use, not all keywords exhaustively.
// There is no token or node type which spans all keywords, so this is
// significantly simpler.
static $keyword_map = array(
'true' => 'true',
'false' => 'false',
'null' => 'null',
'array' => 'array',
'new' => 'new',
);
foreach ($keywords as $keyword) {
$value = $keyword->getValue();
$value_key = strtolower($value);
if (!isset($keyword_map[$value_key])) {
continue;
}
$expected_spelling = $keyword_map[$value_key];
if ($value !== $expected_spelling) {
$this->raiseLintAtToken(
$keyword,
self::LINT_KEYWORD_CASING,
"Convention: spell keyword '{$value}' as '{$expected_spelling}'.",
$expected_spelling);
}
}
}
public function getSuperGlobalNames() {
return array(
'$GLOBALS',
'$_SERVER',
'$_GET',
'$_POST',
'$_FILES',
'$_COOKIE',
'$_SESSION',
'$_REQUEST',
'$_ENV',
);
}
}
diff --git a/src/parser/ArcanistBaseCommitParser.php b/src/parser/ArcanistBaseCommitParser.php
index 3081e7ed..a2fc8a78 100644
--- a/src/parser/ArcanistBaseCommitParser.php
+++ b/src/parser/ArcanistBaseCommitParser.php
@@ -1,178 +1,193 @@
<?php
final class ArcanistBaseCommitParser {
private $api;
private $try;
private $verbose = false;
public function __construct(ArcanistRepositoryAPI $api) {
$this->api = $api;
return $this;
}
private function tokenizeBaseCommitSpecification($raw_spec) {
if (!$raw_spec) {
return array();
}
$spec = preg_split('/\s*,\s*/', $raw_spec);
$spec = array_filter($spec);
foreach ($spec as $rule) {
if (strpos($rule, ':') === false) {
throw new ArcanistUsageException(
"Rule '{$rule}' is invalid, it must have a type and name like ".
"'arc:upstream'.");
}
}
return $spec;
}
private function log($message) {
if ($this->verbose) {
fwrite(STDERR, $message."\n");
}
}
public function resolveBaseCommit(array $specs) {
$specs += array(
- 'args' => '',
+ 'runtime' => '',
'local' => '',
'project' => '',
- 'global' => '',
+ 'user' => '',
'system' => '',
);
foreach ($specs as $source => $spec) {
$specs[$source] = self::tokenizeBaseCommitSpecification($spec);
}
$this->try = array(
- 'args',
+ 'runtime',
'local',
'project',
- 'global',
+ 'user',
'system',
);
while ($this->try) {
$source = head($this->try);
if (!idx($specs, $source)) {
$this->log("No rules left from source '{$source}'.");
array_shift($this->try);
continue;
}
$this->log("Trying rules from source '{$source}'.");
$rules = &$specs[$source];
while ($rule = array_shift($rules)) {
$this->log("Trying rule '{$rule}'.");
$commit = $this->resolveRule($rule, $source);
if ($commit === false) {
// If a rule returns false, it means to go to the next ruleset.
break;
} else if ($commit !== null) {
$this->log("Resolved commit '{$commit}' from rule '{$rule}'.");
return $commit;
}
}
}
return null;
}
/**
* Handle resolving individual rules.
*/
private function resolveRule($rule, $source) {
// NOTE: Returning `null` from this method means "no match".
// Returning `false` from this method means "stop current ruleset".
list($type, $name) = explode(':', $rule, 2);
switch ($type) {
case 'literal':
return $name;
case 'git':
case 'hg':
return $this->api->resolveBaseCommitRule($rule, $source);
case 'arc':
return $this->resolveArcRule($rule, $name, $source);
default:
throw new ArcanistUsageException(
"Base commit rule '{$rule}' (from source '{$source}') ".
"is not a recognized rule.");
}
}
/**
* Handle resolving "arc:*" rules.
*/
private function resolveArcRule($rule, $name, $source) {
+ $name = $this->updateLegacyRuleName($name);
+
switch ($name) {
case 'verbose':
$this->verbose = true;
$this->log("Enabled verbose mode.");
break;
case 'prompt':
$reason = "it is what you typed when prompted.";
$this->api->setBaseCommitExplanation($reason);
return phutil_console_prompt('Against which commit?');
case 'local':
- case 'global':
+ case 'user':
case 'project':
- case 'args':
+ case 'runtime':
case 'system':
// Push the other source on top of the list.
array_unshift($this->try, $name);
$this->log("Switching to source '{$name}'.");
return false;
case 'yield':
// Cycle this source to the end of the list.
$this->try[] = array_shift($this->try);
$this->log("Yielding processing of rules from '{$source}'.");
return false;
case 'halt':
// Dump the whole stack.
$this->try = array();
$this->log("Halting all rule processing.");
return false;
case 'skip':
return null;
case 'empty':
case 'upstream':
case 'outgoing':
case 'bookmark':
case 'amended':
case 'this':
return $this->api->resolveBaseCommitRule($rule, $source);
default:
$matches = null;
if (preg_match('/^exec\((.*)\)$/', $name, $matches)) {
$root = $this->api->getWorkingCopyIdentity()->getProjectRoot();
$future = new ExecFuture('%C', $matches[1]);
$future->setCWD($root);
list($err, $stdout) = $future->resolve();
if (!$err) {
return trim($stdout);
} else {
return null;
}
} else if (preg_match('/^nodiff\((.*)\)$/', $name, $matches)) {
return $this->api->resolveBaseCommitRule($rule, $source);
}
throw new ArcanistUsageException(
"Base commit rule '{$rule}' (from source '{$source}') ".
"is not a recognized rule.");
}
}
+ private function updateLegacyRuleName($name) {
+ $updated = array(
+ 'global' => 'user',
+ 'args' => 'runtime',
+ );
+ $new_name = idx($updated, $name);
+ if ($new_name) {
+ $this->log("translating legacy name '$name' to '$new_name'");
+ return $new_name;
+ }
+ return $name;
+ }
+
}
diff --git a/src/parser/__tests__/ArcanistBaseCommitParserTestCase.php b/src/parser/__tests__/ArcanistBaseCommitParserTestCase.php
index 297f51f6..1a2ba1c7 100644
--- a/src/parser/__tests__/ArcanistBaseCommitParserTestCase.php
+++ b/src/parser/__tests__/ArcanistBaseCommitParserTestCase.php
@@ -1,149 +1,177 @@
<?php
final class ArcanistBaseCommitParserTestCase extends ArcanistTestCase {
public function testBasics() {
// Verify that the very basics of base commit resolution work.
$this->assertCommit(
'Empty Rules',
null,
array(
));
$this->assertCommit(
'Literal',
'xyz',
array(
- 'args' => 'literal:xyz',
+ 'runtime' => 'literal:xyz',
));
}
public function testResolutionOrder() {
// Rules should be resolved in order: args, local, project, global. These
// test cases intentionally scramble argument order to test that resolution
// order is independent of argument order.
$this->assertCommit(
'Order: Args',
'y',
array(
'local' => 'literal:n',
'project' => 'literal:n',
- 'args' => 'literal:y',
- 'global' => 'literal:n',
+ 'runtime' => 'literal:y',
+ 'user' => 'literal:n',
));
$this->assertCommit(
'Order: Local',
'y',
array(
'project' => 'literal:n',
'local' => 'literal:y',
- 'global' => 'literal:n',
+ 'user' => 'literal:n',
));
$this->assertCommit(
'Order: Project',
'y',
array(
'project' => 'literal:y',
- 'global' => 'literal:n',
+ 'user' => 'literal:n',
));
$this->assertCommit(
'Order: Global',
'y',
array(
- 'global' => 'literal:y',
+ 'user' => 'literal:y',
));
}
+ public function testLegacyRule() {
+ // 'global' should translate to 'user'
+ $this->assertCommit(
+ '"global" name',
+ 'y',
+ array(
+ 'runtime' => 'arc:global, arc:halt',
+ 'local' => 'arc:halt',
+ 'project' => 'arc:halt',
+ 'user' => 'literal:y',
+ ));
+
+ // 'args' should translate to 'runtime'
+ $this->assertCommit(
+ '"args" name',
+ 'y',
+ array(
+ 'runtime' => 'arc:project, literal:y',
+ 'local' => 'arc:halt',
+ 'project' => 'arc:args',
+ 'user' => 'arc:halt',
+ ));
+
+ }
+
public function testHalt() {
// 'arc:halt' should halt all processing.
$this->assertCommit(
'Halt',
null,
array(
- 'args' => 'arc:halt',
+ 'runtime' => 'arc:halt',
'local' => 'literal:xyz',
));
}
public function testYield() {
// 'arc:yield' should yield to other rulesets.
$this->assertCommit(
'Yield',
'xyz',
array(
- 'args' => 'arc:yield, literal:abc',
+ 'runtime' => 'arc:yield, literal:abc',
'local' => 'literal:xyz',
));
- // This one should return to 'args' after exhausting 'local'.
+ // This one should return to 'runtime' after exhausting 'local'.
$this->assertCommit(
'Yield + Return',
'abc',
array(
- 'args' => 'arc:yield, literal:abc',
+ 'runtime' => 'arc:yield, literal:abc',
'local' => 'arc:skip',
));
}
public function testJump() {
// This should resolve to 'abc' without hitting any of the halts.
$this->assertCommit(
'Jump',
'abc',
array(
- 'args' => 'arc:project, arc:halt',
+ 'runtime' => 'arc:project, arc:halt',
'local' => 'literal:abc',
- 'project' => 'arc:global, arc:halt',
- 'global' => 'arc:local, arc:halt',
+ 'project' => 'arc:user, arc:halt',
+ 'user' => 'arc:local, arc:halt',
));
}
public function testJumpReturn() {
- // After jumping to project, we should return to 'args'.
+ // After jumping to project, we should return to 'runtime'.
$this->assertCommit(
'Jump Return',
'xyz',
array(
- 'args' => 'arc:project, literal:xyz',
+ 'runtime' => 'arc:project, literal:xyz',
'local' => 'arc:halt',
'project' => '',
- 'global' => 'arc:halt',
+ 'user' => 'arc:halt',
));
}
private function assertCommit($desc, $commit, $rules) {
$parser = $this->buildParser();
$result = $parser->resolveBaseCommit($rules);
$this->assertEqual($commit, $result, $desc);
}
private function buildParser() {
// TODO: This is a little hacky beacuse we're using the Arcanist repository
// itself to execute tests with, but it should be OK until we get proper
// isolation for repository-oriented test cases.
$root = dirname(phutil_get_library_root('arcanist'));
- $copy = ArcanistWorkingCopyIdentity::newFromPath($root);
- $repo = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity($copy);
+ $working_copy = ArcanistWorkingCopyIdentity::newFromPath($root);
+ $configuration_manager = new ArcanistConfigurationManager();
+ $configuration_manager->setWorkingCopyIdentity($working_copy);
+ $repo = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
+ $configuration_manager);
return new ArcanistBaseCommitParser($repo);
}
}
diff --git a/src/parser/__tests__/ArcanistBundleTestCase.php b/src/parser/__tests__/ArcanistBundleTestCase.php
index db0b01a1..53685a48 100644
--- a/src/parser/__tests__/ArcanistBundleTestCase.php
+++ b/src/parser/__tests__/ArcanistBundleTestCase.php
@@ -1,908 +1,911 @@
<?php
final class ArcanistBundleTestCase extends ArcanistTestCase {
private function loadResource($name) {
return Filesystem::readFile($this->getResourcePath($name));
}
private function getResourcePath($name) {
return dirname(__FILE__).'/bundle/'.$name;
}
private function loadDiff($old, $new) {
list($err, $stdout) = exec_manual(
'diff --unified=65535 --label %s --label %s -- %s %s',
'file 9999-99-99',
'file 9999-99-99',
$this->getResourcePath($old),
$this->getResourcePath($new));
$this->assertEqual(
1,
$err,
"Expect `diff` to find changes between '{$old}' and '{$new}'.");
return $stdout;
}
private function loadOneChangeBundle($old, $new) {
$diff = $this->loadDiff($old, $new);
return ArcanistBundle::newFromDiff($diff);
}
/**
* Unarchive a saved git repository and apply each commit as though via
* "arc patch", verifying that the resulting tree hash is identical to the
* tree hash produced by the real commit.
*/
public function testGitRepository() {
if (phutil_is_windows()) {
$this->assertSkipped('This test is not supported under Windows.');
}
$archive = dirname(__FILE__).'/bundle.git.tgz';
$fixture = PhutilDirectoryFixture::newFromArchive($archive);
$old_dir = getcwd();
chdir($fixture->getPath());
$caught = null;
try {
$this->runGitRepositoryTests($fixture);
} catch (Exception $ex) {
$caught = $ex;
}
chdir($old_dir);
if ($caught) {
throw $ex;
}
}
private function runGitRepositoryTests(PhutilDirectoryFixture $fixture) {
$patches = dirname(__FILE__).'/patches/';
list($commits) = execx(
'git log --format=%s',
'%H %T %s');
$commits = explode("\n", trim($commits));
// The very first commit doesn't have a meaningful parent, so don't examine
// it.
array_pop($commits);
foreach ($commits as $commit) {
list($commit_hash, $tree_hash, $subject) = explode(' ', $commit, 3);
execx('git reset --hard %s --', $commit_hash);
$fixture_path = $fixture->getPath();
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($fixture_path);
- $repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
- $working_copy);
+ $configuration_manager = new ArcanistConfigurationManager();
+ $configuration_manager->setWorkingCopyIdentity($working_copy);
+ $repository_api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
+ $configuration_manager);
+
$repository_api->setBaseCommitArgumentRules('arc:this');
$diff = $repository_api->getFullGitDiff();
$parser = new ArcanistDiffParser();
$parser->setRepositoryAPI($repository_api);
$changes = $parser->parseDiff($diff);
$this->makeChangeAssertions($commit_hash, $changes);
$bundle = ArcanistBundle::newFromChanges($changes);
execx('git reset --hard %s^ --', $commit_hash);
$patch = $bundle->toGitPatch();
$expect_path = $patches.'/'.$commit_hash.'.gitpatch';
$expect = null;
if (Filesystem::pathExists($expect_path)) {
$expect = Filesystem::readFile($expect_path);
}
if ($patch === $expect) {
$this->assertEqual($expect, $patch);
} else {
Filesystem::writeFile($expect_path.'.real', $patch);
throw new Exception(
"Expected patch and actual patch for {$commit_hash} differ. ".
"Wrote actual patch to '{$expect_path}.real'.");
}
try {
id(new ExecFuture('git apply --index --reject'))
->write($patch)
->resolvex();
} catch (CommandException $ex) {
$temp = new TempFile(substr($commit_hash, 0, 8).'.patch');
$temp->setPreserveFile(true);
Filesystem::writeFile($temp, $patch);
PhutilConsole::getConsole()->writeErr(
"Wrote failing patch to '%s'.\n",
$temp);
throw $ex;
}
execx('git commit -m %s', $subject);
list($result_hash) = execx('git log -n1 --format=%s', '%T');
$result_hash = trim($result_hash);
$this->assertEqual(
$tree_hash,
$result_hash,
"Commit {$commit_hash}: {$subject}");
}
}
private function makeChangeAssertions($commit, array $raw_changes) {
$changes = array();
// Verify that there are no duplicate changes, and rekey the changes on
// affected path because we don't care about the order in which the
// changes appear.
foreach ($raw_changes as $change) {
$this->assertEqual(
true,
empty($changes[$change->getCurrentPath()]),
"Unique Path: ".$change->getCurrentPath());
$changes[$change->getCurrentPath()] = $change;
}
switch ($commit) {
case '1830a13adf764b55743f7edc6066451898d8ffa4':
// "Mark koan2 as +x and edit it."
$this->assertEqual(1, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
$this->assertEqual(
'100644',
idx($c->getOldProperties(), 'unix:filemode'));
$this->assertEqual(
'100755',
idx($c->getNewProperties(), 'unix:filemode'));
break;
case '8ecc728bcc9b482a9a91527ea471b04fc1a025cf':
// "Move 'text' to 'executable' and mark it +x."
$this->assertEqual(2, count($changes));
$c = $changes['executable'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
$this->assertEqual(
'100644',
idx($c->getOldProperties(), 'unix:filemode'));
$this->assertEqual(
'100755',
idx($c->getNewProperties(), 'unix:filemode'));
break;
case '39c8e7dd3914edff087a6214f0cd996ad08e5b3d':
// "Mark koan as +x."
// Primarily a test against a recusive synthetic hunk construction bug.
$this->assertEqual(1, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
$this->assertEqual(
'100644',
idx($c->getOldProperties(), 'unix:filemode'));
$this->assertEqual(
'100755',
idx($c->getNewProperties(), 'unix:filemode'));
break;
case 'c573c25d1a767d270fed504cd993e78aba936338':
// "Copy a koan over text, editing the original koan."
// Git doesn't really do anything meaningful with this.
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
$c = $changes['text'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
break;
case 'd26628e588cf7d16368845b121c6ac6c781e81d0':
// "Copy a koan, modifying both the source and destination."
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_AWAY,
$c->getType());
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
break;
case 'b0c9663ecda5f666f62dad245a3a7549aac5e636':
// "Remove a koan copy."
$this->assertEqual(1, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
break;
case 'b6ecdb3b4801f3028d88ba49940a558360847dbf':
// "Copy a koan and edit the destination."
// Git does not detect this as a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '30d23787e1ecd254c884afbe37afa612f61e3904':
// "Move and edit a koan."
$this->assertEqual(2, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case 'c0ba9bfe3695f95c3f558bc5797eeba421d32483':
// "Remove two koans."
$this->assertEqual(2, count($changes));
$c = $changes['koan3'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
$c = $changes['koan4'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
break;
case '2658fd01d5355abe5d4c7ead3a0e7b4b3449fe77':
// "Multicopy a koan."
$this->assertEqual(3, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MULTICOPY,
$c->getType());
$c = $changes['koan3'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$c = $changes['koan4'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case '1c5fe4e2243bb19d6b3bf15896177b13768e6eb6':
// "Copy a koan."
// Git does not detect this as a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '6d9eb65a2c2b56dee64d72f59554c1cca748dd34':
// "Move a koan."
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case '141452e2a775ee86409e8779dd2eda767b4fe8ab':
// "Add a koan."
$this->assertEqual(1, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '5dec8bf28557f078d1987c4e8cfb53d08310f522':
// "Copy an image, and replace the original."
// `image_2.png` is copied to `image.png` and then replaced.
$this->assertEqual(2, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_AWAY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
'c9ec1b952480da09b393ba672d9b13da',
md5($c->getCurrentFileData()));
break;
case 'fb28468d25a5fdd063aca4ca559454c998a0af51':
// "Multicopy image."
// `image.png` is copied to `image_2.png` and `image_3.png` and then
// deleted. Git detects this as a move and an add.
$this->assertEqual(3, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MULTICOPY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
$c = $changes['image_3.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case 'df340e88d8aba12e8f2b8827f01f0cd9f35eb758':
// "Remove binary image."
// `image_2.png` is deleted.
$this->assertEqual(1, count($changes));
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
break;
case '3f5c6d735e64c25a04f83be48ef184b25b5282f0':
// "Copy binary image."
// `image_2.png` is copied to `image.png`. Git does not detect this as
// a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case 'b454edb3bb29890ee5b3af5ef66ce6a24d15d882':
// "Move binary image."
// `image.png` is moved to `image_2.png`.
$this->assertEqual(2, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case '5de5f3dfda1b7db2eb054e57699f05aaf1f4483e':
// "Add a binary image."
// `image.png` is added.
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case '176a4c2c3fd88b2d598ce41a55d9c3958be9fd2d':
// "Convert \r\n newlines to \n newlines."
case 'a73b28e139296d23ade768f2346038318b331f94':
// "Add text with \r\n newlines."
case '337ccec314075a2bdb4a912ef467d35d04a713e4':
// "Convert \n newlines to \r\n newlines.";
case '6d5e64a4a7a6a036c53b1d087184cb2c70099f2c':
// "Remove tabs."
case '49395994a1a8a06287e40a3b318be4349e8e0288':
// "Add tabs."
case 'a5a53c424f3c2a7e85f6aee35e834c8ec5b3dbe3':
// "Add trailing newline."
case 'd53dc614090c6c7d6d023e170877d7f611f18f5a':
// "Remove trailing newline."
case 'f19fb9fa1385c01b53bdb6d8842dd154e47151ec':
// "Edit a text file."
$this->assertEqual(1, count($changes));
$c = $changes['text'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_TEXT,
$c->getFileType());
break;
case '228d7be4840313ed805c25c15bba0f7b188af3e6':
// "Add a text file."
// This commit is never reached because we skip the 0th commit junk.
$this->assertEqual(true, "This is never reached.");
break;
default:
throw new Exception(
"Commit {$commit} has no change assertions!");
}
}
public function testTrailingContext() {
// Diffs need to generate without extra trailing context, or 'patch' will
// choke on them.
$this->assertEqual(
$this->loadResource('trailing-context.diff'),
$this->loadOneChangeBundle(
'trailing-context.old',
'trailing-context.new')->toUnifiedDiff());
}
public function testDisjointHunks() {
// Diffs need to generate without overlapping hunks.
$this->assertEqual(
$this->loadResource('disjoint-hunks.diff'),
$this->loadOneChangeBundle(
'disjoint-hunks.old',
'disjoint-hunks.new')->toUnifiedDiff());
}
public function testNonlocalTrailingNewline() {
// Diffs without changes near the end of the file should not generate a
// bogus, change-free hunk if the file has no trailing newline.
$this->assertEqual(
$this->loadResource('trailing-newline.diff'),
$this->loadOneChangeBundle(
'trailing-newline.old',
'trailing-newline.new')->toUnifiedDiff());
}
public function testEncodeBase85() {
$data = '';
for ($ii = 0; $ii <= 255; $ii++) {
$data .= chr($ii);
}
for ($ii = 255; $ii >= 0; $ii--) {
$data .= chr($ii);
}
$expect = Filesystem::readFile(dirname(__FILE__).'/base85/expect1.txt');
$expect = trim($expect);
$this->assertEqual(
$expect,
ArcanistBundle::encodeBase85($data));
// This is just a large block of random binary data, it has no special
// significance.
$data =
"\x56\x4c\xb3\x63\xe5\x4a\x9f\x03\xa3\x4c\xdd\x5d\x85\x86\x10".
"\x30\x3f\xc1\x28\x51\xd8\xb2\x1a\xc3\x79\x15\x85\x31\x66\xf9".
"\x8e\xe1\x20\x8f\x12\xa1\x94\x0e\xbf\xb6\x9c\xb5\xc0\x15\x43".
"\x3d\xad\xed\x00\x3c\x16\xfa\x76\x2f\xed\x99\x3a\x78\x3e\xd1".
"\x91\xf8\xb0\xca\xb9\x29\xfe\xd4\x0f\x16\x70\x19\xad\xd9\x42".
"\x15\xb4\x8f\xd6\x8f\x80\x62\xe9\x48\x77\x9f\x38\x6d\x3f\xd6".
"\x0e\x40\x68\x68\x93\xae\x75\x6d\x7f\x75\x9c\x80\x69\x94\x22".
"\x87\xb6\xc0\x62\x6b\xab\x49\xb8\x91\xe9\x96\xbf\x04\xc2\x50".
"\x30\xae\xea\xc1\x70\x8e\x91\xd0\xb6\xec\x56\x14\x78\xd5\x8a".
"\x8c\x52\xd1\x3c\xde\x65\x21\xec\x93\xab\xcf\x7e\xf5\xfd\x6d".
"\x2d\x69\xb9\x2e\xa3\x42\x7b\x4d\xa5\xfb\x28\x6d\x74\xa3\x7b".
"\x3a\xc5\x34\x7c\x63\xa9\xf9\x8e\x34\x14\x42\xb0\xf1\x0e\xe2".
"\xd0\xd2\x04\x81\xff\x62\xd5\xd9\x46\x3b\x36\x88\x8a\x93\x55".
"\x02\x2c\xff\x9f\x48\xd6\x7a\xcb\xbf\x6a\x33\xaa\x6b\x08\x4c".
"\x96\x98\x89\x53\x56\xb4\xb3\x9b\x06\xb1\xa0\x13\x69\xfa\x6a".
"\xa8\x0d\x6a\xda\xb2\x6f\x62\x0b\xa8\xf6\x59\x29\x46\x7d\x04".
"\x44\xeb\x90\x6f\xd7\xc7\xb6\xca\xc5\xeb\xde\x10\x9b\xbd\xf2".
"\x66\x8e\xd0\x0b\xda\x8c\xeb\x90\x73\x73\x33\xe7\x6f\x26\x57".
"\x4e\xfc\x95\xe0\xfc\x62\x93\xa7\x28\xe6\x0c\x46\x73\xdd\x01".
"\xce\x43\x9b\x4e\x16\x74\x5b\x36\x92\x5a\x66\x4c\xe3\x9e\x90".
"\x2d\x9a\x1a\x3d\x69\x39\x67\x04\xd6\xf8\x5f\x45\xee\xbb\xd4".
"\x63\xcf\x8c\x9b\x31\x69\x98\x1a\x98\x57\x4b\xa9\x49\xf6\x1b".
"\x76\x28\xd7\xe3\x8f\x63\x95\x5b\x06\xe2\xa8\x66\x60\xf9\x49".
"\x4e\x40\x53\x32\x9b\x74\x36\xc0\x56\xf4\x33\xec\x83\xd2\x2c".
"\x69\x60\x55\x11\x3b\x4f\xd6\x0a\xf6\x04\x38\x75\xb6\xc2\x82".
"\x4d\xfa\x83\x56\xba\x35\x42\xc3\xcb\xdc\x28\xf4\x69\x48\xa9".
"\xe0\x51\x41\x79\x66\xfe\x61\xd1\xf2\x9f\x7b\xde\xc4\x3e\x8f".
"\x8f\xb6\x9c\x0a\x74\xf8\x71\x03\x37\x37\x30\x8d\x2a\x6a\xc9".
"\x51\xa1\xe2\x34\xe5\x42\xdb\x4f\x61\x4e\x16\xfc\x23\x72\x12".
"\x46\x53\x12\x82\x3e\x44\x63\x23\x82\xaa\xab\x7e\x8d\x70\x66".
"\xf1\x94\x86\x02\xc5\x3e\x9c\x79\x17\x1e\x9f\x13\x89\x3d\x25".
"\x45\xc9\x3b\x1e\xa0\x1a\x03\x20\x1c\x81\x6b\xfc\xb5\xc9\xe2".
"\xda\xb1\x87\x34\xa0\xb2\x72\x36\x68\x12\x05\x53\x7c\x68\x6b".
"\x1e\x2a\x56\x2a\x7e\x7f\xd0\x9c\x13\xa9\xb2\x4c\xe6\x8a\x65".
"\xd7\x67\xad\xf3\xf3\x2b\x9c\xe8\x10\x07\x8a\xe2\x20\x67\xe4".
"\x51\x47\xc1\x22\x91\x05\x22\x39\x1a\xef\x54\xd2\x8a\x88\x55".
"\x3f\x83\xba\x73\xd4\x95\xc7\xb8\xa2\xfd\x4d\x4e\x5d\xff\xdd".
"\xaf\x1a\xc2\x7e\xb5\xfa\x86\x5f\x93\x38\x5d\xca\x9a\x5a\x7e".
"\xb7\x47\xd5\x5c\x6b\xf3\x32\x03\x11\x44\xe9\x49\x12\x40\x82".
"\x67\x7d\x2a\x5a\x61\x81\xbd\x24\xaa\xd7\x7c\xc9\xcf\xaf\xb0".
"\x3e\xb0\x43\xcd\xce\x21\xe4\x1b\x5a\xd6\x40\xf5\x0e\x23\xef".
"\x70\xf4\xc6\xd2\xd7\x36\xd7\x20\xda\x8d\x39\x46\xea\xfc\x78".
"\x55\xa2\x02\xd6\x77\x21\xc8\x97\x1e\xdf\x45\xde\x93\xa7\x74".
"\xd8\x59\x10\x24\x8a\xe8\xcd\xe9\x00\xb5\x4e\xe6\x49\xb0\xde".
"\x14\x1a\x5d\xdd\x38\x47\xb0\xc7\x1e\xec\x7c\x76\xc9\x21\x3c".
"\x3a\x85\x4f\x71\x97\xed\x4a\x94\x2c\x51\x48\x9c\x43\x90\x70".
"\xe9\x0e\x84\x55\xd2\xa4\x48\xfa\xfd\x54\x12\x11\xb9\x32\xfc".
"\x1d\x66\xe7\x42\xe3\x5e\x65\xf4\x3d\xea\x1a\x53\xe3\x7b\x4b".
"\xee\xdb\x74\xce\x30\xd3\x04\xcb\xda\xa4\xdd\xad\x98\x3a\x76".
"\xe8\xba\x1b\x03\x53\xed\x46\x5d\xef\xd4\x34\xc2\x8d\xef\xae".
"\x51\x35\x0f\x4d\x40\xaa\x3a\xdb\x50\x1a\xbe\x5f\x8b\xb8\x24".
"\x40\x19\x8f\x8a\x6b\x44\x4f\x9b\xe0\xf4\x9c\x4b\xc4\x23\x37".
"\xf0\xb3\xe1\x58\x9d\x0e\xd9\xa9\xf7\x3e\x86\x43\x9b\x5b\x90".
"\x3c\xc0\x20\xa0\xc5\x86\x4f\xc6\xcb\xb5\xcb\xd4\x88\xc6\x72".
"\x57\xa7\x57\x2c\x34\x26\x91\x44\x15\xa8\xf4\x88\xca\x74\x56".
"\x9e\x12\x6c\xdf\x52\xef\xc0\xb4\x5c\x16\xe8\xaa\xf7\xb6\xf3".
"\x7c\xda\xcd\x42\xf9\x1c\x40\x88\x44\x68\x4f\x1b\x5a\x7b\x8f".
"\xc3\x47\x48\xd3\xf3\xe5\xf5\x66\x35\x48\xbe\x64\xdf\xfe\x35".
"\xf1\xc3\xe4\xa8\xfc\x86\xfb\x69\x20\xc9\xf4\x16\x96\xc1\x7a".
"\x51\x14\x77\xa4\x6e\x13\xe8\x59\x35\x24\xf1\xe5\xfe\xe9\x98".
"\x0d\xd1\xe8\xce\x9c\x7f\xf8\x3b\x79\x39\x3a\x1d\xa3\x77\xef".
"\x4f\x4b\x59\x73\x03\xb3\xfe\xae\x70\x2a\x3a\xf0\x79\x9d\x7e".
"\x9b\xaa\xb1\x18\xf9\x43\x69\xf3\x55\x46\xad\x38\xa2\xf1\xcb".
"\xce\x37\xa9\x88\x20\x38\xea\x19\x29\x95\x8c\x75\x06\x9d\x1d".
"\x9e\xf2\xb7\x64\x98\x21\x36\x90\x92\xf8\xb8\x89\x1e\x5c\x5d".
"\x09\x3b\x52\xc5\x6a\x87\x7e\x46\xca\x8c\xdf\xe7\xca\xa9\x7b".
"\x11\x63\x0f\x9e\x42\x9a\x3e\xe0\x8b\x80\x9e\x91\x76\x88\x9a".
"\xa1\xe2\x96\xae\xfb\x18\x39\xdc\x92\x99\x34\xfd\x98\x20\xa8".
"\x89\x61\x2c\x26\xe0\xb8\x83\xa7\xe7\x50\x42\x8f\xfc\x36\x66".
"\x6b\x25\xc5\x6d\xb4\x31\xe1\x4d\x0f\x2e\xf8\x44\xe2\xb6\x6a".
"\x6d\xfe\x83\x9e\x2c\x07\x2f\x15\x41\xf3\xe7\xa6\x18\x2b\x84".
"\x7e\xeb\x43\xcc\xbb\xdb\xa9\x54\x5c\xbc\x59\x6a\xdc\x26\x2a".
"\xf4\x59\xa7\x75\xa4\xac\xed\x73\x8f\x16\x43\x0d\x97\x10\x2c".
"\x70\xef\x9e\xb2\xc9\xdf\xe6\xa7\x9b\x08\x79\xa3\xf7\x99\xf5".
"\x59\xe4\xd5\x89\x10\xe5\xc9\xf7\xe7\x29\x72\x06\xc6\x54\xc3".
"\xcd\xd0\xff\x69\xf8\xdf\x19\xf2\x66\x1c\x69\x40\xbc\x97\xf1".
"\x49\x5e\x78\x62\x52\x46\x7f\xcf\x44\x50\x8b\x5f\xe7\xa8\xeb".
"\xd5\x84\x24\x81\xc0\x2c\x65\xf7\x95\xbd\xf2\x8e\x43\xfb\x6a".
"\x49\x3c\x6a\xe5\x2a\x39\xf0\xfa\x89\x59\x5f\x39\x75\xb4\x6f".
"\x04\xf1\xe0\x2c\xcd\x77\x34\xec\x6b\x45\x16\xe3\x18\x24\x05".
"\xb9\x68\xc1\x4e\x71\x4b\xff\x88\x18\xea\x0d\x56\x49\x55\xdf".
"\xe5\xb0\x59\xdb\x74\x9e\x0b\x38\x03\x9f\x10\x6f\xd9\x34\x07".
"\x44\x29\x08\xb1\xd4\x77\xc6\x84\x0d\xbb\xb5\xd5\x09\x05\x19".
"\x01\x62\x29\x45\x52\x1d\xc6\x4f\x25\x78\x7e\xbc\xae\x07\xb3".
"\xd4\xe0\x19\x91\x03\xd6\x8d\x2f\x00\xc9\xb2\x66\x3b\x4e\x3d".
"\x75\xf7\x23\x9a\x3e\xa4\xd5\x7f\x75\x47\xd0\xbc\xc3\xc8\x2a".
"\xdc\x85\x09\x6c\x0c\x90\x38\xd8\xef\xcf\xf4\x7a\x1b\xc7\x76".
"\xe0\xdb\x81\xa8\x1b\x2b\x8d\xd4\x36\x90\x76\xde\x8a\x90\xc8".
"\x5b\x05\x00\xeb\xb3\x20\xce\x6e\x5c\xb9\x35\x3d\x95\x3a\x79".
"\x4a\x60\xeb\x23\x11\xfb\x90\x2d\xf6\xb7\x05\x4a\x43\x41\x79".
"\x51\xaa\xe6\x90\x0a\x71\x87\x80\xbe\xb0\x89\x0f\xd3\x84\x19".
"\xce\x6c\xf9\xbb\x1b\x15\x4d\x0f\x33\x65\xf7\x9e\x3a\xd9\x8c".
"\x02\x43\xcf\xdf\xb2\x60\xc1\x4c\xe9\xa5\x3c\xaf\xfa\x41\x2d".
"\xb9\x1f\x45\x32\xcb\x39\x2f\x94\xae\x44\x6d\x69\xc1\xc9\x57".
"\x8c\xe5\xf4\xa4\x3a\xb6\x70\x61\xf9\xbb\x41\xdc\x78\xf0\xf7".
"\xbf\xa8\x8e\xe3\x77\x51\xce\x25\x2f\xdf\x27\x6b\x07\x30\x9f".
"\xce\xdb\x59\x58\xaa\xb2\x2e\xdc\x90\x92\x82\x55\xfe\x25\x36".
"\x49\x7f\x6d\x2d\x39\x51\xef\x3d\xc8\xa3\x87\x0b\xe7\xf2\xac".
"\x90\xa0\x1d\xd8\xc7\xea\x93\x53\x3b\x21\x84\x2e\x52\x6c\xfb".
"\x4f\x31\xda\xd1\xea\x45\x3e\xdc\xeb\x52\x81\x8c\x2b\xf4\x2a".
"\xbc\x01\xc4\xe7\x68\x36\x9c\xd5\x2d\xc1\x61\xcb\x9a\x5f\x18".
"\x00\x6a\xc8\x9a\x4e\xfd\x31\x5b\xce\x90\x4e\x45\xff\x7f\xea".
"\xb2\x26\xad\xc1\x3a\x21\xa9\xe8\x7c\x14\xae\x81\x1e\xbe\xa3".
"\x6d\xda\x92\x1b\xeb\xf2\x69\x76\x3e\xf1\x2b\xf7\x1a\x45\xd5".
"\xb3\x81\xb1\xbe\x80\x7f\x24\xba\x0e\xd5\x68\x34\x3f\x1a\x29".
"\x15\x0e\xc2\x26\x62\x0c\xaa\xa9\x20\x4c\x61\x65\x49\x07\xbe".
"\x69\xf4\xc9\xec\x2f\x1c\xfa\x59\x2e\x72\xc0\x17\xc5\x4c\xfa".
"\xba\x2f\x64\xab\xa9\xb4\xcb\xdc\xcb\x25\x5f\xcf\x0c\x87\xcc".
"\xf0\x36\x2b\xce\x81\x5a\x22\x85\xa0\x50\x50\x97\x8e\xda\x36".
"\x80\x74\xb5\x1e\x02\x3f\xd7\xc8\x29\x11\xeb\x1d\x3d\x74\x9f".
"\x26\x1a\xa4\x3d\xf9\x0e\xf0\x2d\x5c\xa9\x43\xbf\x51\x6c\x8d".
"\xe6\x78\xe0\x67\x57\xf0\xc8\x0e\x97\x9c\x57\x23\x30\xac\x63".
"\xdf\x46\x98\xa4\xaf\x4e\xa7\xe5\xac\x31\xbd\xeb\x6a\xa0\xb0".
"\xe4\x94\x7e\x51\xf6\x89\x81\x3e\xab\x4f\x64\xb7\xc5\x51\x71".
"\xcd\x74\x02\xa9\x02\x99\x5c\xab\x0e\x14\x47\x3b\x04\xc1\x9b".
"\x59\x1a\x93\x92\x4c\x71\x20\x5f\x6e\xd3\xf3\xa7\x47\x1b\x39".
"\x3e\x73\x69\xe2\xec\xcb\x52\xb3\x5c\x7a\x95\x25\x3f\x16\x98".
"\x60\xa8\xa2\x5d\xc4\x5a\x67\xe4\x11\x06\x06\xf9\x7a\xb4\x14".
"\xe0\xbc\x7b\x13\x1d\x0f\xf2\xca\x0b\xd4\xaa\x71\x35\x3e\xd6".
"\x2e\x2e\x5d\x7b\x15\xc9\x23\x1a\xa9\x24\x31\x48\xd4\xcf\x4a".
"\xf4\x32\x17\x9b\x1d\x4b\xfe\x49\x69\xd6\xc0\x8f\xb9\xdb\x72".
"\x52\x2c\xe8\xf3\xc4\xfc\x46\xf5\xb8\x1b\x05\x06\xcf\xcc\x23".
"\x34\xbf\x25\x6a\xea\x3c\xc7\x64\xd4\xd5\xb3\x67\xed\x24\x27".
"\xd3\x67\xc1\xbd\x9f\x7b\x7d\x19\x04\x5c\xd1\x96\x7e\xa5\xc7".
"\xbb\xb2\x84\x68\x98\x38\x11\x90\xfb\x62\x15\xfd\xe6\xb7\x24".
"\x77\xb2\x78\xc7\x73\x91\xc9\x60\x1d\x91\x6d\x04\x2b\x41\xe9".
"\xc9\xfa\xe4\x98\x54\x83\x9a\x6e\x76\x8c\x21\xf9\x91\x38\x1f".
"\xdc\xfe\x13\x09\x30\xd7\x53\x63\x62\xba\xe3\x2c\x70\xd5\xfc".
"\x78\x35\x36\x79\x5d\xb6\x0e\x35\x3d\x46\x87\xfb\xf5\x64\x1f".
"\x3e\xfd\x2f\x1c\xbb\xed\x95\x2d\xd6\x63\xdc\xa7\x6a\x39\x8f".
"\xbd\xcb\x79\x95\xe9\x45\xbf\xe4\x3e\x05\x55\x00\xdb\x33\x28".
"\x3a\x6c\xe2\x35\xbb\xac\x70\x52\x2b\xac\x4e\x11\x44\x58\x16".
"\x21\xb4\xae\x0d\x6a\xb9\xdc\x85\x5d\x90\x11\x26\x85\xdb\xc3".
"\xf0\x38\x6f\x8a\xff\x12\xf0\xc9\x9e\xf0\xfc\xae\x94\x11\x4d".
"\xce\x96\x29\x09\x6c\xf4\x2a\x6c\xda\x1e\x4c\x4a\xa2\x96\x5a".
"\xef\xc6\x38\x5c\x60\xa2\x28\x13\x58\x73\x96\xde\x59\x2a\x57".
"\x64\x6c\x14\x94\x8a\x2e\x8e\x21\x3f\xa2\x43\xde\xf6\x2d\x23".
"\x74\x5c\xbd\x7a\x10\xdb\x17\xa8\x93\xd0\x74\x86\x9d\x33\x07".
"\x48\xee\xac\x18\x6d\x64\x61\x7b\x61\x2b\xa4\xa2\xab\x99\x59".
"\xbe\x19\xd7\x19\x41\x1e\x61\x87\xad\x40\x5b\x69\x8c\x32\xf5".
"\xb6\x49\xbe\x1f\xad\xd8\x0f\x3e\xd9\x62\xac\x3a\x76\xde\x32".
"\xa3\xb2\x41\x95\xad\x17\x23\xab\xa1\x37\x9c\xab\x73\x79\x70".
"\xd6\x66\x0d\x6e\x4d\x8b\xa0\xac\xe3\x44\x1e\x0a\xee\xf0\x74".
"\x64\xd8\x44\xd1\x6c\xa6\xd5\x36\x2e\xd9\x55\x6e\x90\x63\xb7".
"\xf7\x8e\xc6\x28\xa3\x40\x00\x60\x9a\x3c\xfe\xff\x03\x30\x11".
"\x18\x92\x2f\x5b\x23\xe1\x4e\x99\xe4\x82\xc9\x51\xe2\x15\x6a".
"\x76\x5c\x67\xae\xa3\xa2\x9c\x85\x51\xe0\x44\x89\x63\xa5\x71".
"\x99\xbc\x2d\x9c\xab\x9a\xfb\x20\x37\x58\xd6\x2d\x8b\x7d\x42".
"\x13\x35\x44\x4c\x11\x97\x66\x27\x17\xac\x44\xe8\x6a\x03\x78".
"\xa2\x88\xc6\x36\x71\x5a\x5a\x5a\x72\xa3\xe9\x72\x0c\x91\x31".
"\xfc\xae\x7b\xa0\x75\x21\x0a\xc1\x4b\x95\xcb\xe3\xc2\xee\x03".
"\x0f\xb8\xb2\x51\xc7\xc8\x9c\x8d\x6d\x3a\xe7\x4e\x2c\xaa\xeb".
"\x5e\x49\x93\xe0\x8f\xa1\x54\x93\xe7\x7c\x5d\x31\xc7\x05\x00".
"\x28\x14\x57\x47\xb3\x05\x2d\x17\x92\x28\x45\xee\x85\x3a\x59".
"\xb6\xa6\x04\xc0\x5c\x07\x1f\xe6\x5b\x36\x53\x62\x82\x64\xd5".
"\xb6\xf2\xf5\x67\x19\x11\xee\xd2\x70\xc5\x14\x63\xc1\x75\xe1".
"\x24\xe5\x01\x59\x52\x7c\x88\x17\xb4\xe0\x15\xe9\x12\x05\xcd".
"\x88\x7a\xd5\xea\x45\xc3\xbb\x65\xd4\xdd\x0d\xde\x36\x94\x98".
"\x0d\x2c\xfb\x3c\x2f\x69\xd0\x28\xe2\x85\xd9\x27\xf3\x7a\xad".
"\x50\x68\x96\x54\x5e\xeb\xbc\x2a\x74\xde\xf3\x4e\x8b\x27\x0a".
"\xcf\x4c\x60\x40\xe8\xc5\x72\xab\x8c\xfd\xe9\xab\xff\x51\xe5".
"\xd6\xea\x9e\x34\x73\xe1\xe6\xf8\x5b\xb1\x10\xf0\xf9\x2d\x23".
"\x0e\xfe\xe5\xf4\x8d\xb6\x6d\x37\x14\xed\x54\x97\x92\x5c\x68".
"\x40\x88\xf1\x43\x29\xef\x5e\x96\x77\xa2\xe8\x3c\xae\x7f\xb1".
"\x99\x17\xa7\x0c\x6f\xe2\x43\x32\x9b\x14\x43\xf2\x15\x6b\x13".
"\x10\x68\x56\x0b\xaa\x06\x2e\xc0\xf8\xde\x9e\x54\x9d\xba\xff".
"\x76\x26\x6d\x5e\x9e\x88\x3a\x2b\x9b\x20\x43\xb9\x1a\x0e\x58".
"\x65\xec\xdb\x9e\x97\xb8\xfb\x03\x6c\xb0\x7f\xa2\xf1\xf4\x27".
"\x24\x21\x47\x51\x21\x40\x45\x28\x71\xf7\xa1\x6b\xbe\x0e\xc8".
"\x3f\x9b\xda\x62\x9d\x73\xf7\x5f\x70\x6c\xba\x1e\xeb\x16\x5c".
"\x2e\x44\x0a\x22\x02\x6c\xbe\xb9\x69\x93\xfd\xa5\x33\x26\x64".
"\x24\x6c\xc2\x3d\x2f\xf3\xd1\x97\xde\x60\x43\x1c\x0d\x1b\x94".
"\xb3\x48\x45\x7c\xd5\xd0\x71\x4d\xad\xbf\xa4\x0a\x22\x27\x04".
"\x38\x84\x19\x66\x63\xf0\xf3\xfc\xb0\xf3\x1d\xea\xba\xb9\xe4".
"\xe5\x80\xed\xe3\xf1\x78\x24\xc3\x25\x27\x71\x81\xc2\xec\x54".
"\xed\xcc\x63\xf7\x39\xcd\x83\xdf\x32\x88\xc0\x3b\xd4\x62\xb8".
"\xea\x34\xd8\xcf\xbc\x3a\x89\x38\x64\x60\x44\xde\xb6\x76\x59".
"\xb1\x95\x6a\x26\x08\xf0\xf4\x71\x25\x8b\xf8\x81\xdd\x0d\x2f".
"\x8c\xe2\x70\xc2\x96\xc2\xd8\x9b\xe4\x3f\xec\x8b\xfd\xbd\xc9".
"\x36\x33\xb7\xbc\x59\x37\x19\x09\x30\x5e\xef\x67\xae\x67\x48".
"\x72\x0b\xf4\x2a\x82\xff\xcb\xd7\xd9\x9d\x6d\x7c\xa6\x20\x42".
"\x50\x2b\x0a\x2f\x45\x99\x5b\x76\x6d\x99\x39\xa9\xb6\x32\x06".
"\x11\xf8\x19\xd1\x3f\xc0\xd6\x1f\x67\xfa\xd5\xae\x7a\x71\x8c".
"\xbc\x3d\xb4\x5f\x5c\x81\x7c\xa1\x39\x70\x0a\x17\x24\xb7\x22".
"\x86\x50\xd8\x1f\xc8\x6c\x59\x9a\xdc\xf0\x71\x01\xda\xd8\x53".
"\x98\x1c\x73\x36\xf1\x09\x86\xc9\xa7\x26\x25\xc0\x03\x3e\x13".
"\x4e\x29\xeb\xf0\x8d\xe3\x38\x03\x54\xee\x37\xfb\x51\x2e\xb4".
"\xf6\x12\x1f\xb2\x8c\x66\x75\x00\x30\x5b\xef\x59\xf9\x63\xa9".
"\x74\x07\x91\xe4\x9c\xb7\xc9\x89\xd9\xa9\x51\x93\xcb\xb1\xa7".
"\x64\x08\x79\x8f\xb4\x6d\x09\xd7\xc5\xbf\x0a\xdb\x50\xe0\x1c".
"\x83\xca\xf8\xcf\xa7\x81\xbb\x0b\xe6\xcf\x1b\x0e\x0a\xe0\xcd".
"\x68\xe2\xde\xc4\x2d\xba\x55\xc7\xc7\x1e\x6c\x5e\xca\x9b\x20".
"\x75\x96\x94\x92\x84\xec\xf5\x22\x25\x78\x67\xcd\xbe\x01\xfe".
"\x53\xa5\xcc\x6a\x40\x33\x83\xa4\x7a\x44\x93\x0b\xf9\x4c\xb2".
"\x95\xb6\x7e\x4b\xa4\xc8\x86\xfe\x8a\xf1\x77\x40\x56\x13\xc1".
"\x31\x2c\x8c\x4a\xa8\x89\x61\x0c\x39\x33\x78\x8c\xd5\x50\x3b".
"\x89\xc3\xd3\x80\x1c\xa7\xb6\x36\xc2\x00\x8d\x0a\x7f\xcc\xd3".
"\x20\x74\x60\x70\x36\x7d\xda\xdc\xc4\x49\x04\xf0\xe6\x6c\xd1".
"\xbe\xcb\xfb\xf1\xa2\xd6\xd4\xe4\x97\x3f\x35\x09\x5b\xda\x06".
"\x6b\x6d\x86\x53\x23\x0c\x26\x51\x2a\x15\xaa\xe2\x73\xfb\xc7".
"\x41\x54\xdc\x5d\x99\x0b\x0a\x1e\xd4\xdb\x70\xa3\x8e\xfd\x5b".
"\xf0\xa8\x3e\x9b\xff\x57\x98\xbc\xd9\x2a\x56\xd3\x19\xf9\x0b".
"\xd9\x67\x0f\x10\x9c\x23\xe5\x6b\x12\xc6\xb6\x4b\xd1\x0c\xe9".
"\x45\x36\xdf\x54\x6f\xcc\xfe\xb5\xcc\xb9\xfe\xde\xc8\xb5\xc9".
"\x04\x59\x61\x75\x1e\x72\x37\x54\xfd\xc6\xc3\x7e\x74\xae\x55".
"\x31\x6a\xbc\x8a\xd8\x45\x91\xe2\x8d\x20\x97\x71\xe7\x55\xd6".
"\x8a\xb8\x82\x2a\x27\x4f\xdc\x53\x89\x28\xf7\x3a\xfe\x07\xef".
"\x60\xb2\x32\x7c\xbc\x13\xc4\x3d\xda\xd7\xfb\xb8\x61\x7d\x69".
"\xae\x0e\x9a\x71\xd6\x00\x26\x97\xff\xdb\xe6\xbe\x45\x7a\xb5".
"\x00\x31\xfd\x70\xcc\xd7\x34\x88\xe4\x05\x61\xf5\x72\x1d\x14".
"\xf0\x7e\x90\xdb\x0e\xc7\xda\xd4\xf3\x99\xd4\x60\xd9\xa7\xc8".
"\x5b\x33\x34\xb5\x23\x74\x2c\x5f\x6b\x56\x95\x9c\x1b\x2a\xac".
"\xf9\xfe\x46\xc3\xf1\x9b\x24\x7e\x4b\xca\x25\x58\x41\x10\x63".
"\xe8\xe7\x68\xda\xcc\xb6\x4d\x5b\x8f\xc9\xa9\x31\xeb\x5c\x2a".
"\xcf\x9d\x89\xd5\x51\x93\x80\x30\xf4\xc9\x2c\x8c\xb8\x8c\x62".
"\xd6\x33\xbd\x95\x9f\xfa\x19\xf2\x48\x28\x09\x73\xc9\x53\x61".
"\x94\x3a\x62\x68\x6c\xc6\xd6\x0a\xb4\xae\x27\x96\xfb\x29\xd7".
"\x46\x67\x11\x7a\xe8\x3a\x9a\x3f\xf4\x9a\x75\xed\x24\x67\x45".
"\x79\xdc\x8b\x19\xf2\xef\x57\xaa\xc7\x84\xff\x9d\x2d\xc3\xa8".
"\x85\x54\xb7\x9d\xe1\xd6\x2b\xe9\x31\x9d\x6c\xb8\x4e\x76\x50".
"\x80\x44\x46\x8f\x5e\x7e\x20\xaa\xa0\x8a\x36\x6b\xef\xd1\x75".
"\xf8\x3f\x20\xdd\x09\x73\xbf\xa5\xf7\xb4\x87\xb2\x44\xc0\x0f".
"\x10\xc0\x95\x2e\x8a\x42\xfa\xc3\x49\x17\xb9\xb5\x1a\xc3\x80".
"\x93\x0c\xd8\xe3\xcd\xa4\x38\x61\x7a\x22\x73\x8e\x32\x8f\x55".
"\x9c\x91\x08\xd9\x65\xa9\x02\x28\xc6\x59\xc8\x51\x32\x20\x48".
"\xea\x2c\xae\x0e\xa6\x35\x5b\xe2\x63\xf9\xf2\x9d\x5f\xe3\x45".
"\xdc\x41\xba\xfb\x40\xcc\x8d\xde\x6c\x3d\x50\x97\x9d\x83\xa0".
"\xda\x41\x61\xba\xaf\xf8\x74\xd2\x21\x7b\x09\xcc\x83\xe1\x08".
"\x01\x04\x42\xce\xcb\xec\x1d\x6b\xb7\x6f\x0f\x4b\xd4\x53\x90".
"\x55\x3b\xcf\x9f\x93\xb8\xad\xce\x5f\x13\x83\xb3\x89\x6f\x5a".
"\x1b\xa4\xf5\x95\x4b\xb4\x22\x22\x1d\x35\xaa\xfa\xc7\x14\x8c".
"\xcd\x50\x66\x14\x47\xff\x67\xb2\xf8\x12\x09\xb3\x8a\xe5\x7d".
"\xb8\xc9\xe4\x89\xf7\xa4\xb5\x70\xfa\x2d\xeb\x95\x89\xec\xbb".
"\x49\x59\xd2\xc1\x6d\x0e\x06\xe4\x5e\xd5\x13\x13\x0d\x72\x6e".
"\xf0\x6d\xa9\xd5\xe7\x54\x68\x35\xcd\xd0\xd5\xa6\xe5\xb2\xe4".
"\xb1\x19\xe4\xf1\xe3\x8a\x56\x4c\x3b\x3d\xb8\x03\xfe\x22\x2f".
"\xc6\xdc\x88\x7b\xca\x5c\xc6\xdd\x17\x34\x08\x22\xf0\x17\x61".
"\x0e\x60\x9c\xb4\x27\x57\x30\x6e\xb8\x4f\xdd\x25\x7b\xef\x9e".
"\x8e\x88\x6b\xd8\x10\x23\xc2\x44\x53\x73\x64\x8f\x40\x22\xe1".
"\xe8\xa2\xb0\x3f\x8a\x07\x66\xcd\x64\x4f\x9c\x1e\x89\x76\x04".
"\x6d\xab\xc2\xbb\x16\x85\x80\x01\xa5\xb1\xe2\x12\x04\x2e\x39".
"\x87\x8c\xee\xbc\xfb\x07\x6d\x03\x4c\x3a\xa5\x7b\x95\xd9\xd7".
"\xd6\xee\x2b\xe9\xcb\xe6\xec\xa8\x84\x6a\x42\xf9\xb2\x25\xc8".
"\xf3\x6a\xaa\x34\x3b\xd9\x72\xd9\x70\x81\x3b\xd4\x5e\x66\x97".
"\x1b\xe6\x2b\x88\x71\x82\xa3\x8a\x98\xb0\x16\xd9\xbb\x97\x8b".
"\x57\x79\x41\x56\x6e\xc2\x8f\xdf\xfa\x5b\xc7\x68\x5b\xb8\x09".
"\x41\x31\x7c\x19\xe1\x95\x2e\x05\x4c\xac\x38\x81\xda\xb3\x8b".
"\x3e\x1c\x79\x9a\x31\xac\x3e\x3d\x6d\xab\xf3\x5a\x5e\xc7\x6e".
"\x8e\x39\xcd\x7b\x6f\x62\xee\xb9\x73\xdd\x82\x42\x6f\x09\xe4".
"\xc3\xae\x92\xe8\x18\x99\xa0\x5e\xa2\x12\xf4\xe2\xe0\xe6\x95".
"\x58\x3a\x45\xad\xfe\x23\x79\x5f\x82\xce\x95\x88\x73\xeb\x46".
"\xc8\x00\xac\xc3\x2a\xdc\x7e\xab\x9b\xf8\xbb\x46\x5c\xa8\x46".
"\xbc\xfd\x99\xae\x4c\xa7\x77\xeb\x7c\x58\xbf\xbb\x52\x68\x62".
"\x3d\x0b\x79\x64\x38\x65\xa7\xcb\x7b\xe9\xb2\x33\xb5\x59\x52".
"\x7b\x17\xb4\x02\x2b\x07\x0d\x3a\x11\x57\x92\xa5\x22\x2b\xbc".
"\xe6\x97\x05\x12\x05\xe7\x91\xe3\xfa\xae\x15\xbe\x20\xe5\x5c".
"\x71\x24\x80\x85\xc9\x66\xc1\x53\x5c\x8f\x08\xd4\x52\xe1\x10".
"\xb6\xd6\x20\x08\x01\x79\x33\x9f\x1b\xbd\xa0\xab\x7c\xb1\xd9".
"\xdc\xca\x44\x22\x49\xb7\xb7\x3d\x84\xac\x92\xf4\xfa\x0a\xc9".
"\xc5\xb2\x42\x2b\x9a\x63\xbb\x8a\x82\x04\x2f\xf7\xe9\x30\x05".
"\x67\x32\xd1\x41\x1a\x69\x6e\xb9\xf8\x5f\x6d\xb7\xe5\x4e\x85".
"\x21\xfa\x16\x8a\x44\xfd\xf6\xd9\xa2\x5f\x68\x2b\xf3\xe2\x3c".
"\x8a\x69\xd2\xc1\x38\xed\x83\xef\x0d\x53\x86\x93\x32\x23\xc6".
"\x14\x0c\xb0\xb6\x6e\x77\xa4\x20\x0f\xb1\x6e\xe2\xce\xca\x6f".
"\x93\x1c\x3a\x8f\xd0\xd2\x5a\x6e\x30\xd6\x8e\x5f\x4b\xa5\xef".
"\xa9\x62\xeb\x28\xa0\x5e\x3f\xc1\xbc\x0a\x68\xab\xd7\xfa\xa2".
"\xb7\x8f\x12\xb0\x99\xbc\x93\x20\xb8\x95\x8d\xca\xc7\xa7\xd9".
"\x2e\x19\xac\x06\xb9\x4e\x56\x8e\x74\xef\x2a\x04\xd8\x75\x04".
"\x38\x2a\xc7\xa0\xa4\x89\xf3\xa4\x8a\xd4\x2c\x2c\x58\x6f\x00".
"\x03\x23\xb8\xaf\x02\x48\x7d\x50\x46\x6f\x5a\x08\x41\xe3\x56".
"\x6d\xcb\xe2\x4f\xea\x8e\xab\x74\xcd\xf9\xef\xcf\xf9\x1e\xf1".
"\xf8\xb9\x6c\xaa\x3b\x37\xd1\x21\x42\x67\xec\xd6\x44\x55\x33".
"\xe8\x1d\xa4\x18\xf3\x73\x82\xb4\x50\x59\xc2\x34\x36\x05\xeb";
$expect = Filesystem::readFile(dirname(__FILE__).'/base85/expect2.txt');
$expect = trim($expect);
$this->assertEqual(
$expect,
ArcanistBundle::encodeBase85($data));
}
}
diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php
index 7b109f96..3c5d8340 100644
--- a/src/repository/api/ArcanistGitAPI.php
+++ b/src/repository/api/ArcanistGitAPI.php
@@ -1,1090 +1,1090 @@
<?php
/**
* Interfaces with Git working copies.
*
* @group workingcopy
*/
final class ArcanistGitAPI extends ArcanistRepositoryAPI {
private $repositoryHasNoCommits = false;
const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16;
/**
* For the repository's initial commit, 'git diff HEAD^' and similar do
* not work. Using this instead does work; it is the hash of the empty tree.
*/
const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
public static function newHookAPI($root) {
return new ArcanistGitAPI($root);
}
protected function buildLocalFuture(array $argv) {
$argv[0] = 'git '.$argv[0];
$future = newv('ExecFuture', $argv);
$future->setCWD($this->getPath());
return $future;
}
public function execPassthru($pattern /* , ... */) {
$args = func_get_args();
static $git = null;
if ($git === null) {
if (phutil_is_windows()) {
// NOTE: On Windows, phutil_passthru() uses 'bypass_shell' because
// everything goes to hell if we don't. We must provide an absolute
// path to Git for this to work properly.
$git = Filesystem::resolveBinary('git');
$git = csprintf('%s', $git);
} else {
$git = 'git';
}
}
$args[0] = $git.' '.$args[0];
return call_user_func_array('phutil_passthru', $args);
}
public function getSourceControlSystemName() {
return 'git';
}
public function getMetadataPath() {
static $path = null;
if ($path === null) {
list($stdout) = $this->execxLocal('rev-parse --git-dir');
$path = rtrim($stdout, "\n");
// the output of git rev-parse --git-dir is an absolute path, unless
// the cwd is the root of the repository, in which case it uses the
// relative path of .git. If we get this relative path, turn it into
// an absolute path.
if ($path === '.git') {
$path = $this->getPath('.git');
}
}
return $path;
}
public function getHasCommits() {
return !$this->repositoryHasNoCommits;
}
public function getLocalCommitInformation() {
if ($this->repositoryHasNoCommits) {
// Zero commits.
throw new Exception(
"You can't get local commit information for a repository with no ".
"commits.");
} else if ($this->getBaseCommit() == self::GIT_MAGIC_ROOT_COMMIT) {
// One commit.
$against = 'HEAD';
} else {
// 2..N commits. We include commits reachable from HEAD which are
// not reachable from the relative commit; this is consistent with
// user expectations even though it is not actually the diff range.
// Particularly:
//
// |
// D <----- master branch
// |
// C Y <- feature branch
// | /|
// B X
// | /
// A
// |
//
// If "A, B, C, D" are master, and the user is at Y, when they run
// "arc diff B" they want (and get) a diff of B vs Y, but they think about
// this as being the commits X and Y. If we log "B..Y", we only show
// Y. With "Y --not B", we show X and Y.
$against = csprintf('%s --not %s', 'HEAD', $this->getBaseCommit());
}
// NOTE: Windows escaping of "%" symbols apparently is inherently broken;
// when passed throuhgh escapeshellarg() they are replaced with spaces.
// TODO: Learn how cmd.exe works and find some clever workaround?
// NOTE: If we use "%x00", output is truncated in Windows.
list($info) = $this->execxLocal(
phutil_is_windows()
? 'log %C --format=%C --'
: 'log %C --format=%s --',
$against,
// NOTE: "%B" is somewhat new, use "%s%n%n%b" instead.
'%H%x01%T%x01%P%x01%at%x01%an%x01%aE%x01%s%x01%s%n%n%b%x02');
$commits = array();
$info = trim($info, " \n\2");
if (!strlen($info)) {
return array();
}
$info = explode("\2", $info);
foreach ($info as $line) {
list($commit, $tree, $parents, $time, $author, $author_email,
$title, $message) = explode("\1", trim($line), 8);
$message = rtrim($message);
$commits[$commit] = array(
'commit' => $commit,
'tree' => $tree,
'parents' => array_filter(explode(' ', $parents)),
'time' => $time,
'author' => $author,
'summary' => $title,
'message' => $message,
'authorEmail' => $author_email,
);
}
return $commits;
}
protected function buildBaseCommit($symbolic_commit) {
if ($symbolic_commit !== null) {
if ($symbolic_commit == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
$this->setBaseCommitExplanation(
"you explicitly specified the empty tree.");
return $symbolic_commit;
}
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$symbolic_commit);
if ($err) {
throw new ArcanistUsageException(
"Unable to find any git commit named '{$symbolic_commit}' in ".
"this repository.");
}
$this->setBaseCommitExplanation(
"it is the merge-base of '{$symbolic_commit}' and HEAD, as you ".
"explicitly specified.");
return trim($merge_base);
}
// Detect zero-commit or one-commit repositories. There is only one
// relative-commit value that makes any sense in these repositories: the
// empty tree.
list($err) = $this->execManualLocal('rev-parse --verify HEAD^');
if ($err) {
list($err) = $this->execManualLocal('rev-parse --verify HEAD');
if ($err) {
$this->repositoryHasNoCommits = true;
}
if ($this->repositoryHasNoCommits) {
$this->setBaseCommitExplanation(
"the repository has no commits.");
} else {
$this->setBaseCommitExplanation(
"the repository has only one commit.");
}
return self::GIT_MAGIC_ROOT_COMMIT;
}
if ($this->getBaseCommitArgumentRules() ||
- $this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) {
+ $this->getConfigurationManager()->getConfigFromAnySource('base')) {
$base = $this->resolveBaseCommit();
if (!$base) {
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
return $base;
}
$do_write = false;
$default_relative = null;
$working_copy = $this->getWorkingCopyIdentity();
if ($working_copy) {
$default_relative = $working_copy->getConfig(
'git.default-relative-commit');
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in 'git.default-relative-commit' in '.arcconfig'. This ".
"setting overrides other settings.");
}
if (!$default_relative) {
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
$default_relative = trim($upstream);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' (the Git upstream ".
"of the current branch) HEAD.");
}
}
if (!$default_relative) {
$default_relative = $this->readScratchFile('default-relative-commit');
$default_relative = trim($default_relative);
if ($default_relative) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in '.git/arc/default-relative-commit'.");
}
}
if (!$default_relative) {
// TODO: Remove the history lesson soon.
echo phutil_console_format(
"<bg:green>** Select a Default Commit Range **</bg>\n\n");
echo phutil_console_wrap(
"You're running a command which operates on a range of revisions ".
"(usually, from some revision to HEAD) but have not specified the ".
"revision that should determine the start of the range.\n\n".
"Previously, arc assumed you meant 'HEAD^' when you did not specify ".
"a start revision, but this behavior does not make much sense in ".
"most workflows outside of Facebook's historic git-svn workflow.\n\n".
"arc no longer assumes 'HEAD^'. You must specify a relative commit ".
"explicitly when you invoke a command (e.g., `arc diff HEAD^`, not ".
"just `arc diff`) or select a default for this working copy.\n\n".
"In most cases, the best default is 'origin/master'. You can also ".
"select 'HEAD^' to preserve the old behavior, or some other remote ".
"or branch. But you almost certainly want to select ".
"'origin/master'.\n\n".
"(Technically: the merge-base of the selected revision and HEAD is ".
"used to determine the start of the commit range.)");
$prompt = "What default do you want to use? [origin/master]";
$default = phutil_console_prompt($prompt);
if (!strlen(trim($default))) {
$default = 'origin/master';
}
$default_relative = $default;
$do_write = true;
}
list($object_type) = $this->execxLocal(
'cat-file -t %s',
$default_relative);
if (trim($object_type) !== 'commit') {
throw new Exception(
"Relative commit '{$default_relative}' is not the name of a commit!");
}
if ($do_write) {
// Don't perform this write until we've verified that the object is a
// valid commit name.
$this->writeScratchFile('default-relative-commit', $default_relative);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as you ".
"just specified.");
}
list($merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$default_relative);
return trim($merge_base);
}
private function getDiffFullOptions($detect_moves_and_renames = true) {
$options = array(
self::getDiffBaseOptions(),
'--no-color',
'--src-prefix=a/',
'--dst-prefix=b/',
'-U'.$this->getDiffLinesOfContext(),
);
if ($detect_moves_and_renames) {
$options[] = '-M';
$options[] = '-C';
}
return implode(' ', $options);
}
private function getDiffBaseOptions() {
$options = array(
// Disable external diff drivers, like graphical differs, since Arcanist
// needs to capture the diff text.
'--no-ext-diff',
// Disable textconv so we treat binary files as binary, even if they have
// an alternative textual representation. TODO: Ideally, Differential
// would ship up the binaries for 'arc patch' but display the textconv
// output in the visual diff.
'--no-textconv',
);
return implode(' ', $options);
}
public function getFullGitDiff() {
$options = $this->getDiffFullOptions();
list($stdout) = $this->execxLocal(
"diff {$options} %s --",
$this->getBaseCommit());
return $stdout;
}
/**
* @param string Path to generate a diff for.
* @param bool If true, detect moves and renames. Otherwise, ignore
* moves/renames; this is useful because it prompts git to
* generate real diff text.
*/
public function getRawDiffText($path, $detect_moves_and_renames = true) {
$options = $this->getDiffFullOptions($detect_moves_and_renames);
list($stdout) = $this->execxLocal(
"diff {$options} %s -- %s",
$this->getBaseCommit(),
$path);
return $stdout;
}
public function getBranchName() {
// TODO: consider:
//
// $ git rev-parse --abbrev-ref `git symbolic-ref HEAD`
//
// But that may fail if you're not on a branch.
list($stdout) = $this->execxLocal('branch --no-color');
// Assume that any branch beginning with '(' means 'no branch', or whatever
// 'no branch' is in the current locale.
$matches = null;
if (preg_match('/^\* ([^\(].*)$/m', $stdout, $matches)) {
return $matches[1];
}
return null;
}
public function getSourceControlPath() {
// TODO: Try to get something useful here.
return null;
}
public function getGitCommitLog() {
$relative = $this->getBaseCommit();
if ($this->repositoryHasNoCommits) {
// No commits yet.
return '';
} else if ($relative == self::GIT_MAGIC_ROOT_COMMIT) {
// First commit.
list($stdout) = $this->execxLocal(
'log --format=medium HEAD');
} else {
// 2..N commits.
list($stdout) = $this->execxLocal(
'log --first-parent --format=medium %s..HEAD',
$this->getBaseCommit());
}
return $stdout;
}
public function getGitHistoryLog() {
list($stdout) = $this->execxLocal(
'log --format=medium -n%d %s',
self::SEARCH_LENGTH_FOR_PARENT_REVISIONS,
$this->getBaseCommit());
return $stdout;
}
public function getSourceControlBaseRevision() {
list($stdout) = $this->execxLocal(
'rev-parse %s',
$this->getBaseCommit());
return rtrim($stdout, "\n");
}
public function getCanonicalRevisionName($string) {
$match = null;
if (preg_match('/@([0-9]+)$/', $string, $match)) {
$stdout = $this->getHashFromFromSVNRevisionNumber($match[1]);
} else {
list($stdout) = $this->execxLocal(
'show -s --format=%C %s',
'%H',
$string);
}
return rtrim($stdout);
}
private function executeSVNFindRev($input, $vcs) {
$match = array();
list($stdout) = $this->execxLocal(
'svn find-rev %s',
$input);
if (!$stdout) {
throw new ArcanistUsageException("Cannot find the {$vcs} equivalent "
."of {$input}.");
}
// When git performs a partial-rebuild during svn
// look-up, we need to parse the final line
$lines = explode("\n", $stdout);
$stdout = $lines[count($lines) - 2];
return rtrim($stdout);
}
// Convert svn revision number to git hash
public function getHashFromFromSVNRevisionNumber($revision_id) {
return $this->executeSVNFindRev("r".$revision_id, "Git");
}
// Convert a git hash to svn revision number
public function getSVNRevisionNumberFromHash($hash) {
return $this->executeSVNFindRev($hash, "SVN");
}
protected function buildUncommittedStatus() {
$diff_options = $this->getDiffBaseOptions();
if ($this->repositoryHasNoCommits) {
$diff_base = self::GIT_MAGIC_ROOT_COMMIT;
} else {
$diff_base = 'HEAD';
}
// Find uncommitted changes.
$uncommitted_future = $this->buildLocalFuture(
array(
'diff %C --raw %s --',
$diff_options,
$diff_base,
));
$untracked_future = $this->buildLocalFuture(
array(
'ls-files --others --exclude-standard',
));
// TODO: This doesn't list unstaged adds. It's not clear how to get that
// list other than "git status --porcelain" and then parsing it. :/
// Unstaged changes
$unstaged_future = $this->buildLocalFuture(
array(
'ls-files -m',
));
$futures = array(
$uncommitted_future,
$untracked_future,
$unstaged_future,
);
Futures($futures)->resolveAll();
$result = new PhutilArrayWithDefaultValue();
list($stdout) = $uncommitted_future->resolvex();
$uncommitted_files = $this->parseGitStatus($stdout);
foreach ($uncommitted_files as $path => $mask) {
$result[$path] |= ($mask | self::FLAG_UNCOMMITTED);
}
list($stdout) = $untracked_future->resolvex();
$stdout = rtrim($stdout, "\n");
if (strlen($stdout)) {
$stdout = explode("\n", $stdout);
foreach ($stdout as $path) {
$result[$path] |= self::FLAG_UNTRACKED;
}
}
list($stdout, $stderr) = $unstaged_future->resolvex();
$stdout = rtrim($stdout, "\n");
if (strlen($stdout)) {
$stdout = explode("\n", $stdout);
foreach ($stdout as $path) {
$result[$path] |= self::FLAG_UNSTAGED;
}
}
return $result->toArray();
}
protected function buildCommitRangeStatus() {
list($stdout, $stderr) = $this->execxLocal(
'diff %C --raw %s --',
$this->getDiffBaseOptions(),
$this->getBaseCommit());
return $this->parseGitStatus($stdout);
}
public function getGitConfig($key, $default = null) {
list($err, $stdout) = $this->execManualLocal('config %s', $key);
if ($err) {
return $default;
}
return rtrim($stdout);
}
public function getAuthor() {
list($stdout) = $this->execxLocal('var GIT_AUTHOR_IDENT');
return preg_replace('/\s+<.*/', '', rtrim($stdout, "\n"));
}
public function addToCommit(array $paths) {
$this->execxLocal(
'add -A -- %Ls',
$paths);
$this->reloadWorkingCopy();
return $this;
}
public function doCommit($message) {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
// NOTE: "--allow-empty-message" was introduced some time after 1.7.0.4,
// so we do not provide it and thus require a message.
$this->execxLocal(
'commit -F %s',
$tmp_file);
$this->reloadWorkingCopy();
return $this;
}
public function amendCommit($message = null) {
if ($message === null) {
$this->execxLocal('commit --amend --allow-empty -C HEAD');
} else {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$this->execxLocal(
'commit --amend --allow-empty -F %s',
$tmp_file);
}
$this->reloadWorkingCopy();
return $this;
}
public function getPreReceiveHookStatus($old_ref, $new_ref) {
$options = $this->getDiffBaseOptions();
list($stdout) = $this->execxLocal(
"diff {$options} --raw %s %s --",
$old_ref,
$new_ref);
return $this->parseGitStatus($stdout, $full = true);
}
private function parseGitStatus($status, $full = false) {
static $flags = array(
'A' => self::FLAG_ADDED,
'M' => self::FLAG_MODIFIED,
'D' => self::FLAG_DELETED,
);
$status = trim($status);
$lines = array();
foreach (explode("\n", $status) as $line) {
if ($line) {
$lines[] = preg_split("/[ \t]/", $line, 6);
}
}
$files = array();
foreach ($lines as $line) {
$mask = 0;
$flag = $line[4];
$file = $line[5];
foreach ($flags as $key => $bits) {
if ($flag == $key) {
$mask |= $bits;
}
}
if ($full) {
$files[$file] = array(
'mask' => $mask,
'ref' => rtrim($line[3], '.'),
);
} else {
$files[$file] = $mask;
}
}
return $files;
}
public function getAllFiles() {
$future = $this->buildLocalFuture(array('ls-files -z'));
return id(new LinesOfALargeExecFuture($future))
->setDelimiter("\0");
}
public function getChangedFiles($since_commit) {
list($stdout) = $this->execxLocal(
'diff --raw %s',
$since_commit);
return $this->parseGitStatus($stdout);
}
public function getBlame($path) {
// TODO: 'git blame' supports --porcelain and we should probably use it.
list($stdout) = $this->execxLocal(
'blame --date=iso -w -M %s -- %s',
$this->getBaseCommit(),
$path);
$blame = array();
foreach (explode("\n", trim($stdout)) as $line) {
if (!strlen($line)) {
continue;
}
// lines predating a git repo's history are blamed to the oldest revision,
// with the commit hash prepended by a ^. we shouldn't count these lines
// as blaming to the oldest diff's unfortunate author
if ($line[0] == '^') {
continue;
}
$matches = null;
$ok = preg_match(
'/^([0-9a-f]+)[^(]+?[(](.*?) +\d\d\d\d-\d\d-\d\d/',
$line,
$matches);
if (!$ok) {
throw new Exception("Bad blame? `{$line}'");
}
$revision = $matches[1];
$author = $matches[2];
$blame[] = array($author, $revision);
}
return $blame;
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getBaseCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision($path, 'HEAD');
}
private function parseGitTree($stdout) {
$result = array();
$stdout = trim($stdout);
if (!strlen($stdout)) {
return $result;
}
$lines = explode("\n", $stdout);
foreach ($lines as $line) {
$matches = array();
$ok = preg_match(
'/^(\d{6}) (blob|tree|commit) ([a-z0-9]{40})[\t](.*)$/',
$line,
$matches);
if (!$ok) {
throw new Exception("Failed to parse git ls-tree output!");
}
$result[$matches[4]] = array(
'mode' => $matches[1],
'type' => $matches[2],
'ref' => $matches[3],
);
}
return $result;
}
private function getFileDataAtRevision($path, $revision) {
// NOTE: We don't want to just "git show {$revision}:{$path}" since if the
// path was a directory at the given revision we'll get a list of its files
// and treat it as though it as a file containing a list of other files,
// which is silly.
list($stdout) = $this->execxLocal(
'ls-tree %s -- %s',
$revision,
$path);
$info = $this->parseGitTree($stdout);
if (empty($info[$path])) {
// No such path, or the path is a directory and we executed 'ls-tree dir/'
// and got a list of its contents back.
return null;
}
if ($info[$path]['type'] != 'blob') {
// Path is or was a directory, not a file.
return null;
}
list($stdout) = $this->execxLocal(
'cat-file blob %s',
$info[$path]['ref']);
return $stdout;
}
/**
* Returns names of all the branches in the current repository.
*
* @return list<dict<string, string>> Dictionary of branch information.
*/
public function getAllBranches() {
list($branch_info) = $this->execxLocal(
'branch --no-color');
$lines = explode("\n", rtrim($branch_info));
$result = array();
foreach ($lines as $line) {
if (preg_match('/^[* ]+\(no branch\)/', $line)) {
// This is indicating that the working copy is in a detached state;
// just ignore it.
continue;
}
list($current, $name) = preg_split('/\s+/', $line, 2);
$result[] = array(
'current' => !empty($current),
'name' => $name,
);
}
return $result;
}
public function getWorkingCopyRevision() {
list($stdout) = $this->execxLocal('rev-parse HEAD');
return rtrim($stdout, "\n");
}
public function getUnderlyingWorkingCopyRevision() {
list($err, $stdout) = $this->execManualLocal('svn find-rev HEAD');
if (!$err && $stdout) {
return rtrim($stdout, "\n");
}
return $this->getWorkingCopyRevision();
}
public function isHistoryDefaultImmutable() {
return false;
}
public function supportsAmend() {
return true;
}
public function supportsCommitRanges() {
return true;
}
public function supportsLocalCommits() {
return true;
}
public function hasLocalCommit($commit) {
try {
if (!$this->getCanonicalRevisionName($commit)) {
return false;
}
} catch (CommandException $exception) {
return false;
}
return true;
}
public function getAllLocalChanges() {
$diff = $this->getFullGitDiff();
if (!strlen(trim($diff))) {
return array();
}
$parser = new ArcanistDiffParser();
return $parser->parseDiff($diff);
}
public function supportsLocalBranchMerge() {
return true;
}
public function performLocalBranchMerge($branch, $message) {
if (!$branch) {
throw new ArcanistUsageException(
"Under git, you must specify the branch you want to merge.");
}
$err = phutil_passthru(
'(cd %s && git merge --no-ff -m %s %s)',
$this->getPath(),
$message,
$branch);
if ($err) {
throw new ArcanistUsageException("Merge failed!");
}
}
public function getFinalizedRevisionMessage() {
return "You may now push this commit upstream, as appropriate (e.g. with ".
"'git push', or 'git svn dcommit', or by printing and faxing it).";
}
public function getCommitMessage($commit) {
list($message) = $this->execxLocal(
'log -n1 --format=%C %s --',
'%s%n%n%b',
$commit);
return $message;
}
public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query) {
$messages = $this->getGitCommitLog();
if (!strlen($messages)) {
return array();
}
$parser = new ArcanistDiffParser();
$messages = $parser->parseDiff($messages);
// First, try to find revisions by explicit revision IDs in commit messages.
$reason_map = array();
$revision_ids = array();
foreach ($messages as $message) {
$object = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$message->getMetadata('message'));
if ($object->getRevisionID()) {
$revision_ids[] = $object->getRevisionID();
$reason_map[$object->getRevisionID()] = $message->getCommitHash();
}
}
if ($revision_ids) {
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'ids' => $revision_ids,
));
foreach ($results as $key => $result) {
$hash = substr($reason_map[$result['id']], 0, 16);
$results[$key]['why'] =
"Commit message for '{$hash}' has explicit 'Differential Revision'.";
}
return $results;
}
// If we didn't succeed, try to find revisions by hash.
$hashes = array();
foreach ($this->getLocalCommitInformation() as $commit) {
$hashes[] = array('gtcm', $commit['commit']);
$hashes[] = array('gttr', $commit['tree']);
}
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'commitHashes' => $hashes,
));
foreach ($results as $key => $result) {
$results[$key]['why'] =
"A git commit or tree hash in the commit range is already attached ".
"to the Differential revision.";
}
return $results;
}
public function updateWorkingCopy() {
$this->execxLocal('pull');
$this->reloadWorkingCopy();
}
public function getCommitSummary($commit) {
if ($commit == self::GIT_MAGIC_ROOT_COMMIT) {
return '(The Empty Tree)';
}
list($summary) = $this->execxLocal(
'log -n 1 --format=%C %s',
'%s',
$commit);
return trim($summary);
}
public function backoutCommit($commit_hash) {
$this->execxLocal(
'revert %s -n --no-edit', $commit_hash);
$this->reloadWorkingCopy();
if (!$this->getUncommittedStatus()) {
throw new ArcanistUsageException(
"{$commit_hash} has already been reverted.");
}
}
public function getBackoutMessage($commit_hash) {
return "This reverts commit ".$commit_hash.".";
}
public function isGitSubversionRepo() {
return Filesystem::pathExists($this->getPath('.git/svn'));
}
public function resolveBaseCommitRule($rule, $source) {
list($type, $name) = explode(':', $rule, 2);
switch ($type) {
case 'git':
$matches = null;
if (preg_match('/^merge-base\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$matches[1]);
if (!$err) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$matches[1]}' and HEAD, as ".
"specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return trim($merge_base);
}
} else if (preg_match('/^branch-unique\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$matches[1]);
if ($err) {
return null;
}
$merge_base = trim($merge_base);
list($commits) = $this->execxLocal(
'log --format=%C %s..HEAD --',
'%H',
$merge_base);
$commits = array_filter(explode("\n", $commits));
if (!$commits) {
return null;
}
$commits[] = $merge_base;
$head_branch_count = null;
foreach ($commits as $commit) {
list($branches) = $this->execxLocal(
'branch --contains %s',
$commit);
$branches = array_filter(explode("\n", $branches));
if ($head_branch_count === null) {
// If this is the first commit, it's HEAD. Count how many
// branches it is on; we want to include commits on the same
// number of branches. This covers a case where this branch
// has sub-branches and we're running "arc diff" here again
// for whatever reason.
$head_branch_count = count($branches);
} else if (count($branches) > $head_branch_count) {
foreach ($branches as $key => $branch) {
$branches[$key] = trim($branch, ' *');
}
$branches = implode(', ', $branches);
$this->setBaseCommitExplanation(
"it is the first commit between '{$merge_base}' (the ".
"merge-base of '{$matches[1]}' and HEAD) which is also ".
"contained by another branch ({$branches}).");
return $commit;
}
}
} else {
list($err) = $this->execManualLocal(
'cat-file -t %s',
$name);
if (!$err) {
$this->setBaseCommitExplanation(
"it is specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return $name;
}
}
break;
case 'arc':
switch ($name) {
case 'empty':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return self::GIT_MAGIC_ROOT_COMMIT;
case 'amended':
$text = $this->getCommitMessage('HEAD');
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$text);
if ($message->getRevisionID()) {
$this->setBaseCommitExplanation(
"HEAD has been amended with 'Differential Revision:', ".
"as specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return 'HEAD^';
}
break;
case 'upstream':
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
$upstream = rtrim($upstream);
list($upstream_merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$upstream);
$upstream_merge_base = rtrim($upstream_merge_base);
$this->setBaseCommitExplanation(
"it is the merge-base of the upstream of the current branch ".
"and HEAD, and matched the rule '{$rule}' in your {$source} ".
"'base' configuration.");
return $upstream_merge_base;
}
break;
case 'this':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return 'HEAD^';
}
default:
return null;
}
return null;
}
public function canStashChanges() {
return true;
}
public function stashChanges() {
$this->execxLocal('stash');
$this->reloadWorkingCopy();
}
public function unstashChanges() {
$this->execxLocal('stash pop');
}
}
diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php
index b45184bb..1c58fdb4 100644
--- a/src/repository/api/ArcanistRepositoryAPI.php
+++ b/src/repository/api/ArcanistRepositoryAPI.php
@@ -1,663 +1,674 @@
<?php
/**
* Interfaces with the VCS in the working copy.
*
* @task status Path Status
* @group workingcopy
*/
abstract class ArcanistRepositoryAPI {
const FLAG_MODIFIED = 1;
const FLAG_ADDED = 2;
const FLAG_DELETED = 4;
const FLAG_UNTRACKED = 8;
const FLAG_CONFLICT = 16;
const FLAG_MISSING = 32;
const FLAG_UNSTAGED = 64;
const FLAG_UNCOMMITTED = 128;
const FLAG_EXTERNALS = 256;
// Occurs in SVN when you replace a file with a directory without telling
// SVN about it.
const FLAG_OBSTRUCTED = 512;
// Occurs in SVN when an update was interrupted or failed, e.g. you ^C'd it.
const FLAG_INCOMPLETE = 1024;
protected $path;
protected $diffLinesOfContext = 0x7FFF;
private $baseCommitExplanation = '???';
- private $workingCopyIdentity;
+ private $configurationManager;
private $baseCommitArgumentRules;
private $uncommittedStatusCache;
private $commitRangeStatusCache;
private $symbolicBaseCommit;
private $resolvedBaseCommit;
abstract public function getSourceControlSystemName();
public function getDiffLinesOfContext() {
return $this->diffLinesOfContext;
}
public function setDiffLinesOfContext($lines) {
$this->diffLinesOfContext = $lines;
return $this;
}
public function getWorkingCopyIdentity() {
- return $this->workingCopyIdentity;
+ return $this->configurationManager->getWorkingCopyIdentity();
}
- public static function newAPIFromWorkingCopyIdentity(
- ArcanistWorkingCopyIdentity $working_copy) {
+ public function getConfigurationManager() {
+ return $this->configurationManager;
+ }
+
+ public static function newAPIFromConfigurationManager(
+ ArcanistConfigurationManager $configuration_manager) {
+
+ $working_copy = $configuration_manager->getWorkingCopyIdentity();
+
+ if (!$working_copy) {
+ throw new Exception(
+ "Trying to create a RepositoryApi without a working copy");
+ }
$root = $working_copy->getProjectRoot();
if (!$root) {
throw new ArcanistUsageException(
"There is no readable '.arcconfig' file in the working directory or ".
"any parent directory. Create an '.arcconfig' file to configure arc.");
}
if (Filesystem::pathExists($root.'/.hg')) {
$api = new ArcanistMercurialAPI($root);
- $api->workingCopyIdentity = $working_copy;
+ $api->configurationManager = $configuration_manager;
return $api;
}
$git_root = self::discoverGitBaseDirectory($root);
if ($git_root) {
if (!Filesystem::pathsAreEquivalent($root, $git_root)) {
throw new ArcanistUsageException(
"'.arcconfig' file is located at '{$root}', but working copy root ".
"is '{$git_root}'. Move '.arcconfig' file to the working copy root.");
}
$api = new ArcanistGitAPI($root);
- $api->workingCopyIdentity = $working_copy;
+ $api->configurationManager = $configuration_manager;
return $api;
}
// check if we're in an svn working copy
foreach (Filesystem::walkToRoot($root) as $dir) {
if (Filesystem::pathExists($dir . '/.svn')) {
$api = new ArcanistSubversionAPI($root);
- $api->workingCopyIdentity = $working_copy;
+ $api->configurationManager = $configuration_manager;
return $api;
}
}
throw new ArcanistUsageException(
"The current working directory is not part of a working copy for a ".
"supported version control system (svn, git or mercurial).");
}
public function __construct($path) {
$this->path = $path;
}
public function getPath($to_file = null) {
if ($to_file !== null) {
return $this->path.DIRECTORY_SEPARATOR.
ltrim($to_file, DIRECTORY_SEPARATOR);
} else {
return $this->path.DIRECTORY_SEPARATOR;
}
}
/* -( Path Status )-------------------------------------------------------- */
abstract protected function buildUncommittedStatus();
abstract protected function buildCommitRangeStatus();
/**
* Get a list of uncommitted paths in the working copy that have been changed
* or are affected by other status effects, like conflicts or untracked
* files.
*
* Convenience methods @{method:getUntrackedChanges},
* @{method:getUnstagedChanges}, @{method:getUncommittedChanges},
* @{method:getMergeConflicts}, and @{method:getIncompleteChanges} allow
* simpler selection of paths in a specific state.
*
* This method returns a map of paths to bitmasks with status, using
* `FLAG_` constants. For example:
*
* array(
* 'some/uncommitted/file.txt' => ArcanistRepositoryAPI::FLAG_UNSTAGED,
* );
*
* A file may be in several states. Not all states are possible with all
* version control systems.
*
* @return map<string, bitmask> Map of paths, see above.
* @task status
*/
final public function getUncommittedStatus() {
if ($this->uncommittedStatusCache === null) {
$status = $this->buildUncommittedStatus();
ksort($status);
$this->uncommittedStatusCache = $status;
}
return $this->uncommittedStatusCache;
}
/**
* @task status
*/
final public function getUntrackedChanges() {
return $this->getUncommittedPathsWithMask(self::FLAG_UNTRACKED);
}
/**
* @task status
*/
final public function getUnstagedChanges() {
return $this->getUncommittedPathsWithMask(self::FLAG_UNSTAGED);
}
/**
* @task status
*/
final public function getUncommittedChanges() {
return $this->getUncommittedPathsWithMask(self::FLAG_UNCOMMITTED);
}
/**
* @task status
*/
final public function getMergeConflicts() {
return $this->getUncommittedPathsWithMask(self::FLAG_CONFLICT);
}
/**
* @task status
*/
final public function getIncompleteChanges() {
return $this->getUncommittedPathsWithMask(self::FLAG_INCOMPLETE);
}
/**
* @task status
*/
private function getUncommittedPathsWithMask($mask) {
$match = array();
foreach ($this->getUncommittedStatus() as $path => $flags) {
if ($flags & $mask) {
$match[] = $path;
}
}
return $match;
}
/**
* Get a list of paths affected by the commits in the current commit range.
*
* See @{method:getUncommittedStatus} for a description of the return value.
*
* @return map<string, bitmask> Map from paths to status.
* @task status
*/
final public function getCommitRangeStatus() {
if ($this->commitRangeStatusCache === null) {
$status = $this->buildCommitRangeStatus();
ksort($status);
$this->commitRangeStatusCache = $status;
}
return $this->commitRangeStatusCache;
}
/**
* Get a list of paths affected by commits in the current commit range, or
* uncommitted changes in the working copy. See @{method:getUncommittedStatus}
* or @{method:getCommitRangeStatus} to retreive smaller parts of the status.
*
* See @{method:getUncommittedStatus} for a description of the return value.
*
* @return map<string, bitmask> Map from paths to status.
* @task status
*/
final public function getWorkingCopyStatus() {
$range_status = $this->getCommitRangeStatus();
$uncommitted_status = $this->getUncommittedStatus();
$result = new PhutilArrayWithDefaultValue($range_status);
foreach ($uncommitted_status as $path => $mask) {
$result[$path] |= $mask;
}
$result = $result->toArray();
ksort($result);
return $result;
}
/**
* Drops caches after changes to the working copy. By default, some queries
* against the working copy are cached. They
*
* @return this
* @task status
*/
final public function reloadWorkingCopy() {
$this->uncommittedStatusCache = null;
$this->commitRangeStatusCache = null;
$this->didReloadWorkingCopy();
$this->reloadCommitRange();
return $this;
}
/**
* Hook for implementations to dirty working copy caches after the working
* copy has been updated.
*
* @return this
* @task status
*/
protected function didReloadWorkingCopy() {
return;
}
private static function discoverGitBaseDirectory($root) {
try {
// NOTE: This awkward construction is to make sure things work on Windows.
$future = new ExecFuture('git rev-parse --show-cdup');
$future->setCWD($root);
list($stdout) = $future->resolvex();
return Filesystem::resolvePath(rtrim($stdout, "\n"), $root);
} catch (CommandException $ex) {
// This might be because the $root isn't a Git working copy, or the user
// might not have Git installed at all so the `git` command fails. Assume
// that users trying to work with git working copies will have a working
// `git` binary.
return null;
}
}
/**
* Fetches the original file data for each path provided.
*
* @return map<string, string> Map from path to file data.
*/
public function getBulkOriginalFileData($paths) {
$filedata = array();
foreach ($paths as $path) {
$filedata[$path] = $this->getOriginalFileData($path);
}
return $filedata;
}
/**
* Fetches the current file data for each path provided.
*
* @return map<string, string> Map from path to file data.
*/
public function getBulkCurrentFileData($paths) {
$filedata = array();
foreach ($paths as $path) {
$filedata[$path] = $this->getCurrentFileData($path);
}
return $filedata;
}
/**
* @return Traversable
*/
abstract public function getAllFiles();
abstract public function getBlame($path);
abstract public function getRawDiffText($path);
abstract public function getOriginalFileData($path);
abstract public function getCurrentFileData($path);
abstract public function getLocalCommitInformation();
abstract public function getSourceControlBaseRevision();
abstract public function getCanonicalRevisionName($string);
abstract public function getBranchName();
abstract public function getSourceControlPath();
abstract public function isHistoryDefaultImmutable();
abstract public function supportsAmend();
abstract public function getWorkingCopyRevision();
abstract public function updateWorkingCopy();
abstract public function getMetadataPath();
abstract public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query);
public function getUnderlyingWorkingCopyRevision() {
return $this->getWorkingCopyRevision();
}
public function getChangedFiles($since_commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getAuthor() {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function addToCommit(array $paths) {
throw new ArcanistCapabilityNotSupportedException($this);
}
abstract public function supportsLocalCommits();
public function doCommit($message) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function amendCommit($message = null) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getAllBranches() {
// TODO: Implement for Mercurial/SVN and make abstract.
return array();
}
public function hasLocalCommit($commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getCommitMessage($commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getCommitSummary($commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getAllLocalChanges() {
throw new ArcanistCapabilityNotSupportedException($this);
}
abstract public function supportsLocalBranchMerge();
public function performLocalBranchMerge($branch, $message) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getFinalizedRevisionMessage() {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function execxLocal($pattern /* , ... */) {
$args = func_get_args();
return $this->buildLocalFuture($args)->resolvex();
}
public function execManualLocal($pattern /* , ... */) {
$args = func_get_args();
return $this->buildLocalFuture($args)->resolve();
}
public function execFutureLocal($pattern /* , ... */) {
$args = func_get_args();
return $this->buildLocalFuture($args);
}
abstract protected function buildLocalFuture(array $argv);
public function canStashChanges() {
return false;
}
public function stashChanges() {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function unstashChanges() {
throw new ArcanistCapabilityNotSupportedException($this);
}
/* -( Scratch Files )------------------------------------------------------ */
/**
* Try to read a scratch file, if it exists and is readable.
*
* @param string Scratch file name.
* @return mixed String for file contents, or false for failure.
* @task scratch
*/
public function readScratchFile($path) {
$full_path = $this->getScratchFilePath($path);
if (!$full_path) {
return false;
}
if (!Filesystem::pathExists($full_path)) {
return false;
}
try {
$result = Filesystem::readFile($full_path);
} catch (FilesystemException $ex) {
return false;
}
return $result;
}
/**
* Try to write a scratch file, if there's somewhere to put it and we can
* write there.
*
* @param string Scratch file name to write.
* @param string Data to write.
* @return bool True on success, false on failure.
* @task scratch
*/
public function writeScratchFile($path, $data) {
$dir = $this->getScratchFilePath('');
if (!$dir) {
return false;
}
if (!Filesystem::pathExists($dir)) {
try {
Filesystem::createDirectory($dir);
} catch (Exception $ex) {
return false;
}
}
try {
Filesystem::writeFile($this->getScratchFilePath($path), $data);
} catch (FilesystemException $ex) {
return false;
}
return true;
}
/**
* Try to remove a scratch file.
*
* @param string Scratch file name to remove.
* @return bool True if the file was removed successfully.
* @task scratch
*/
public function removeScratchFile($path) {
$full_path = $this->getScratchFilePath($path);
if (!$full_path) {
return false;
}
try {
Filesystem::remove($full_path);
} catch (FilesystemException $ex) {
return false;
}
return true;
}
/**
* Get a human-readable description of the scratch file location.
*
* @param string Scratch file name.
* @return mixed String, or false on failure.
* @task scratch
*/
public function getReadableScratchFilePath($path) {
$full_path = $this->getScratchFilePath($path);
if ($full_path) {
return Filesystem::readablePath(
$full_path,
$this->getPath());
} else {
return false;
}
}
/**
* Get the path to a scratch file, if possible.
*
* @param string Scratch file name.
* @return mixed File path, or false on failure.
* @task scratch
*/
public function getScratchFilePath($path) {
$new_scratch_path = Filesystem::resolvePath(
'arc',
$this->getMetadataPath());
static $checked = false;
if (!$checked) {
$checked = true;
$old_scratch_path = $this->getPath('.arc');
// we only want to do the migration once
// unfortunately, people have checked in .arc directories which
// means that the old one may get recreated after we delete it
if (Filesystem::pathExists($old_scratch_path) &&
!Filesystem::pathExists($new_scratch_path)) {
Filesystem::createDirectory($new_scratch_path);
$existing_files = Filesystem::listDirectory($old_scratch_path, true);
foreach ($existing_files as $file) {
$new_path = Filesystem::resolvePath($file, $new_scratch_path);
$old_path = Filesystem::resolvePath($file, $old_scratch_path);
Filesystem::writeFile(
$new_path,
Filesystem::readFile($old_path));
}
Filesystem::remove($old_scratch_path);
}
}
return Filesystem::resolvePath($path, $new_scratch_path);
}
/* -( Base Commits )------------------------------------------------------- */
abstract public function supportsCommitRanges();
final public function setBaseCommit($symbolic_commit) {
if (!$this->supportsCommitRanges()) {
throw new ArcanistCapabilityNotSupportedException($this);
}
$this->symbolicBaseCommit = $symbolic_commit;
$this->reloadCommitRange();
return $this;
}
final public function getBaseCommit() {
if (!$this->supportsCommitRanges()) {
throw new ArcanistCapabilityNotSupportedException($this);
}
if ($this->resolvedBaseCommit === null) {
$commit = $this->buildBaseCommit($this->symbolicBaseCommit);
$this->resolvedBaseCommit = $commit;
}
return $this->resolvedBaseCommit;
}
final public function reloadCommitRange() {
$this->resolvedBaseCommit = null;
$this->baseCommitExplanation = null;
$this->didReloadCommitRange();
return $this;
}
protected function didReloadCommitRange() {
return;
}
protected function buildBaseCommit($symbolic_commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getBaseCommitExplanation() {
return $this->baseCommitExplanation;
}
public function setBaseCommitExplanation($explanation) {
$this->baseCommitExplanation = $explanation;
return $this;
}
public function resolveBaseCommitRule($rule, $source) {
return null;
}
public function setBaseCommitArgumentRules($base_commit_argument_rules) {
$this->baseCommitArgumentRules = $base_commit_argument_rules;
return $this;
}
public function getBaseCommitArgumentRules() {
return $this->baseCommitArgumentRules;
}
public function resolveBaseCommit() {
- $working_copy = $this->getWorkingCopyIdentity();
- $global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
- $system_config = ArcanistBaseWorkflow::readSystemArcConfig();
+ $base_commit_rules = array(
+ 'runtime' => $this->getBaseCommitArgumentRules(),
+ 'local' => '',
+ 'project' => '',
+ 'user' => '',
+ 'system' => '',
+ );
+ $all_sources = $this->configurationManager->getConfigFromAllSources('base');
+
+ $base_commit_rules = $all_sources + $base_commit_rules;
$parser = new ArcanistBaseCommitParser($this);
- $commit = $parser->resolveBaseCommit(
- array(
- 'args' => $this->getBaseCommitArgumentRules(),
- 'local' => $working_copy->getLocalConfig('base', ''),
- 'project' => $working_copy->getConfig('base', ''),
- 'global' => idx($global_config, 'base', ''),
- 'system' => idx($system_config, 'base', ''),
- ));
+ $commit = $parser->resolveBaseCommit($base_commit_rules);
return $commit;
}
}
diff --git a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php
index 3afbbc2c..0de40543 100644
--- a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php
+++ b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php
@@ -1,121 +1,123 @@
<?php
final class ArcanistRepositoryAPIStateTestCase extends ArcanistTestCase {
public function testGitStateParsing() {
if (Filesystem::binaryExists('git')) {
$this->parseState('git_basic.git.tgz');
} else {
$this->assertSkipped('Git is not installed');
}
}
public function testHgStateParsing() {
if (Filesystem::binaryExists('hg')) {
$this->parseState('hg_basic.hg.tgz');
} else {
$this->assertSkipped('Mercurial is not installed');
}
}
public function testSvnStateParsing() {
if (Filesystem::binaryExists('svn')) {
$this->parseState('svn_basic.svn.tgz');
} else {
$this->assertSkipped('Subversion is not installed');
}
}
private function parseState($test) {
$dir = dirname(__FILE__) . '/state/';
$fixture = PhutilDirectoryFixture::newFromArchive($dir.'/'.$test);
$fixture_path = $fixture->getPath();
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($fixture_path);
+ $configuration_manager = new ArcanistConfigurationManager();
+ $configuration_manager->setWorkingCopyIdentity($working_copy);
+ $api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
+ $configuration_manager);
- $api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
- $working_copy);
$api->setBaseCommitArgumentRules('arc:this');
if ($api instanceof ArcanistSubversionAPI) {
// Upgrade the repository so that the test will still pass if the local
// `svn` is newer than the `svn` which created the repository.
// NOTE: Some versions of Subversion (1.7.x?) exit with an error code on
// a no-op upgrade, although newer versions do not. We just ignore the
// error here; if it's because of an actual problem we'll hit an error
// shortly anyway.
$api->execManualLocal('upgrade');
}
$this->assertCorrectState($test, $api);
}
private function assertCorrectState($test, ArcanistRepositoryAPI $api) {
$f_mod = ArcanistRepositoryAPI::FLAG_MODIFIED;
$f_add = ArcanistRepositoryAPI::FLAG_ADDED;
$f_del = ArcanistRepositoryAPI::FLAG_DELETED;
$f_unt = ArcanistRepositoryAPI::FLAG_UNTRACKED;
$f_con = ArcanistRepositoryAPI::FLAG_CONFLICT;
$f_mis = ArcanistRepositoryAPI::FLAG_MISSING;
$f_uns = ArcanistRepositoryAPI::FLAG_UNSTAGED;
$f_unc = ArcanistRepositoryAPI::FLAG_UNCOMMITTED;
$f_ext = ArcanistRepositoryAPI::FLAG_EXTERNALS;
$f_obs = ArcanistRepositoryAPI::FLAG_OBSTRUCTED;
$f_inc = ArcanistRepositoryAPI::FLAG_INCOMPLETE;
switch ($test) {
case 'svn_basic.svn.tgz':
$expect = array(
'ADDED' => $f_add,
'COPIED_TO' => $f_add,
'DELETED' => $f_del,
'MODIFIED' => $f_mod,
'MOVED' => $f_del,
'MOVED_TO' => $f_add,
'PROPCHANGE' => $f_mod,
'UNTRACKED' => $f_unt,
);
$this->assertEqual($expect, $api->getUncommittedStatus());
$this->assertEqual($expect, $api->getCommitRangeStatus());
break;
case 'git_basic.git.tgz':
$expect_uncommitted = array(
'UNCOMMITTED' => $f_add | $f_unc,
'UNSTAGED' => $f_mod | $f_uns | $f_unc,
'UNTRACKED' => $f_unt,
);
$this->assertEqual($expect_uncommitted, $api->getUncommittedStatus());
$expect_range = array(
'ADDED' => $f_add,
'DELETED' => $f_del,
'MODIFIED' => $f_mod,
'UNCOMMITTED' => $f_add,
'UNSTAGED' => $f_add,
);
$this->assertEqual($expect_range, $api->getCommitRangeStatus());
break;
case 'hg_basic.hg.tgz':
$expect_uncommitted = array(
'UNCOMMITTED' => $f_mod | $f_unc,
'UNTRACKED' => $f_unt,
);
$this->assertEqual($expect_uncommitted, $api->getUncommittedStatus());
$expect_range = array(
'ADDED' => $f_add,
'DELETED' => $f_del,
'MODIFIED' => $f_mod,
'UNCOMMITTED' => $f_add,
);
$this->assertEqual($expect_range, $api->getCommitRangeStatus());
break;
default:
throw new Exception(
"No test cases for working copy '{$test}'!");
}
}
}
diff --git a/src/workflow/ArcanistAliasWorkflow.php b/src/workflow/ArcanistAliasWorkflow.php
index 7b6362a5..a53aa0b1 100644
--- a/src/workflow/ArcanistAliasWorkflow.php
+++ b/src/workflow/ArcanistAliasWorkflow.php
@@ -1,171 +1,173 @@
<?php
/**
* Manages aliases for commands with options.
*
* @group workflow
*/
final class ArcanistAliasWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'alias';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**alias**
**alias** __command__
**alias** __command__ __target__ -- [__options__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Create an alias from __command__ to __target__ (optionally, with
__options__). For example:
arc alias fpatch patch -- --force
...will create a new 'arc' command, 'arc fpatch', which invokes
'arc patch --force ...' when run. NOTE: use "--" before specifying
options!
If you start an alias with "!", the remainder of the alias will be
invoked as a shell command. For example, if you want to implement
'arc ls', you can do so like this:
arc alias ls '!ls'
You can now run "arc ls" and it will behave like "ls". Of course, this
example is silly and would make your life worse.
You can not overwrite builtins, including 'alias' itself. The builtin
will always execute, even if it was added after your alias.
To remove an alias, run:
arc alias fpatch
Without any arguments, 'arc alias' will list aliases.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'argv',
);
}
- public static function getAliases($working_copy) {
- $working_copy_config_aliases = $working_copy->getConfig('aliases');
+ public static function getAliases(
+ ArcanistConfigurationManager $configuration_manager) {
+
+ $working_copy_config_aliases =
+ $configuration_manager->getProjectConfig('aliases');
if (!$working_copy_config_aliases) {
$working_copy_config_aliases = array();
}
- $user_config_aliases =
- idx(self::readUserConfigurationFile(), 'aliases', array());
+ $user_config_aliases = idx(
+ $configuration_manager->readUserConfigurationFile(),
+ 'aliases',
+ array());
return $user_config_aliases + $working_copy_config_aliases;
}
private function writeAliases(array $aliases) {
- $config = self::readUserConfigurationFile();
+ $config = $this->getConfigurationManager()->readUserConfigurationFile();
$config['aliases'] = $aliases;
- self::writeUserConfigurationFile($config);
+ $this->getConfigurationManager()->writeUserConfigurationFile($config);
}
public function run() {
- // We might not be in a working directory, so we don't want to require a
- // working copy identity here.
- $working_copy = ArcanistWorkingCopyIdentity::newFromPath(getcwd());
- $aliases = self::getAliases($working_copy);
+ $aliases = self::getAliases($this->getConfigurationManager());
$argv = $this->getArgument('argv');
if (count($argv) == 0) {
if ($aliases) {
foreach ($aliases as $alias => $binding) {
echo phutil_console_format(
"**%s** %s\n",
$alias,
implode(' ' , $binding));
}
} else {
echo "You haven't defined any aliases yet.\n";
}
} else if (count($argv) == 1) {
if (empty($aliases[$argv[0]])) {
echo "No alias '{$argv[0]}' to remove.\n";
} else {
echo phutil_console_format(
"'**arc %s**' is currently aliased to '**arc %s**'.",
$argv[0],
implode(' ', $aliases[$argv[0]]));
$ok = phutil_console_confirm('Delete this alias?');
if ($ok) {
$was = implode(' ', $aliases[$argv[0]]);
unset($aliases[$argv[0]]);
$this->writeAliases($aliases);
echo "Unaliased '{$argv[0]}' (was '{$was}').\n";
} else {
throw new ArcanistUserAbortException();
}
}
} else {
$arc_config = $this->getArcanistConfiguration();
if ($arc_config->buildWorkflow($argv[0])) {
throw new ArcanistUsageException(
"You can not create an alias for '{$argv[0]}' because it is a ".
"builtin command. 'arc alias' can only create new commands.");
}
$aliases[$argv[0]] = array_slice($argv, 1);
echo phutil_console_format(
"Aliased '**arc %s**' to '**arc %s**'.\n",
$argv[0],
implode(' ', $aliases[$argv[0]]));
$this->writeAliases($aliases);
}
return 0;
}
public static function isShellCommandAlias($command) {
return preg_match('/^!/', $command);
}
public static function resolveAliases(
$command,
ArcanistConfiguration $config,
array $argv,
- ArcanistWorkingCopyIdentity $working_copy) {
+ ArcanistConfigurationManager $configuration_manager) {
- $aliases = ArcanistAliasWorkflow::getAliases($working_copy);
+ $aliases = ArcanistAliasWorkflow::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);
}
}
diff --git a/src/workflow/ArcanistBaseWorkflow.php b/src/workflow/ArcanistBaseWorkflow.php
index 705f172e..2be6c7da 100644
--- a/src/workflow/ArcanistBaseWorkflow.php
+++ b/src/workflow/ArcanistBaseWorkflow.php
@@ -1,1613 +1,1505 @@
<?php
/**
* 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:getUserPHID}
* or @{method:getUserName} after authentication occurs.
*
* = Scratch Files =
*
* Arcanist workflows can read and write 'scratch files', which are temporary
* files stored in the project that persist across commands. They can be useful
* if you want to save some state, or keep a copy of a long message the user
* entered if something goes wrong.
*
*
* @task conduit Conduit
* @task scratch Scratch Files
* @group workflow
* @stable
*/
abstract class ArcanistBaseWorkflow extends Phobject {
const COMMIT_DISABLE = 0;
const COMMIT_ALLOW = 1;
const COMMIT_ENABLE = 2;
const AUTO_COMMIT_TITLE = 'Automatic commit by arc';
private $commitMode = self::COMMIT_DISABLE;
private $shouldAmend;
private $conduit;
private $conduitURI;
private $conduitCredentials;
private $conduitAuthenticated;
private $forcedConduitVersion;
private $conduitTimeout;
private $userPHID;
private $userName;
private $repositoryAPI;
+ private $configurationManager;
private $workingCopy;
private $arguments;
private $passedArguments;
private $command;
private $stashed;
private $projectInfo;
private $arcanistConfiguration;
private $parentWorkflow;
private $workingDirectory;
private $repositoryVersion;
private $changeCache = array();
public function __construct() {
}
abstract public function run();
/**
* Finalizes any cleanup operations that need to occur regardless of
* whether the command succeeded or failed.
*/
public function finalize() {
$this->finalizeWorkingCopy();
}
/**
* Return the command used to invoke this workflow from the command like,
* e.g. "help" for @{class:ArcanistHelpWorkflow}.
*
* @return string The command a user types to invoke this workflow.
*/
abstract public function getWorkflowName();
/**
* Return console formatted string with all command synopses.
*
* @return string 6-space indented list of available command synopses.
*/
abstract public function getCommandSynopses();
/**
* Return console formatted string with command help printed in `arc help`.
*
* @return string 10-space indented help to use the command.
*/
abstract public function getCommandHelp();
/* -( 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;
}
/**
* Returns the URI the conduit connection within the workflow uses.
*
* @return string
* @task conduit
*/
final public function getConduitURI() {
return $this->conduitURI;
}
/**
* 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);
if ($this->conduitTimeout) {
$this->conduit->setTimeout($this->conduitTimeout);
}
- $user = $this->getConfigFromWhateverSourceAvailiable('http.basicauth.user');
- $pass = $this->getConfigFromWhateverSourceAvailiable('http.basicauth.pass');
+ $user = $this->getConfigFromAnySource('http.basicauth.user');
+ $pass = $this->getConfigFromAnySource('http.basicauth.pass');
if ($user !== null && $pass !== null) {
$this->conduit->setBasicAuthCredentials($user, $pass);
}
return $this;
}
- final public function getConfigFromWhateverSourceAvailiable($key) {
- if ($this->requiresWorkingCopy()) {
- $working_copy = $this->getWorkingCopy();
- return $working_copy->getConfigFromAnySource($key);
- } else {
- $global_config = self::readGlobalArcConfig();
- $pval = idx($global_config, $key);
-
- if ($pval === null) {
- $system_config = self::readSystemArcConfig();
- $pval = idx($system_config, $key);
- }
-
- return $pval;
- }
+ final public function getConfigFromAnySource($key) {
+ return $this->configurationManager->getConfigFromAnySource($key);
}
/**
* 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->isConduitAuthenticated()) {
throw new Exception(
"You may not set new credentials after authenticating conduit.");
}
$this->conduitCredentials = $credentials;
return $this;
}
/**
* Force arc to identify with a specific Conduit version during the
* protocol handshake. This is primarily useful for development (especially
* for sending diffs which bump the client Conduit version), since the client
* still actually speaks the builtin version of the protocol.
*
* Controlled by the --conduit-version flag.
*
* @param int Version the client should pretend to be.
* @return this
* @task conduit
*/
public function forceConduitVersion($version) {
$this->forcedConduitVersion = $version;
return $this;
}
/**
* Get the protocol version the client should identify with.
*
* @return int Version the client should claim to be.
* @task conduit
*/
public function getConduitVersion() {
return nonempty($this->forcedConduitVersion, 6);
}
/**
* Override the default timeout for Conduit.
*
* Controlled by the --conduit-timeout flag.
*
* @param float Timeout, in seconds.
* @return this
* @task conduit
*/
public function setConduitTimeout($timeout) {
$this->conduitTimeout = $timeout;
if ($this->conduit) {
$this->conduit->setConduitTimeout($timeout);
}
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:getUserPHID} 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->isConduitAuthenticated()) {
return $this;
}
$this->establishConduit();
$credentials = $this->conduitCredentials;
try {
if (!$credentials) {
throw new Exception(
"Set conduit credentials with setConduitCredentials() before ".
"authenticating conduit!");
}
if (empty($credentials['user'])) {
throw new ConduitClientException('ERR-INVALID-USER',
'Empty user in credentials.');
}
if (empty($credentials['certificate'])) {
throw new ConduitClientException('ERR-NO-CERTIFICATE',
'Empty certificate in credentials.');
}
$description = idx($credentials, 'description', '');
$user = $credentials['user'];
$certificate = $credentials['certificate'];
$connection = $this->getConduit()->callMethodSynchronous(
'conduit.connect',
array(
'client' => 'arc',
'clientVersion' => $this->getConduitVersion(),
'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') {
$conduit_uri = $this->conduitURI;
$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 if ($ex->getErrorCode() == 'NEW-ARC-VERSION') {
// Cleverly disguise this as being AWESOME!!!
echo phutil_console_format("**New Version Available!**\n\n");
echo phutil_console_wrap($ex->getMessage());
echo "\n\n";
echo "In most cases, arc can be upgraded automatically.\n";
$ok = phutil_console_confirm(
"Upgrade arc now?",
$default_no = false);
if (!$ok) {
throw $ex;
}
$root = dirname(phutil_get_library_root('arcanist'));
chdir($root);
$err = phutil_passthru('%s upgrade', $root.'/bin/arc');
if (!$err) {
echo "\nTry running your arc command again.\n";
}
exit(1);
} else {
throw $ex;
}
}
$this->userName = $user;
$this->userPHID = $connection['userPHID'];
$this->conduitAuthenticated = true;
return $this;
}
/**
* @return bool True if conduit is authenticated, false otherwise.
* @task conduit
*/
final protected function isConduitAuthenticated() {
return (bool)$this->conduitAuthenticated;
}
/**
* 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.
*
* @return phid Authenticated user PHID.
* @task conduit
*/
final public function getUserPHID() {
if (!$this->userPHID) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires authentication, override ".
"requiresAuthentication() to return true.");
}
return $this->userPHID;
}
/**
* Deprecated. See @{method:getUserPHID}.
*
* @deprecated
*/
final public function getUserGUID() {
phutil_deprecated(
'ArcanistBaseWorkflow::getUserGUID',
'This method has been renamed to getUserPHID().');
return $this->getUserPHID();
}
/**
* 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(
ArcanistConfiguration $arcanist_configuration) {
$this->arcanistConfiguration = $arcanist_configuration;
return $this;
}
public function getArcanistConfiguration() {
return $this->arcanistConfiguration;
}
+ public function setConfigurationManager(
+ ArcanistConfigurationManager $arcanist_configuration_manager) {
+
+ $this->configurationManager = $arcanist_configuration_manager;
+ return $this;
+ }
+
+ public function getConfigurationManager() {
+ return $this->configurationManager;
+ }
+
public function requiresWorkingCopy() {
return false;
}
public function desiresWorkingCopy() {
return false;
}
public function requiresRepositoryAPI() {
return false;
}
public function desiresRepositoryAPI() {
return false;
}
public function setCommand($command) {
$this->command = $command;
return $this;
}
public function getCommand() {
return $this->command;
}
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);
+ $workflow->setConfigurationManager($this->getConfigurationManager());
if ($this->repositoryAPI) {
$workflow->setRepositoryAPI($this->repositoryAPI);
}
if ($this->userPHID) {
$workflow->userPHID = $this->getUserPHID();
$workflow->userName = $this->getUserName();
}
if ($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) {
return idx($this->arguments, $key, $default);
}
public function getPassedArguments() {
return $this->passedArguments;
}
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) {
$this->passedArguments = $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;
}
}
foreach ($spec as $long => $options) {
if (!empty($options['repeat'])) {
$dict[$long] = array();
}
}
$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)) {
$corrected = ArcanistConfiguration::correctArgumentSpelling(
$arg_key,
array_keys($spec));
if (count($corrected) == 1) {
PhutilConsole::getConsole()->writeErr(
pht(
"(Assuming '%s' is the British spelling of '%s'.)",
'--'.$arg_key,
'--'.head($corrected))."\n");
$arg_key = head($corrected);
} else {
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.");
}
if (!empty($options['repeat'])) {
$dict[$arg_key][] = $args[$ii + 1];
} else {
$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) {
+ $working_copy = $this->getConfigurationManager()->getWorkingCopyIdentity();
+ if (!$working_copy) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a working copy, override ".
"requiresWorkingCopy() to return true.");
}
- return $this->workingCopy;
+ return $working_copy;
}
public function setWorkingCopy(
ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
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']);
}
public function setCommitMode($mode) {
$this->commitMode = $mode;
return $this;
}
public function finalizeWorkingCopy() {
if ($this->stashed) {
$api = $this->getRepositoryAPI();
$api->unstashChanges();
echo "Restored stashed changes to the working directory.\n";
}
}
public function requireCleanWorkingCopy() {
$api = $this->getRepositoryAPI();
$must_commit = array();
$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.\n");
} 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.\n");
} else if ($api instanceof ArcanistMercurialAPI) {
echo phutil_console_wrap(
"Since you don't have '.hgignore' rules for these files, you ".
"may have forgotten to 'hg add' them to your commit.\n");
}
if ($this->askForAdd($untracked)) {
$api->addToCommit($untracked);
$must_commit += array_flip($untracked);
} else if ($this->commitMode == self::COMMIT_DISABLE) {
$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) {
echo "You have unstaged changes in this working copy.\n\n".
$working_copy_desc.
" Unstaged changes in working copy:\n".
" ".implode("\n ", $unstaged)."\n";
if ($this->askForAdd($unstaged)) {
$api->addToCommit($unstaged);
$must_commit += array_flip($unstaged);
} else {
- $permit_autostash = $this->getWorkingCopy()->getConfigFromAnySource(
+ $permit_autostash = $this->getConfigFromAnySource(
'arc.autostash',
false);
if ($permit_autostash && $api->canStashChanges()) {
echo "Stashing uncommitted changes. (You can restore them with ".
"`git stash pop`.)\n";
$api->stashChanges();
$this->stashed = true;
} else {
throw new ArcanistUsageException(
"Stage and commit (or revert) them before proceeding.");
}
}
}
$uncommitted = $api->getUncommittedChanges();
foreach ($uncommitted as $key => $path) {
if (array_key_exists($path, $must_commit)) {
unset($uncommitted[$key]);
}
}
if ($uncommitted) {
echo "You have uncommitted changes in this working copy.\n\n".
$working_copy_desc.
" Uncommitted changes in working copy:\n".
" ".implode("\n ", $uncommitted)."\n";
if ($this->askForAdd($uncommitted)) {
$must_commit += array_flip($uncommitted);
} else {
throw new ArcanistUncommittedChangesException(
"Commit (or revert) them before proceeding.");
}
}
if ($must_commit) {
if ($this->shouldAmend) {
$commit = head($api->getLocalCommitInformation());
$api->amendCommit($commit['message']);
} else if ($api->supportsLocalCommits()) {
$api->doCommit(self::AUTO_COMMIT_TITLE);
}
}
}
private function shouldAmend() {
$api = $this->getRepositoryAPI();
if ($this->isHistoryImmutable() || !$api->supportsAmend()) {
return false;
}
$commits = $api->getLocalCommitInformation();
if (!$commits) {
return false;
}
$commit = reset($commits);
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$commit['message']);
if ($message->getGitSVNBaseRevision()) {
return false;
}
if ($api->getAuthor() != $commit['author']) {
return false;
}
if ($message->getRevisionID() && $this->getArgument('create')) {
return false;
}
// TODO: Check commits since tracking branch. If empty then return false.
$repository = $this->loadProjectRepository();
if ($repository) {
$callsign = $repository['callsign'];
$known_commits = $this->getConduit()->callMethodSynchronous(
'diffusion.getcommits',
array('commits' => array('r'.$callsign.$commit['commit'])));
if (ifilter($known_commits, 'error', $negate = true)) {
return false;
}
}
if (!$message->getRevisionID()) {
return true;
}
$in_working_copy = $api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
array(
'authors' => array($this->getUserPHID()),
'status' => 'status-open',
));
if ($in_working_copy) {
return true;
}
return false;
}
private function askForAdd(array $files) {
if ($this->commitMode == self::COMMIT_DISABLE) {
return false;
}
if ($this->shouldAmend === null) {
$this->shouldAmend = $this->shouldAmend();
}
if ($this->commitMode == self::COMMIT_ENABLE) {
return true;
}
if ($this->shouldAmend) {
$prompt = pht(
'Do you want to amend these files to the commit?',
count($files));
} else {
$prompt = pht(
'Do you want to add these files to the commit?',
count($files));
}
return phutil_console_confirm($prompt);
}
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);
// since the conduit method has changes, assume that these fields
// could be unset
$bundle->setProjectID(idx($diff, 'projectName'));
$bundle->setBaseRevision(idx($diff, 'sourceControlBaseRevision'));
$bundle->setRevisionID(idx($diff, 'revisionID'));
$bundle->setAuthorName(idx($diff, 'authorName'));
$bundle->setAuthorEmail(idx($diff, 'authorEmail'));
return $bundle;
}
/**
* Return a list of lines changed by the current diff, or ##null## if the
* change list is meaningless (for example, because the path is a directory
* or binary file).
*
* @param string Path within the repository.
* @param string Change selection mode (see ArcanistDiffHunk).
* @return list|null List of changed line numbers, or null to indicate that
* the path is not a line-oriented text file.
*/
protected function getChangedLines($path, $mode) {
$repository_api = $this->getRepositoryAPI();
$full_path = $repository_api->getPath($path);
if (is_dir($full_path)) {
return null;
}
if (!file_exists($full_path)) {
return null;
}
$change = $this->getChange($path);
if ($change->getFileType() !== ArcanistDiffChangeType::FILE_TEXT) {
return null;
}
$lines = $change->getChangedLines($mode);
return array_keys($lines);
}
protected function getChange($path) {
$repository_api = $this->getRepositoryAPI();
// TODO: Very gross
$is_git = ($repository_api instanceof ArcanistGitAPI);
$is_hg = ($repository_api instanceof ArcanistMercurialAPI);
$is_svn = ($repository_api instanceof ArcanistSubversionAPI);
if ($is_svn) {
// NOTE: In SVN, we don't currently support a "get all local changes"
// operation, so special case it.
if (empty($this->changeCache[$path])) {
$diff = $repository_api->getRawDiffText($path);
$parser = $this->newDiffParser();
$changes = $parser->parseDiff($diff);
if (count($changes) != 1) {
throw new Exception("Expected exactly one change.");
}
$this->changeCache[$path] = reset($changes);
}
} else if ($is_git || $is_hg) {
if (empty($this->changeCache)) {
$changes = $repository_api->getAllLocalChanges();
foreach ($changes as $change) {
$this->changeCache[$change->getCurrentPath()] = $change;
}
}
} else {
throw new Exception("Missing VCS support.");
}
if (empty($this->changeCache[$path])) {
if ($is_git) {
// 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 normalizeRevisionID($revision_id) {
return preg_replace('/^D/i', '', $revision_id);
}
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 getSystemArcConfigLocation() {
- if (phutil_is_windows()) {
- return Filesystem::resolvePath(
- 'Phabricator/Arcanist/config',
- getenv('ProgramData'));
- } else {
- return '/etc/arcconfig';
- }
- }
-
- public static function readSystemArcConfig() {
- $system_config = array();
- $system_config_path = self::getSystemArcConfigLocation();
- if (Filesystem::pathExists($system_config_path)) {
- $file = Filesystem::readFile($system_config_path);
- if ($file) {
- $system_config = json_decode($file, true);
- }
- }
- return $system_config;
- }
- public static function getUserConfigurationFileLocation() {
- if (phutil_is_windows()) {
- return getenv('APPDATA').'/.arcrc';
- } else {
- return getenv('HOME').'/.arcrc';
- }
- }
-
- public static function readUserConfigurationFile() {
- $user_config = array();
- $user_config_path = self::getUserConfigurationFileLocation();
- if (Filesystem::pathExists($user_config_path)) {
-
- if (!phutil_is_windows()) {
- $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);
-
- // 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);
- $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;
- }
-
-
- public static function writeUserConfigurationFile($config) {
- $json_encoder = new PhutilJSON();
- $json = $json_encoder->encodeFormatted($config);
-
- $path = self::getUserConfigurationFileLocation();
- Filesystem::writeFile($path, $json);
-
- if (!phutil_is_windows()) {
- execx('chmod 600 %s', $path);
- }
- }
-
- public static function readGlobalArcConfig() {
- return idx(self::readUserConfigurationFile(), 'config', array());
- }
-
- public static function writeGlobalArcConfig(array $options) {
- $config = self::readUserConfigurationFile();
- $config['config'] = $options;
- self::writeUserConfigurationFile($config);
- }
-
- public function readLocalArcConfig() {
- $local = array();
- $file = $this->readScratchFile('config');
- if ($file) {
- $local = json_decode($file, true);
- }
-
- return $local;
- }
-
- public function writeLocalArcConfig(array $config) {
- $json_encoder = new PhutilJSON();
- $json = $json_encoder->encodeFormatted($config);
-
- $this->writeScratchFile('config', $json);
-
- return $this;
- }
-
-
/**
* Write a message to stderr so that '--json' flags or stdout which is meant
* to be piped somewhere aren't disrupted.
*
* @param string Message to write to stderr.
* @return void
*/
protected function writeStatusMessage($msg) {
fwrite(STDERR, $msg);
}
protected function isHistoryImmutable() {
$repository_api = $this->getRepositoryAPI();
- $working_copy = $this->getWorkingCopy();
- $config = $working_copy->getConfigFromAnySource('history.immutable');
+ $config = $this->getConfigFromAnySource('history.immutable');
if ($config !== null) {
return $config;
}
return $repository_api->isHistoryDefaultImmutable();
}
/**
* Workflows like 'lint' and 'unit' operate on a list of working copy paths.
* The user can either specify the paths explicitly ("a.js b.php"), or by
* specfifying a revision ("--rev a3f10f1f") to select all paths modified
* since that revision, or by omitting both and letting arc choose the
* default relative revision.
*
* This method takes the user's selections and returns the paths that the
* workflow should act upon.
*
* @param list List of explicitly provided paths.
* @param string|null Revision name, if provided.
* @param mask Mask of ArcanistRepositoryAPI flags to exclude.
* Defaults to ArcanistRepositoryAPI::FLAG_UNTRACKED.
* @return list List of paths the workflow should act on.
*/
protected function selectPathsForWorkflow(
array $paths,
$rev,
$omit_mask = null) {
if ($omit_mask === null) {
$omit_mask = ArcanistRepositoryAPI::FLAG_UNTRACKED;
}
if ($paths) {
$working_copy = $this->getWorkingCopy();
foreach ($paths as $key => $path) {
$full_path = Filesystem::resolvePath($path);
if (!Filesystem::pathExists($full_path)) {
throw new ArcanistUsageException("Path '{$path}' does not exist!");
}
$relative_path = Filesystem::readablePath(
$full_path,
$working_copy->getProjectRoot());
$paths[$key] = $relative_path;
}
} else {
$repository_api = $this->getRepositoryAPI();
if ($rev) {
$this->parseBaseCommitArgument(array($rev));
}
$paths = $repository_api->getWorkingCopyStatus();
foreach ($paths as $path => $flags) {
if ($flags & $omit_mask) {
unset($paths[$path]);
}
}
$paths = array_keys($paths);
}
return array_values($paths);
}
protected function renderRevisionList(array $revisions) {
$list = array();
foreach ($revisions as $revision) {
$list[] = ' - D'.$revision['id'].': '.$revision['title']."\n";
}
return implode('', $list);
}
/* -( Scratch Files )------------------------------------------------------ */
/**
* Try to read a scratch file, if it exists and is readable.
*
* @param string Scratch file name.
* @return mixed String for file contents, or false for failure.
* @task scratch
*/
protected function readScratchFile($path) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->readScratchFile($path);
}
/**
* Try to read a scratch JSON file, if it exists and is readable.
*
* @param string Scratch file name.
* @return array Empty array for failure.
* @task scratch
*/
protected function readScratchJSONFile($path) {
$file = $this->readScratchFile($path);
if (!$file) {
return array();
}
return json_decode($file, true);
}
/**
* Try to write a scratch file, if there's somewhere to put it and we can
* write there.
*
* @param string Scratch file name to write.
* @param string Data to write.
* @return bool True on success, false on failure.
* @task scratch
*/
protected function writeScratchFile($path, $data) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->writeScratchFile($path, $data);
}
/**
* Try to write a scratch JSON file, if there's somewhere to put it and we can
* write there.
*
* @param string Scratch file name to write.
* @param array Data to write.
* @return bool True on success, false on failure.
* @task scratch
*/
protected function writeScratchJSONFile($path, array $data) {
return $this->writeScratchFile($path, json_encode($data));
}
/**
* Try to remove a scratch file.
*
* @param string Scratch file name to remove.
* @return bool True if the file was removed successfully.
* @task scratch
*/
protected function removeScratchFile($path) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->removeScratchFile($path);
}
/**
* Get a human-readable description of the scratch file location.
*
* @param string Scratch file name.
* @return mixed String, or false on failure.
* @task scratch
*/
protected function getReadableScratchFilePath($path) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->getReadableScratchFilePath($path);
}
/**
* Get the path to a scratch file, if possible.
*
* @param string Scratch file name.
* @return mixed File path, or false on failure.
* @task scratch
*/
protected function getScratchFilePath($path) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->getScratchFilePath($path);
}
protected function getRepositoryEncoding() {
$default = 'UTF-8';
return nonempty(idx($this->getProjectInfo(), 'encoding'), $default);
}
protected function getProjectInfo() {
if ($this->projectInfo === null) {
$project_id = $this->getWorkingCopy()->getProjectID();
if (!$project_id) {
$this->projectInfo = array();
} else {
try {
$this->projectInfo = $this->getConduit()->callMethodSynchronous(
'arcanist.projectinfo',
array(
'name' => $project_id,
));
} catch (ConduitClientException $ex) {
if ($ex->getErrorCode() != 'ERR-BAD-ARCANIST-PROJECT') {
throw $ex;
}
// TODO: Implement a proper query method that doesn't throw on
// project not found. We just swallow this because some pathways,
// like Git with uncommitted changes in a repository with a new
// project ID, may attempt to access project information before
// the project is created. See T2153.
return array();
}
}
}
return $this->projectInfo;
}
protected function loadProjectRepository() {
$project = $this->getProjectInfo();
if (isset($project['repository'])) {
return $project['repository'];
}
// NOTE: The rest of the code is here for backwards compatibility.
$repository_phid = idx($project, 'repositoryPHID');
if (!$repository_phid) {
return array();
}
$repositories = $this->getConduit()->callMethodSynchronous(
'repository.query',
array());
$repositories = ipull($repositories, null, 'phid');
return idx($repositories, $repository_phid, array());
}
protected function newInteractiveEditor($text) {
$editor = new PhutilInteractiveEditor($text);
- $preferred = $this->getWorkingCopy()->getConfigFromAnySource('editor');
+ $preferred = $this->getConfigFromAnySource('editor');
if ($preferred) {
$editor->setPreferredEditor($preferred);
}
return $editor;
}
protected function newDiffParser() {
$parser = new ArcanistDiffParser();
if ($this->repositoryAPI) {
$parser->setRepositoryAPI($this->getRepositoryAPI());
}
$parser->setWriteDiffOnFailure(true);
return $parser;
}
protected function resolveCall(ConduitFuture $method, $timeout = null) {
try {
return $method->resolve($timeout);
} catch (ConduitClientException $ex) {
if ($ex->getErrorCode() == 'ERR-CONDUIT-CALL') {
echo phutil_console_wrap(
"This feature requires a newer version of Phabricator. Please ".
"update it using these instructions: ".
"http://www.phabricator.com/docs/phabricator/article/".
"Installation_Guide.html#updating-phabricator\n\n");
}
throw $ex;
}
}
protected function dispatchEvent($type, array $data) {
$data += array(
'workflow' => $this,
);
$event = new PhutilEvent($type, $data);
PhutilEventEngine::dispatchEvent($event);
return $event;
}
public function parseBaseCommitArgument(array $argv) {
if (!count($argv)) {
return;
}
$api = $this->getRepositoryAPI();
if (!$api->supportsCommitRanges()) {
throw new ArcanistUsageException(
"This version control system does not support commit ranges.");
}
if (count($argv) > 1) {
throw new ArcanistUsageException(
"Specify exactly one base commit. The end of the commit range is ".
"always the working copy state.");
}
$api->setBaseCommit(head($argv));
return $this;
}
protected function getRepositoryVersion() {
if (!$this->repositoryVersion) {
$api = $this->getRepositoryAPI();
$commit = $api->getSourceControlBaseRevision();
$versions = array('' => $commit);
foreach ($api->getChangedFiles($commit) as $path => $mask) {
$versions[$path] = (Filesystem::pathExists($path)
? md5_file($path)
: '');
}
$this->repositoryVersion = md5(json_encode($versions));
}
return $this->repositoryVersion;
}
}
diff --git a/src/workflow/ArcanistBrowseWorkflow.php b/src/workflow/ArcanistBrowseWorkflow.php
index 347824bc..55dfc613 100644
--- a/src/workflow/ArcanistBrowseWorkflow.php
+++ b/src/workflow/ArcanistBrowseWorkflow.php
@@ -1,149 +1,149 @@
<?php
/*
* Copyright 2012 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.
*/
/**
* Browse file in Diffusion.
*
* @group workflow
*/
final class ArcanistBrowseWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'browse';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**browse** [__options__] __path__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git
Browse file in Diffusion (Web interface).
Set the 'browser' value using 'arc set-config' to select a browser. If
no browser is set, the command will try to guess which browser to use.
EOTEXT
);
}
public function getArguments() {
return array(
'branch' => array(
'param' => 'branch_name',
'help' =>
"Select branch name to view (On server). Defaults to 'master'."
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function run() {
$repository_api = $this->getRepositoryAPI();
$project_root = $this->getWorkingCopy()->getProjectRoot();
$in_paths = $this->getArgument('paths');
$paths = array();
foreach ($in_paths as $key => $path) {
$path = preg_replace('/:([0-9]+)$/', '$\1', $path);
$full_path = Filesystem::resolvePath($path);
$paths[$key] = Filesystem::readablePath(
$full_path,
$project_root);
}
if (!$paths) {
throw new ArcanistUsageException("Specify a path to browse.");
}
$base_uri = $this->getBaseURI();
$browser = $this->getBrowserCommand();
foreach ($paths as $path) {
$ret_code = phutil_passthru("%s %s", $browser, $base_uri . $path);
if ($ret_code) {
throw new ArcanistUsageException(
"It seems we failed to open the browser; perhaps you should try to ".
"set the 'browser' config option. The command we tried to use was: ".
$browser);
}
}
return 0;
}
private function getBaseURI() {
$conduit = $this->getConduit();
$project_id = $this->getWorkingCopy()->getProjectID();
$project_info = $this->getConduit()->callMethodSynchronous(
'arcanist.projectinfo',
array(
'name' => $project_id,
));
$repo_info = $project_info['repository'];
$branch = $this->getArgument('branch', 'master');
return $repo_info['uri'].'browse/'.$branch.'/';
}
private function getBrowserCommand() {
- $config = $this->getWorkingCopy()->getConfigFromAnySource('browser');
+ $config = $this->getConfigFromAnySource('browser');
if ($config) {
return $config;
}
if (phutil_is_windows()) {
return "start";
}
$candidates = array("sensible-browser", "xdg-open", "open");
// on many Linuxes, "open" exists and is not the right program.
foreach ($candidates as $cmd) {
list($ret_code) = exec_manual("which %s", $cmd);
if ($ret_code == 0) {
return $cmd;
}
}
throw new ArcanistUsageException(
"Could not find a browser to run; Try setting the 'browser' option " .
"using arc set-config.");
}
}
diff --git a/src/workflow/ArcanistGetConfigWorkflow.php b/src/workflow/ArcanistGetConfigWorkflow.php
index 955ebd58..bb37f5a2 100644
--- a/src/workflow/ArcanistGetConfigWorkflow.php
+++ b/src/workflow/ArcanistGetConfigWorkflow.php
@@ -1,90 +1,95 @@
<?php
/**
* Read configuration settings.
*
* @group workflow
*/
final class ArcanistGetConfigWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'get-config';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**get-config** -- [__name__ ...]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Reads an arc configuration option. With no argument, reads all
options.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'argv',
);
}
public function desiresRepositoryAPI() {
return true;
}
public function run() {
$argv = $this->getArgument('argv');
$settings = new ArcanistSettings();
+ $configuration_manager = $this->getConfigurationManager();
$configs = array(
- 'system' => self::readSystemArcConfig(),
- 'global' => self::readGlobalArcConfig(),
- 'project' => $this->getWorkingCopy()->getProjectConfig(),
- 'local' => $this->readLocalArcConfig(),
+ ArcanistConfigurationManager::CONFIG_SOURCE_SYSTEM =>
+ $configuration_manager->readSystemArcConfig(),
+ ArcanistConfigurationManager::CONFIG_SOURCE_USER =>
+ $configuration_manager->readUserArcConfig(),
+ ArcanistConfigurationManager::CONFIG_SOURCE_PROJECT =>
+ $this->getWorkingCopy()->readProjectConfig(),
+ ArcanistConfigurationManager::CONFIG_SOURCE_LOCAL =>
+ $configuration_manager->readLocalArcConfig(),
);
if ($argv) {
$keys = $argv;
} else {
$keys = array_mergev(array_map('array_keys', $configs));
$keys = array_unique($keys);
sort($keys);
}
$multi = (count($keys) > 1);
foreach ($keys as $key) {
if ($multi) {
echo "{$key}\n";
}
foreach ($configs as $name => $config) {
switch ($name) {
- case 'project':
+ case ArcanistConfigurationManager::CONFIG_SOURCE_PROJECT:
// Respect older names in project config.
$val = $this->getWorkingCopy()->getConfig($key);
break;
default:
$val = idx($config, $key);
break;
}
if ($val === null) {
continue;
}
$val = $settings->formatConfigValueForDisplay($key, $val);
printf("% 10.10s: %s\n", $name, $val);
}
if ($multi) {
echo "\n";
}
}
return 0;
}
}
diff --git a/src/workflow/ArcanistInstallCertificateWorkflow.php b/src/workflow/ArcanistInstallCertificateWorkflow.php
index fb1cd46e..033b358f 100644
--- a/src/workflow/ArcanistInstallCertificateWorkflow.php
+++ b/src/workflow/ArcanistInstallCertificateWorkflow.php
@@ -1,139 +1,140 @@
<?php
/**
* Installs arcanist certificates.
*
* @group workflow
*/
final class ArcanistInstallCertificateWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'install-certificate';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**install-certificate** [uri]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: http, https
Installs Conduit credentials into your ~/.arcrc for the given install
of Phabricator. You need to do this before you can use 'arc', as it
enables 'arc' to link your command-line activity with your account on
the web. Run this command from within a project directory to install
that project's certificate, or specify an explicit URI (like
"https://phabricator.example.com/").
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'uri',
);
}
public function shouldShellComplete() {
return false;
}
public function requiresConduit() {
return false;
}
public function requiresWorkingCopy() {
return false;
}
public function run() {
$uri = $this->determineConduitURI();
$this->setConduitURI($uri);
+ $configuration_manager = $this->getConfigurationManager();
echo "Installing certificate for '{$uri}'...\n";
- $config = self::readUserConfigurationFile();
+ $config = $configuration_manager->readUserConfigurationFile();
echo "Trying to connect to server...\n";
$conduit = $this->establishConduit()->getConduit();
try {
$conduit->callMethodSynchronous('conduit.ping', array());
} catch (Exception $ex) {
throw new ArcanistUsageException(
"Failed to connect to server: ".$ex->getMessage());
}
echo "Connection OK!\n";
$token_uri = new PhutilURI($uri);
$token_uri->setPath('/conduit/token/');
echo "\n";
echo phutil_console_format("**LOGIN TO PHABRICATOR**\n");
echo "Open this page in your browser and login to Phabricator if ".
"necessary:\n";
echo "\n";
echo " {$token_uri}\n";
echo "\n";
echo "Then paste the token on that page below.";
do {
$token = phutil_console_prompt('Paste token from that page:');
$token = trim($token);
if (strlen($token)) {
break;
}
} while (true);
echo "\n";
echo "Downloading authentication certificate...\n";
$info = $conduit->callMethodSynchronous(
'conduit.getcertificate',
array(
'token' => $token,
'host' => $uri,
));
$user = $info['username'];
echo "Installing certificate for '{$user}'...\n";
$config['hosts'][$uri] = array(
'user' => $user,
'cert' => $info['certificate'],
);
echo "Writing ~/.arcrc...\n";
- self::writeUserConfigurationFile($config);
+ $configuration_manager->writeUserConfigurationFile($config);
echo phutil_console_format(
"<bg:green>** SUCCESS! **</bg> Certificate installed.\n");
return 0;
}
private function determineConduitURI() {
$uri = $this->getArgument('uri');
if (count($uri) > 1) {
throw new ArcanistUsageException("Specify at most one URI.");
} else if (count($uri) == 1) {
$uri = reset($uri);
} else {
$conduit_uri = $this->getConduitURI();
if (!$conduit_uri) {
throw new ArcanistUsageException(
"Specify an explicit URI or run this command from within a project ".
"which is configured with a .arcconfig.");
}
$uri = $conduit_uri;
}
$uri = new PhutilURI($uri);
$uri->setPath('/api/');
return (string)$uri;
}
}
diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php
index 714f1918..d1b9dc31 100644
--- a/src/workflow/ArcanistLandWorkflow.php
+++ b/src/workflow/ArcanistLandWorkflow.php
@@ -1,1076 +1,1075 @@
<?php
/**
* Lands a branch by rebasing, merging and amending it.
*
* @group workflow
*/
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
private $isGit;
private $isGitSvn;
private $isHg;
private $isHgSvn;
private $oldBranch;
private $branch;
private $onto;
private $ontoRemoteBranch;
private $remote;
private $useSquash;
private $keepBranch;
private $shouldUpdateWithRebase;
private $branchType;
private $ontoType;
private $preview;
private $revision;
private $messageFile;
public function getWorkflowName() {
return 'land';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**land** [__options__] [__branch__] [--onto __master__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, hg
Land an accepted change (currently sitting in local feature branch
__branch__) onto __master__ and push it to the remote. Then, delete
the feature branch. If you omit __branch__, the current branch will
be used.
In mutable repositories, this will perform a --squash merge (the
entire branch will be represented by one commit on __master__). In
immutable repositories (or when --merge is provided), it will perform
a --no-ff merge (the branch will always be merged into __master__ with
a merge commit).
Under hg, bookmarks can be landed the same way as branches.
EOTEXT
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function getArguments() {
return array(
'onto' => array(
'param' => 'master',
'help' => "Land feature branch onto a branch other than the default ".
"('master' in git, 'default' in hg). You can change the ".
"default by setting 'arc.land.onto.default' with ".
"`arc set-config` or for the entire project in .arcconfig.",
),
'hold' => array(
'help' => "Prepare the change to be pushed, but do not actually ".
"push it.",
),
'keep-branch' => array(
'help' => "Keep the feature branch after pushing changes to the ".
"remote (by default, it is deleted).",
),
'remote' => array(
'param' => 'origin',
'help' => "Push to a remote other than the default ('origin' in git).",
),
'merge' => array(
'help' => 'Perform a --no-ff merge, not a --squash merge. If the '.
'project is marked as having an immutable history, this is '.
'the default behavior.',
'supports' => array(
'git',
),
'nosupport' => array(
'hg' => 'Use the --squash strategy when landing in mercurial.',
),
),
'squash' => array(
'help' => 'Perform a --squash merge, not a --no-ff merge. If the '.
'project is marked as having a mutable history, this is '.
'the default behavior.',
'conflicts' => array(
'merge' => '--merge and --squash are conflicting merge strategies.',
),
),
'delete-remote' => array(
'help' => 'Delete the feature branch in the remote after '.
'landing it.',
'conflicts' => array(
'keep-branch' => true,
),
),
'update-with-rebase' => array(
'help' => 'When updating the feature branch, use rebase instead of '.
'merge. This might make things work better in some cases.'.
' Set arc.land.update.default to \'rebase\' to make this '.
'default.',
'conflicts' => array(
'merge' => 'The --merge strategy does not update the feature branch.',
'update-with-merge' => 'Cannot be used with --update-with-merge.',
),
'supports' => array(
'git',
),
),
'update-with-merge' => array(
'help' => 'When updating the feature branch, use merge instead of '.
'rebase. This is the default behavior. '.
'Setting arc.land.update.default to \'merge\' can also '.
'be used to make this the default.',
'conflicts' => array(
'merge' => 'The --merge strategy does not update the feature branch.',
'update-with-rebase' => 'Cannot be used with --update-with-rebase.',
),
'supports' => array(
'git',
),
),
'revision' => array(
'param' => 'id',
'help' => 'Use the message from a specific revision, rather than '.
'inferring the revision based on branch content.',
),
'preview' => array(
'help' => 'Prints the commits that would be landed. Does not actually '.
'modify or land the commits.'
),
'*' => 'branch',
);
}
public function run() {
$this->readArguments();
$this->validate();
try {
$this->pullFromRemote();
} catch (Exception $ex) {
$this->restoreBranch();
throw $ex;
}
$this->printPendingCommits();
if ($this->preview) {
$this->restoreBranch();
return 0;
}
$this->checkoutBranch();
$this->findRevision();
if ($this->useSquash) {
$this->rebase();
$this->squash();
} else {
$this->merge();
}
$this->push();
if (!$this->keepBranch) {
$this->cleanupBranch();
}
if ($this->oldBranch != $this->onto) {
// If we were on some branch A and the user ran "arc land B",
// switch back to A.
if ($this->keepBranch || $this->oldBranch != $this->branch) {
$this->restoreBranch();
}
}
echo "Done.\n";
return 0;
}
private function readArguments() {
$repository_api = $this->getRepositoryAPI();
$this->isGit = $repository_api instanceof ArcanistGitAPI;
$this->isHg = $repository_api instanceof ArcanistMercurialAPI;
if (!$this->isGit && !$this->isHg) {
throw new ArcanistUsageException(
"'arc land' only supports git and mercurial.");
}
if ($this->isGit) {
$repository = $this->loadProjectRepository();
$this->isGitSvn = (idx($repository, 'vcs') == 'svn');
}
if ($this->isHg) {
$this->isHgSvn = $repository_api->isHgSubversionRepo();
}
$branch = $this->getArgument('branch');
if (empty($branch)) {
$branch = $this->getBranchOrBookmark();
if ($branch) {
$this->branchType = $this->getBranchType($branch);
echo "Landing current {$this->branchType} '{$branch}'.\n";
$branch = array($branch);
}
}
if (count($branch) !== 1) {
throw new ArcanistUsageException(
"Specify exactly one branch or bookmark to land changes from.");
}
$this->branch = head($branch);
$this->keepBranch = $this->getArgument('keep-branch');
- $working_copy = $this->getWorkingCopy();
- $update_strategy = $working_copy->getConfigFromAnySource(
+ $update_strategy = $this->getConfigFromAnySource(
'arc.land.update.default',
'merge');
$this->shouldUpdateWithRebase = $update_strategy == 'rebase';
if ($this->getArgument('update-with-rebase')) {
$this->shouldUpdateWithRebase = true;
} else if ($this->getArgument('update-with-merge')) {
$this->shouldUpdateWithRebase = false;
}
$this->preview = $this->getArgument('preview');
if (!$this->branchType) {
$this->branchType = $this->getBranchType($this->branch);
}
$onto_default = $this->isGit ? 'master' : 'default';
$onto_default = nonempty(
- $working_copy->getConfigFromAnySource('arc.land.onto.default'),
+ $this->getConfigFromAnySource('arc.land.onto.default'),
$onto_default);
$this->onto = $this->getArgument('onto', $onto_default);
$this->ontoType = $this->getBranchType($this->onto);
$remote_default = $this->isGit ? 'origin' : '';
$this->remote = $this->getArgument('remote', $remote_default);
if ($this->getArgument('merge')) {
$this->useSquash = false;
} else if ($this->getArgument('squash')) {
$this->useSquash = true;
} else {
$this->useSquash = !$this->isHistoryImmutable();
}
$this->ontoRemoteBranch = $this->onto;
if ($this->isGitSvn) {
$this->ontoRemoteBranch = 'trunk';
} else if ($this->isGit) {
$this->ontoRemoteBranch = $this->remote.'/'.$this->onto;
}
$this->oldBranch = $this->getBranchOrBookmark();
}
private function validate() {
$repository_api = $this->getRepositoryAPI();
if ($this->onto == $this->branch) {
$message =
"You can not land a {$this->branchType} onto itself -- you are trying ".
"to land '{$this->branch}' onto '{$this->onto}'. For more ".
"information on how to push changes, see 'Pushing and Closing ".
"Revisions' in 'Arcanist User Guide: arc diff' in the documentation.";
if (!$this->isHistoryImmutable()) {
$message .= " You may be able to 'arc amend' instead.";
}
throw new ArcanistUsageException($message);
}
if ($this->isHg) {
if ($this->useSquash) {
if (!$repository_api->supportsRebase()) {
throw new ArcanistUsageException(
"You must enable the rebase extension to use ".
"the --squash strategy.");
}
}
if ($this->branchType != $this->ontoType) {
throw new ArcanistUsageException(
"Source {$this->branch} is a {$this->branchType} but destination ".
"{$this->onto} is a {$this->ontoType}. When landing a ".
"{$this->branchType}, the destination must also be a ".
"{$this->branchType}. Use --onto to specify a {$this->branchType}, ".
"or set arc.land.onto.default in .arcconfig.");
}
}
if ($this->isGit) {
list($err) = $repository_api->execManualLocal(
'rev-parse --verify %s',
$this->branch);
if ($err) {
throw new ArcanistUsageException(
"Branch '{$this->branch}' does not exist.");
}
}
$this->requireCleanWorkingCopy();
}
private function checkoutBranch() {
$repository_api = $this->getRepositoryAPI();
if ($this->getBranchOrBookmark() != $this->branch) {
$repository_api->execxLocal(
'checkout %s',
$this->branch);
}
echo phutil_console_format(
"Switched to {$this->branchType} **%s**. Identifying and merging...\n",
$this->branch);
}
private function printPendingCommits() {
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistGitAPI) {
list($out) = $repository_api->execxLocal(
'log --oneline %s %s --',
$this->branch,
'^'.$this->onto);
} else if ($repository_api instanceof ArcanistMercurialAPI) {
$common_ancestor = $repository_api->getCanonicalRevisionName(
hgsprintf('ancestor(%s,%s)',
$this->onto,
$this->branch));
$branch_range = hgsprintf(
'reverse((%s::%s) - %s)',
$common_ancestor,
$this->branch,
$common_ancestor);
list($out) = $repository_api->execxLocal(
'log -r %s --template %s',
$branch_range,
'{node|short} {desc|firstline}\n');
}
if (!trim($out)) {
$this->restoreBranch();
throw new ArcanistUsageException(
"No commits to land from {$this->branch}.");
}
echo "The following commit(s) will be landed:\n\n{$out}\n";
}
private function findRevision() {
$repository_api = $this->getRepositoryAPI();
$this->parseBaseCommitArgument(array($this->ontoRemoteBranch));
$revision_id = $this->getArgument('revision');
if ($revision_id) {
$revision_id = $this->normalizeRevisionID($revision_id);
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => array($revision_id),
));
if (!$revisions) {
throw new ArcanistUsageException("No such revision 'D{$revision_id}'!");
}
} else {
$revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
array(
'authors' => array($this->getUserPHID()),
));
}
if (!count($revisions)) {
throw new ArcanistUsageException(
"arc can not identify which revision exists on {$this->branchType} ".
"'{$this->branch}'. Update the revision with recent changes ".
"to synchronize the {$this->branchType} name and hashes, or use ".
"'arc amend' to amend the commit message at HEAD, or use ".
"'--revision <id>' to select a revision explicitly.");
} else if (count($revisions) > 1) {
$message =
"There are multiple revisions on feature {$this->branchType} ".
"'{$this->branch}' which are not present on '{$this->onto}':\n\n".
$this->renderRevisionList($revisions)."\n".
"Separate these revisions onto different {$this->branchType}s, or use ".
"'--revision <id>' to use the commit message from <id> and land them ".
"all.";
throw new ArcanistUsageException($message);
}
$this->revision = head($revisions);
$rev_status = $this->revision['status'];
$rev_id = $this->revision['id'];
$rev_title = $this->revision['title'];
$rev_auxiliary = idx($this->revision, 'auxiliary', array());
if ($rev_status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
$ok = phutil_console_confirm(
"Revision 'D{$rev_id}: {$rev_title}' has not been ".
"accepted. Continue anyway?");
if (!$ok) {
throw new ArcanistUserAbortException();
}
}
if ($rev_auxiliary) {
$phids = idx($rev_auxiliary, 'phabricator:depends-on', array());
if ($phids) {
$dep_on_revs = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'phids' => $phids,
'status' => 'status-open',
));
$open_dep_revs = array();
foreach ($dep_on_revs as $dep_on_rev) {
$dep_on_rev_id = $dep_on_rev['id'];
$dep_on_rev_title = $dep_on_rev['title'];
$dep_on_rev_status = $dep_on_rev['status'];
$open_dep_revs[$dep_on_rev_id] = $dep_on_rev_title;
}
if (!empty($open_dep_revs)) {
$open_revs = array();
foreach ($open_dep_revs as $id => $title) {
$open_revs[] = " - D".$id.": ".$title;
}
$open_revs = implode("\n", $open_revs);
echo "Revision 'D{$rev_id}: {$rev_title}' depends ".
"on open revisions:\n\n";
echo $open_revs;
$ok = phutil_console_confirm("Continue anyway?");
if (!$ok) {
throw new ArcanistUserAbortException();
}
}
}
}
$message = $this->getConduit()->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $rev_id,
));
$this->messageFile = new TempFile();
Filesystem::writeFile($this->messageFile, $message);
echo "Landing revision 'D{$rev_id}: ".
"{$rev_title}'...\n";
}
private function pullFromRemote() {
$repository_api = $this->getRepositoryAPI();
$local_ahead_of_remote = false;
if ($this->isGit) {
$repository_api->execxLocal('checkout %s', $this->onto);
echo phutil_console_format(
"Switched to branch **%s**. Updating branch...\n",
$this->onto);
try {
$repository_api->execxLocal('pull --ff-only --no-stat');
} catch (CommandException $ex) {
if (!$this->isGitSvn) {
throw $ex;
}
list($out) = $repository_api->execxLocal(
'log %s..%s',
$this->ontoRemoteBranch,
$this->onto);
if (strlen(trim($out))) {
$local_ahead_of_remote = true;
} else {
$repository_api->execxLocal('svn rebase');
}
}
} else if ($this->isHg) {
echo phutil_console_format(
"Updating **%s**...\n",
$this->onto);
try {
list($out, $err) = $repository_api->execxLocal('pull');
$divergedbookmark = $this->onto.'@'.$repository_api->getBranchName();
if (strpos($err, $divergedbookmark) !== false) {
throw new ArcanistUsageException(phutil_console_format(
"Local bookmark **{$this->onto}** has diverged from the ".
"server's **{$this->onto}** (now labeled ".
"**{$divergedbookmark}**). Please resolve this divergence and ".
"run 'arc land' again."));
}
} catch (CommandException $ex) {
$err = $ex->getError();
$stdout = $ex->getStdOut();
// Copied from: PhabricatorRepositoryPullLocalDaemon.php
// NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the
// behavior of "hg pull" to return 1 in case of a successful pull
// with no changes. This behavior has been reverted, but users who
// updated between Feb 1, 2012 and Mar 1, 2012 will have the
// erroring version. Do a dumb test against stdout to check for this
// possibility.
// See: https://github.com/facebook/phabricator/issues/101/
// NOTE: Mercurial has translated versions, which translate this error
// string. In a translated version, the string will be something else,
// like "aucun changement trouve". There didn't seem to be an easy way
// to handle this (there are hard ways but this is not a common
// problem and only creates log spam, not application failures).
// Assume English.
// TODO: Remove this once we're far enough in the future that
// deployment of 2.1 is exceedingly rare?
if ($err != 1 || !preg_match('/no changes found/', $stdout)) {
throw $ex;
}
}
// Pull succeeded. Now make sure master is not on an outgoing change
if ($repository_api->supportsPhases()) {
list($out) = $repository_api->execxLocal(
'log -r %s --template %s', $this->onto, "{phase}");
if ($out != 'public') {
$local_ahead_of_remote = true;
}
} else {
// execManual instead of execx because outgoing returns
// code 1 when there is nothing outgoing
list($err, $out) = $repository_api->execManualLocal(
'outgoing -r %s',
$this->onto);
// $err === 0 means something is outgoing
if ($err === 0) {
$local_ahead_of_remote = true;
}
}
}
if ($local_ahead_of_remote) {
throw new ArcanistUsageException(
"Local {$this->ontoType} '{$this->onto}' is ahead of remote ".
"{$this->ontoType} '{$this->ontoRemoteBranch}', so landing a feature ".
"{$this->ontoType} would push additional changes. Push or reset the ".
"changes in '{$this->onto}' before running 'arc land'.");
}
}
private function rebase() {
$repository_api = $this->getRepositoryAPI();
echo phutil_console_format(
"Rebasing **%s** onto **%s**\n",
$this->branch,
$this->onto);
chdir($repository_api->getPath());
if ($this->isGit) {
if ($this->shouldUpdateWithRebase) {
$err = phutil_passthru('git rebase %s', $this->onto);
if ($err) {
throw new ArcanistUsageException(
"'git rebase {$this->onto}' failed. ".
"You can abort with 'git rebase --abort', ".
"or resolve conflicts and use 'git rebase ".
"--continue' to continue forward. After resolving the rebase, ".
"run 'arc land' again.");
}
} else {
$err = phutil_passthru(
'git merge --no-stat %s -m %s',
$this->onto,
"Automatic merge by 'arc land'");
if ($err) {
throw new ArcanistUsageException(
"'git merge {$this->onto}' failed. ".
"To continue: resolve the conflicts, commit the changes, then run ".
"'arc land' again. To abort: run 'git merge --abort'.");
}
}
} else if ($this->isHg) {
$onto_tip = $repository_api->getCanonicalRevisionName($this->onto);
$common_ancestor = $repository_api->getCanonicalRevisionName(
hgsprintf("ancestor(%s, %s)",
$this->onto,
$this->branch));
// Only rebase if the local branch is not at the tip of the onto branch.
if ($onto_tip != $common_ancestor) {
// keep branch here so later we can decide whether to remove it
$err = $repository_api->execPassthru(
'rebase -d %s --keepbranches',
$this->onto);
if ($err) {
echo phutil_console_format("Aborting rebase\n");
$repository_api->execManualLocal(
'rebase --abort');
$this->restoreBranch();
throw new ArcanistUsageException(
"'hg rebase {$this->onto}' failed and the rebase was aborted. ".
"This is most likely due to conflicts. Manually rebase ".
"{$this->branch} onto {$this->onto}, resolve the conflicts, ".
"then run 'arc land' again.");
}
}
}
$repository_api->reloadWorkingCopy();
}
private function squash() {
$repository_api = $this->getRepositoryAPI();
if ($this->isGit) {
$repository_api->execxLocal('checkout %s', $this->onto);
$repository_api->execxLocal(
'merge --no-stat --squash --ff-only %s',
$this->branch);
} else if ($this->isHg) {
// The hg code is a little more complex than git's because we
// need to handle the case where the landing branch has child branches:
// -a--------b master
// \
// w--x mybranch
// \--y subbranch1
// \--z subbranch2
//
// arc land --branch mybranch --onto master :
// -a--b--wx master
// \--y subbranch1
// \--z subbranch2
$branch_rev_id = $repository_api->getCanonicalRevisionName($this->branch);
// At this point $this->onto has been pulled from remote and
// $this->branch has been rebased on top of onto(by the rebase()
// function). So we're guaranteed to have onto as an ancestor of branch
// when we use first((onto::branch)-onto) below.
$branch_root = $repository_api->getCanonicalRevisionName(
hgsprintf("first((%s::%s)-%s)",
$this->onto,
$this->branch,
$this->onto));
$branch_range = hgsprintf(
"(%s::%s)",
$branch_root,
$this->branch);
if (!$this->keepBranch) {
$this->handleAlternateBranches($branch_root, $branch_range);
}
// Collapse just the landing branch onto master.
// Leave its children on the original branch.
$err = $repository_api->execPassthru(
'rebase --collapse --keep --logfile %s -r %s -d %s',
$this->messageFile,
$branch_range,
$this->onto);
if ($err) {
$repository_api->execManualLocal(
'rebase --abort');
$this->restoreBranch();
throw new ArcanistUsageException(
"Squashing the commits under {$this->branch} failed. ".
"Manually squash your commits and run 'arc land' again.");
}
if ($repository_api->isBookmark($this->branch)) {
// a bug in mercurial means bookmarks end up on the revision prior
// to the collapse when using --collapse with --keep,
// so we manually move them to the correct spots
// see: http://bz.selenic.com/show_bug.cgi?id=3716
$repository_api->execxLocal(
'bookmark -f %s',
$this->onto);
$repository_api->execxLocal(
'bookmark -f %s -r %s',
$this->branch,
$branch_rev_id);
}
// check if the branch had children
list($output) = $repository_api->execxLocal(
"log -r %s --template %s",
hgsprintf("children(%s)", $this->branch),
'{node}\n');
$child_branch_roots = phutil_split_lines($output, false);
$child_branch_roots = array_filter($child_branch_roots);
if ($child_branch_roots) {
// move the branch's children onto the collapsed commit
foreach ($child_branch_roots as $child_root) {
$repository_api->execxLocal(
'rebase -d %s -s %s --keep --keepbranches',
$this->onto,
$child_root);
}
}
// All the rebases may have moved us to another branch
// so we move back.
$repository_api->execxLocal('checkout %s', $this->onto);
}
}
/**
* Detect alternate branches and prompt the user for how to handle
* them. An alternate branch is a branch that forks from the landing
* branch prior to the landing branch tip.
*
* In a situation like this:
* -a--------b master
* \
* w--x landingbranch
* \ \-- g subbranch
* \--y altbranch1
* \--z altbranch2
*
* y and z are alternate branches and will get deleted by the squash,
* so we need to detect them and ask the user what they want to do.
*
* @param string The revision id of the landing branch's root commit.
* @param string The revset specifying all the commits in the landing branch.
* @return void
*/
private function handleAlternateBranches($branch_root, $branch_range) {
$repository_api = $this->getRepositoryAPI();
// Using the tree in the doccomment, the revset below resolves as follows:
// 1. roots(descendants(w) - descendants(x) - (w::x))
// 2. roots({x,g,y,z} - {g} - {w,x})
// 3. roots({y,z})
// 4. {y,z}
$alt_branch_revset = hgsprintf(
'roots(descendants(%s)-descendants(%s)-%R)',
$branch_root,
$this->branch,
$branch_range);
list($alt_branches) = $repository_api->execxLocal(
"log --template %s -r %s",
'{node}\n',
$alt_branch_revset);
$alt_branches = phutil_split_lines($alt_branches, false);
$alt_branches = array_filter($alt_branches);
$alt_count = count($alt_branches);
if ($alt_count > 0) {
$input = phutil_console_prompt(
ucfirst($this->branchType)." '{$this->branch}' has {$alt_count} ".
"{$this->branchType}(s) forking off of it that would be deleted ".
"during a squash. Would you like to keep a non-squashed copy, rebase ".
"them on top of '{$this->branch}', or abort and deal with them ".
"yourself? (k)eep, (r)ebase, (a)bort:");
if ($input == 'k' || $input == 'keep') {
$this->keepBranch = true;
} else if ($input == 'r' || $input == 'rebase') {
foreach ($alt_branches as $alt_branch) {
$repository_api->execxLocal(
'rebase --keep --keepbranches -d %s -s %s',
$this->branch,
$alt_branch);
}
} else if ($input == 'a' || $input == 'abort') {
$branch_string = implode("\n", $alt_branches);
echo "\nRemove the {$this->branchType}s starting at these revisions ".
"and run arc land again:\n{$branch_string}\n\n";
throw new ArcanistUserAbortException();
} else {
throw new ArcanistUsageException("Invalid choice. Aborting arc land.");
}
}
}
private function merge() {
$repository_api = $this->getRepositoryAPI();
// In immutable histories, do a --no-ff merge to force a merge commit with
// the right message.
$repository_api->execxLocal('checkout %s', $this->onto);
chdir($repository_api->getPath());
if ($this->isGit) {
$err = phutil_passthru(
'git merge --no-stat --no-ff --no-commit %s',
$this->branch);
if ($err) {
throw new ArcanistUsageException(
"'git merge' failed. Your working copy has been left in a partially ".
"merged state. You can: abort with 'git merge --abort'; or follow ".
"the instructions to complete the merge.");
}
} else if ($this->isHg) {
// HG arc land currently doesn't support --merge.
// When merging a bookmark branch to a master branch that
// hasn't changed since the fork, mercurial fails to merge.
// Instead of only working in some cases, we just disable --merge
// until there is a demand for it.
// The user should never reach this line, since --merge is
// forbidden at the command line argument level.
throw new ArcanistUsageException(
"--merge is not currently supported for hg repos.");
}
}
private function push() {
$repository_api = $this->getRepositoryAPI();
// these commands can fail legitimately (e.g. commit hooks)
try {
if ($this->isGit) {
$repository_api->execxLocal(
'commit -F %s',
$this->messageFile);
} else if ($this->isHg) {
// hg rebase produces a commit earlier as part of rebase
if (!$this->useSquash) {
$repository_api->execxLocal(
'commit --logfile %s',
$this->messageFile);
}
}
// We dispatch this event so we can run checks on the merged revision,
// right before it gets pushed out. It's easier to do this in arc land
// than to try to hook into git/hg.
$this->dispatchEvent(
ArcanistEventType::TYPE_LAND_WILLPUSHREVISION,
array());
} catch (Exception $ex) {
$this->executeCleanupAfterFailedPush();
throw $ex;
}
if ($this->getArgument('hold')) {
echo phutil_console_format(
"Holding change in **%s**: it has NOT been pushed yet.\n",
$this->onto);
} else {
echo "Pushing change...\n\n";
chdir($repository_api->getPath());
if ($this->isGitSvn) {
$err = phutil_passthru('git svn dcommit');
$cmd = "git svn dcommit";
} else if ($this->isGit) {
$err = phutil_passthru(
'git push %s %s',
$this->remote,
$this->onto);
$cmd = "git push";
} else if ($this->isHgSvn) {
// hg-svn doesn't support 'push -r', so we do a normal push
// which hg-svn modifies to only push the current branch and
// ancestors.
$err = $repository_api->execPassthru(
'push %s',
$this->remote);
$cmd = "hg push";
} else if ($this->isHg) {
$err = $repository_api->execPassthru(
'push -r %s %s',
$this->onto,
$this->remote);
$cmd = "hg push";
}
if ($err) {
echo phutil_console_format("<bg:red>** PUSH FAILED! **</bg>\n");
$this->executeCleanupAfterFailedPush();
if ($this->isGit) {
throw new ArcanistUsageException(
"'{$cmd}' failed! Fix the error and run 'arc land' again.");
}
throw new ArcanistUsageException(
"'{$cmd}' failed! Fix the error and push this change manually.");
}
$mark_workflow = $this->buildChildWorkflow(
'close-revision',
array(
'--finalize',
'--quiet',
$this->revision['id'],
));
$mark_workflow->run();
echo "\n";
}
}
private function executeCleanupAfterFailedPush() {
$repository_api = $this->getRepositoryAPI();
if ($this->isGit) {
$repository_api->execxLocal('reset --hard HEAD^');
$this->restoreBranch();
} else if ($this->isHg) {
$repository_api->execxLocal(
'--config extensions.mq= strip %s',
$this->onto);
$this->restoreBranch();
}
}
private function cleanupBranch() {
$repository_api = $this->getRepositoryAPI();
echo "Cleaning up feature {$this->branchType}...\n";
if ($this->isGit) {
list($ref) = $repository_api->execxLocal(
'rev-parse --verify %s',
$this->branch);
$ref = trim($ref);
$recovery_command = csprintf(
'git checkout -b %s %s',
$this->branch,
$ref);
echo "(Use `{$recovery_command}` if you want it back.)\n";
$repository_api->execxLocal(
'branch -D %s',
$this->branch);
} else if ($this->isHg) {
$common_ancestor = $repository_api->getCanonicalRevisionName(
hgsprintf("ancestor(%s,%s)",
$this->onto,
$this->branch));
$branch_root = $repository_api->getCanonicalRevisionName(
hgsprintf("first((%s::%s)-%s)",
$common_ancestor,
$this->branch,
$common_ancestor));
$repository_api->execxLocal(
'--config extensions.mq= strip -r %s',
$branch_root);
if ($repository_api->isBookmark($this->branch)) {
$repository_api->execxLocal(
'bookmark -d %s',
$this->branch);
}
}
if ($this->getArgument('delete-remote')) {
if ($this->isGit) {
list($err, $ref) = $repository_api->execManualLocal(
'rev-parse --verify %s/%s',
$this->remote,
$this->branch);
if ($err) {
echo "No remote feature {$this->branchType} to clean up.\n";
} else {
// NOTE: In Git, you delete a remote branch by pushing it with a
// colon in front of its name:
//
// git push <remote> :<branch>
echo "Cleaning up remote feature branch...\n";
$repository_api->execxLocal(
'push %s :%s',
$this->remote,
$this->branch);
}
} else if ($this->isHg) {
// named branches were closed as part of the earlier commit
// so only worry about bookmarks
if ($repository_api->isBookmark($this->branch)) {
$repository_api->execxLocal(
'push -B %s %s',
$this->branch,
$this->remote);
}
}
}
}
protected function getSupportedRevisionControlSystems() {
return array('git', 'hg');
}
private function getBranchOrBookmark() {
$repository_api = $this->getRepositoryAPI();
if ($this->isGit) {
$branch = $repository_api->getBranchName();
} else if ($this->isHg) {
$branch = $repository_api->getActiveBookmark();
if (!$branch) {
$branch = $repository_api->getBranchName();
}
}
return $branch;
}
private function getBranchType($branch) {
$repository_api = $this->getRepositoryAPI();
if ($this->isHg && $repository_api->isBookmark($branch)) {
return "bookmark";
}
return "branch";
}
/**
* Restore the original branch, e.g. after a successful land or a failed
* pull.
*/
private function restoreBranch() {
$repository_api = $this->getRepositoryAPI();
$repository_api->execxLocal(
'checkout %s',
$this->oldBranch);
if ($this->isGit) {
$repository_api->execxLocal(
'submodule update --init --recursive');
}
echo phutil_console_format(
"Switched back to {$this->branchType} **%s**.\n",
$this->oldBranch);
}
}
diff --git a/src/workflow/ArcanistLintWorkflow.php b/src/workflow/ArcanistLintWorkflow.php
index a4931dae..45ce9dfe 100644
--- a/src/workflow/ArcanistLintWorkflow.php
+++ b/src/workflow/ArcanistLintWorkflow.php
@@ -1,665 +1,666 @@
<?php
/**
* Runs lint rules on changes.
*
* @group workflow
*/
final class ArcanistLintWorkflow extends ArcanistBaseWorkflow {
const RESULT_OKAY = 0;
const RESULT_WARNINGS = 1;
const RESULT_ERRORS = 2;
const RESULT_SKIP = 3;
const RESULT_POSTPONED = 4;
const DEFAULT_SEVERITY = ArcanistLintSeverity::SEVERITY_ADVICE;
private $unresolvedMessages;
private $shouldLintAll;
private $shouldAmendChanges = false;
private $shouldAmendWithoutPrompt = false;
private $shouldAmendAutofixesWithoutPrompt = false;
private $engine;
private $postponedLinters;
public function getWorkflowName() {
return 'lint';
}
public function setShouldAmendChanges($should_amend) {
$this->shouldAmendChanges = $should_amend;
return $this;
}
public function setShouldAmendWithoutPrompt($should_amend) {
$this->shouldAmendWithoutPrompt = $should_amend;
return $this;
}
public function setShouldAmendAutofixesWithoutPrompt($should_amend) {
$this->shouldAmendAutofixesWithoutPrompt = $should_amend;
return $this;
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**lint** [__options__] [__paths__]
**lint** [__options__] --rev [__rev__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
Run static analysis on changes to check for mistakes. If no files
are specified, lint will be run on all files which have been modified.
EOTEXT
);
}
public function getArguments() {
return array(
'lintall' => array(
'help' =>
"Show all lint warnings, not just those on changed lines. When " .
"paths are specified, this is the default behavior.",
'conflicts' => array(
'only-changed' => true,
),
),
'only-changed' => array(
'help' =>
"Show lint warnings just on changed lines. When no paths are " .
"specified, this is the default. This differs from only-new " .
"in cases where line modifications introduce lint on other " .
"unmodified lines.",
'conflicts' => array(
'lintall' => true,
),
),
'rev' => array(
'param' => 'revision',
'help' => "Lint changes since a specific revision.",
'supports' => array(
'git',
'hg',
),
'nosupport' => array(
'svn' => "Lint does not currently support --rev in SVN.",
),
),
'output' => array(
'param' => 'format',
'help' =>
"With 'summary', show lint warnings in a more compact format. ".
"With 'json', show lint warnings in machine-readable JSON format. ".
"With 'none', show no lint warnings. ".
"With 'compiler', show lint warnings in suitable for your editor."
),
'only-new' => array(
'param' => 'bool',
'supports' => array('git', 'hg'), // TODO: svn
'help' => 'Display only messages not present in the original code.',
),
'engine' => array(
'param' => 'classname',
'help' =>
"Override configured lint engine for this project."
),
'apply-patches' => array(
'help' =>
'Apply patches suggested by lint to the working copy without '.
'prompting.',
'conflicts' => array(
'never-apply-patches' => true,
),
),
'never-apply-patches' => array(
'help' => 'Never apply patches suggested by lint.',
'conflicts' => array(
'apply-patches' => true,
),
),
'amend-all' => array(
'help' =>
'When linting git repositories, amend HEAD with all patches '.
'suggested by lint without prompting.',
),
'amend-autofixes' => array(
'help' =>
'When linting git repositories, amend HEAD with autofix '.
'patches suggested by lint without prompting.',
),
'everything' => array(
'help' => 'Lint all files in the project.',
'conflicts' => array(
'cache' => '--everything lints all files',
'rev' => '--everything lints all files'
),
),
'severity' => array(
'param' => 'string',
'help' =>
"Set minimum message severity. One of: '".
implode(
"', '",
array_keys(ArcanistLintSeverity::getLintSeverities())).
"'. Defaults to '".self::DEFAULT_SEVERITY."'.",
),
'cache' => array(
'param' => 'bool',
'help' =>
"0 to disable cache, 1 to enable. The default value is ".
"determined by 'arc.lint.cache' in configuration, which defaults ".
"to off. See notes in 'arc.lint.cache'.",
),
'*' => 'paths',
);
}
public function requiresAuthentication() {
return (bool)$this->getArgument('only-new');
}
public function requiresWorkingCopy() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
private function getCacheKey() {
return implode("\n", array(
get_class($this->engine),
$this->getArgument('severity', self::DEFAULT_SEVERITY),
$this->shouldLintAll,
));
}
public function run() {
$console = PhutilConsole::getConsole();
$working_copy = $this->getWorkingCopy();
+ $configuration_manager = $this->getConfigurationManager();
$engine = $this->getArgument('engine');
if (!$engine) {
- $engine = $working_copy->getConfigFromAnySource('lint.engine');
+ $engine = $configuration_manager->getConfigFromAnySource('lint.engine');
}
if (!$engine) {
if (Filesystem::pathExists($working_copy->getProjectPath('.arclint'))) {
$engine = 'ArcanistConfigurationDrivenLintEngine';
}
}
if (!$engine) {
throw new ArcanistNoEngineException(
"No lint engine configured for this project. Edit '.arcconfig' to ".
"specify a lint engine, or create an '.arclint' file.");
}
$rev = $this->getArgument('rev');
$paths = $this->getArgument('paths');
$use_cache = $this->getArgument('cache', null);
$everything = $this->getArgument('everything');
if ($everything && $paths) {
throw new ArcanistUsageException(
"You can not specify paths with --everything. The --everything ".
"flag lints every file.");
}
if ($use_cache === null) {
- $use_cache = (bool)$working_copy->getConfigFromAnySource(
+ $use_cache = (bool)$configuration_manager->getConfigFromAnySource(
'arc.lint.cache',
false);
}
if ($rev && $paths) {
throw new ArcanistUsageException("Specify either --rev or paths.");
}
// NOTE: When the user specifies paths, we imply --lintall and show all
// warnings for the paths in question. This is easier to deal with for
// us and less confusing for users.
$this->shouldLintAll = $paths ? true : false;
if ($this->getArgument('lintall')) {
$this->shouldLintAll = true;
} else if ($this->getArgument('only-changed')) {
$this->shouldLintAll = false;
}
if ($everything) {
// Recurse through project from root
switch ($this->getRepositoryApi()->getSourceControlSystemName()) {
case 'git':
$filter = '*/.git';
break;
case 'svn':
$filter = '*/.svn';
break;
case 'hg':
$filter = '*/.hg';
break;
}
$paths = id(new FileFinder($working_copy->getProjectRoot()))
->excludePath($filter)
->find();
$this->shouldLintAll = true;
} else {
$paths = $this->selectPathsForWorkflow($paths, $rev);
}
if (!class_exists($engine) ||
!is_subclass_of($engine, 'ArcanistLintEngine')) {
throw new ArcanistUsageException(
"Configured lint engine '{$engine}' is not a subclass of ".
"'ArcanistLintEngine'.");
}
$engine = newv($engine, array());
$this->engine = $engine;
- $engine->setWorkingCopy($working_copy);
+ $engine->setWorkingCopy($working_copy); // todo setConfig?
$engine->setMinimumSeverity(
$this->getArgument('severity', self::DEFAULT_SEVERITY));
$file_hashes = array();
if ($use_cache) {
$engine->setRepositoryVersion($this->getRepositoryVersion());
$cache = $this->readScratchJSONFile('lint-cache.json');
$cache = idx($cache, $this->getCacheKey(), array());
$cached = array();
foreach ($paths as $path) {
$abs_path = $engine->getFilePathOnDisk($path);
if (!Filesystem::pathExists($abs_path)) {
continue;
}
$file_hashes[$abs_path] = md5_file($abs_path);
if (!isset($cache[$path])) {
continue;
}
$messages = idx($cache[$path], $file_hashes[$abs_path]);
if ($messages !== null) {
$cached[$path] = $messages;
}
}
if ($cached) {
$console->writeErr(
pht("Using lint cache, use '--cache 0' to disable it.")."\n");
}
$engine->setCachedResults($cached);
}
// Propagate information about which lines changed to the lint engine.
// This is used so that the lint engine can drop warning messages
// concerning lines that weren't in the change.
$engine->setPaths($paths);
if (!$this->shouldLintAll) {
foreach ($paths as $path) {
// Note that getChangedLines() returns null to indicate that a file
// is binary or a directory (i.e., changed lines are not relevant).
$engine->setPathChangedLines(
$path,
$this->getChangedLines($path, 'new'));
}
}
// Enable possible async linting only for 'arc diff' not 'arc lint'
if ($this->getParentWorkflow()) {
$engine->setEnableAsyncLint(true);
} else {
$engine->setEnableAsyncLint(false);
}
if ($this->getArgument('only-new')) {
$conduit = $this->getConduit();
$api = $this->getRepositoryAPI();
if ($rev) {
$api->setBaseCommit($rev);
}
$svn_root = id(new PhutilURI($api->getSourceControlPath()))->getPath();
$all_paths = array();
foreach ($paths as $path) {
$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
$full_paths = array($path);
$change = $this->getChange($path);
$type = $change->getType();
if (ArcanistDiffChangeType::isOldLocationChangeType($type)) {
$full_paths = $change->getAwayPaths();
} else if (ArcanistDiffChangeType::isNewLocationChangeType($type)) {
continue;
} else if (ArcanistDiffChangeType::isDeleteChangeType($type)) {
continue;
}
foreach ($full_paths as $full_path) {
$all_paths[$svn_root.'/'.$full_path] = $path;
}
}
$lint_future = $conduit->callMethod('diffusion.getlintmessages', array(
'arcanistProject' => $this->getWorkingCopy()->getProjectID(),
'branch' => '', // TODO: Tracking branch.
'commit' => $api->getBaseCommit(),
'files' => array_keys($all_paths),
));
}
$failed = null;
try {
$engine->run();
} catch (Exception $ex) {
$failed = $ex;
}
$results = $engine->getResults();
if ($this->getArgument('only-new')) {
$total = 0;
foreach ($results as $result) {
$total += count($result->getMessages());
}
// Don't wait for response with default value of --only-new.
$timeout = null;
if ($this->getArgument('only-new') === null || !$total) {
$timeout = 0;
}
$raw_messages = $this->resolveCall($lint_future, $timeout);
if ($raw_messages && $total) {
$old_messages = array();
$line_maps = array();
foreach ($raw_messages as $message) {
$path = $all_paths[$message['path']];
$line = $message['line'];
$code = $message['code'];
if (!isset($line_maps[$path])) {
$line_maps[$path] = $this->getChange($path)->buildLineMap();
}
$new_lines = idx($line_maps[$path], $line);
if (!$new_lines) { // Unmodified lines after last hunk.
$last_old = ($line_maps[$path] ? last_key($line_maps[$path]) : 0);
$news = array_filter($line_maps[$path]);
$last_new = ($news ? last(end($news)) : 0);
$new_lines = array($line + $last_new - $last_old);
}
$error = array($code => array(true));
foreach ($new_lines as $new) {
if (isset($old_messages[$path][$new])) {
$old_messages[$path][$new][$code][] = true;
break;
}
$old_messages[$path][$new] = &$error;
}
unset($error);
}
foreach ($results as $result) {
foreach ($result->getMessages() as $message) {
$path = str_replace(DIRECTORY_SEPARATOR, '/', $message->getPath());
$line = $message->getLine();
$code = $message->getCode();
if (!empty($old_messages[$path][$line][$code])) {
$message->setObsolete(true);
array_pop($old_messages[$path][$line][$code]);
}
}
$result->sortAndFilterMessages();
}
}
}
// It'd be nice to just return a single result from the run method above
// which contains both the lint messages and the postponed linters.
// However, to maintain compatibility with existing lint subclasses, use
// a separate method call to grab the postponed linters.
$this->postponedLinters = $engine->getPostponedLinters();
if ($this->getArgument('never-apply-patches')) {
$apply_patches = false;
} else {
$apply_patches = true;
}
if ($this->getArgument('apply-patches')) {
$prompt_patches = false;
} else {
$prompt_patches = true;
}
if ($this->getArgument('amend-all')) {
$this->shouldAmendChanges = true;
$this->shouldAmendWithoutPrompt = true;
}
if ($this->getArgument('amend-autofixes')) {
$prompt_autofix_patches = false;
$this->shouldAmendChanges = true;
$this->shouldAmendAutofixesWithoutPrompt = true;
} else {
$prompt_autofix_patches = true;
}
$repository_api = $this->getRepositoryAPI();
if ($this->shouldAmendChanges) {
$this->shouldAmendChanges = $repository_api->supportsAmend() &&
!$this->isHistoryImmutable();
}
$wrote_to_disk = false;
switch ($this->getArgument('output')) {
case 'json':
$renderer = new ArcanistLintJSONRenderer();
$prompt_patches = false;
$apply_patches = $this->getArgument('apply-patches');
break;
case 'summary':
$renderer = new ArcanistLintSummaryRenderer();
break;
case 'none':
$prompt_patches = false;
$apply_patches = $this->getArgument('apply-patches');
$renderer = new ArcanistLintNoneRenderer();
break;
case 'compiler':
$renderer = new ArcanistLintLikeCompilerRenderer();
$prompt_patches = false;
$apply_patches = $this->getArgument('apply-patches');
break;
default:
$renderer = new ArcanistLintConsoleRenderer();
$renderer->setShowAutofixPatches($prompt_autofix_patches);
break;
}
$all_autofix = true;
foreach ($results as $result) {
$result_all_autofix = $result->isAllAutofix();
if (!$result->getMessages() && !$result_all_autofix) {
continue;
}
if (!$result_all_autofix) {
$all_autofix = false;
}
$lint_result = $renderer->renderLintResult($result);
if ($lint_result) {
$console->writeOut('%s', $lint_result);
}
if ($apply_patches && $result->isPatchable()) {
$patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
$old_file = $result->getFilePathOnDisk();
if ($prompt_patches &&
!($result_all_autofix && !$prompt_autofix_patches)) {
if (!Filesystem::pathExists($old_file)) {
$old_file = '/dev/null';
}
$new_file = new TempFile();
$new = $patcher->getModifiedFileContent();
Filesystem::writeFile($new_file, $new);
// TODO: Improve the behavior here, make it more like
// difference_render().
list(, $stdout, $stderr) =
exec_manual("diff -u %s %s", $old_file, $new_file);
$console->writeOut('%s', $stdout);
$console->writeErr('%s', $stderr);
$prompt = phutil_console_format(
"Apply this patch to __%s__?",
$result->getPath());
if (!$console->confirm($prompt, $default_no = false)) {
continue;
}
}
$patcher->writePatchToDisk();
$wrote_to_disk = true;
$file_hashes[$old_file] = md5_file($old_file);
}
}
if ($wrote_to_disk && $this->shouldAmendChanges) {
if ($this->shouldAmendWithoutPrompt ||
($this->shouldAmendAutofixesWithoutPrompt && $all_autofix)) {
$console->writeOut(
"<bg:yellow>** LINT NOTICE **</bg> Automatically amending HEAD ".
"with lint patches.\n");
$amend = true;
} else {
$amend = $console->confirm("Amend HEAD with lint patches?");
}
if ($amend) {
if ($repository_api instanceof ArcanistGitAPI) {
// Add the changes to the index before amending
$repository_api->execxLocal('add -u');
}
$repository_api->amendCommit();
} else {
throw new ArcanistUsageException(
"Sort out the lint changes that were applied to the working ".
"copy and relint.");
}
}
if ($this->getArgument('output') == 'json') {
// NOTE: Required by save_lint.php in Phabricator.
return 0;
}
if ($failed) {
if ($failed instanceof ArcanistNoEffectException) {
if ($renderer instanceof ArcanistLintNoneRenderer) {
return 0;
}
}
throw $failed;
}
$unresolved = array();
$has_warnings = false;
$has_errors = false;
foreach ($results as $result) {
foreach ($result->getMessages() as $message) {
if (!$message->isPatchApplied()) {
if ($message->isError()) {
$has_errors = true;
} else if ($message->isWarning()) {
$has_warnings = true;
}
$unresolved[] = $message;
}
}
}
$this->unresolvedMessages = $unresolved;
$cache = $this->readScratchJSONFile('lint-cache.json');
$cached = idx($cache, $this->getCacheKey(), array());
if ($cached || $use_cache) {
$stopped = $engine->getStoppedPaths();
foreach ($results as $result) {
$path = $result->getPath();
if (!$use_cache) {
unset($cached[$path]);
continue;
}
$abs_path = $engine->getFilePathOnDisk($path);
if (!Filesystem::pathExists($abs_path)) {
continue;
}
$version = $result->getCacheVersion();
$cached_path = array();
if (isset($stopped[$path])) {
$cached_path['stopped'] = $stopped[$path];
}
$cached_path['repository_version'] = $this->getRepositoryVersion();
foreach ($result->getMessages() as $message) {
$granularity = $message->getGranularity();
if ($granularity == ArcanistLinter::GRANULARITY_GLOBAL) {
continue;
}
if (!$message->isPatchApplied()) {
$cached_path[] = $message->toDictionary();
}
}
$hash = idx($file_hashes, $abs_path);
if (!$hash) {
$hash = md5_file($abs_path);
}
$cached[$path] = array($hash => array($version => $cached_path));
}
$cache[$this->getCacheKey()] = $cached;
// TODO: Garbage collection.
$this->writeScratchJSONFile('lint-cache.json', $cache);
}
// Take the most severe lint message severity and use that
// as the result code.
if ($has_errors) {
$result_code = self::RESULT_ERRORS;
} else if ($has_warnings) {
$result_code = self::RESULT_WARNINGS;
} else if (!empty($this->postponedLinters)) {
$result_code = self::RESULT_POSTPONED;
} else {
$result_code = self::RESULT_OKAY;
}
if (!$this->getParentWorkflow()) {
if ($result_code == self::RESULT_OKAY) {
$console->writeOut('%s', $renderer->renderOkayResult());
}
}
return $result_code;
}
public function getUnresolvedMessages() {
return $this->unresolvedMessages;
}
public function getPostponedLinters() {
return $this->postponedLinters;
}
}
diff --git a/src/workflow/ArcanistSetConfigWorkflow.php b/src/workflow/ArcanistSetConfigWorkflow.php
index 9f7a0e32..ffd16f50 100644
--- a/src/workflow/ArcanistSetConfigWorkflow.php
+++ b/src/workflow/ArcanistSetConfigWorkflow.php
@@ -1,156 +1,157 @@
<?php
/**
* Write configuration settings.
*
* @group workflow
*/
final class ArcanistSetConfigWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'set-config';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**set-config** [__options__] -- __name__ __value__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Sets an arc configuration option.
- Options are either global (apply to all arc commands you invoke
+ Options are either user (apply to all arc commands you invoke
from the current user) or local (apply only to the current working
- copy). By default, global configuration is written. Use __--local__
+ copy). By default, user configuration is written. Use __--local__
to write local configuration.
- Global values are written to '~/.arcrc' on Linux and Mac OS X, and an
+ User values are written to '~/.arcrc' on Linux and Mac OS X, and an
undisclosed location on Windows. Local values are written to an arc
directory under either .git, .hg, or .svn as appropriate.
With __--show__, a description of supported configuration values
is shown.
EOTEXT
);
}
public function getArguments() {
return array(
'show' => array(
'help' => 'Show available configuration values.',
),
'local' => array(
- 'help' => 'Set a local config value instead of a global one',
+ 'help' => 'Set a local config value instead of a user one',
),
'*' => 'argv',
);
}
public function requiresRepositoryAPI() {
return $this->getArgument('local');
}
public function run() {
if ($this->getArgument('show')) {
return $this->show();
}
$argv = $this->getArgument('argv');
if (count($argv) != 2) {
throw new ArcanistUsageException(
"Specify a key and a value, or --show.");
}
+ $configuration_manager = $this->getConfigurationManager();
$is_local = $this->getArgument('local');
if ($is_local) {
- $config = $this->readLocalArcConfig();
+ $config = $configuration_manager->readLocalArcConfig();
$which = 'local';
} else {
- $config = self::readGlobalArcConfig();
- $which = 'global';
+ $config = $configuration_manager->readUserArcConfig();
+ $which = 'user';
}
$key = $argv[0];
$val = $argv[1];
$settings = new ArcanistSettings();
$old = null;
if (array_key_exists($key, $config)) {
$old = $config[$key];
}
if (!strlen($val)) {
unset($config[$key]);
if ($is_local) {
- $this->writeLocalArcConfig($config);
+ $configuration_manager->writeLocalArcConfig($config);
} else {
- self::writeGlobalArcConfig($config);
+ $configuration_manager->writeUserArcConfig($config);
}
$old = $settings->formatConfigValueForDisplay($key, $old);
if ($old === null) {
echo "Deleted key '{$key}' from {$which} config.\n";
} else {
echo "Deleted key '{$key}' from {$which} config (was {$old}).\n";
}
} else {
$val = $settings->willWriteValue($key, $val);
$config[$key] = $val;
if ($is_local) {
- $this->writeLocalArcConfig($config);
+ $configuration_manager->writeLocalArcConfig($config);
} else {
- self::writeGlobalArcConfig($config);
+ $configuration_manager->writeUserArcConfig($config);
}
$val = $settings->formatConfigValueForDisplay($key, $val);
$old = $settings->formatConfigValueForDisplay($key, $old);
if ($old === null) {
echo "Set key '{$key}' = {$val} in {$which} config.\n";
} else {
echo "Set key '{$key}' = {$val} in {$which} config (was {$old}).\n";
}
}
return 0;
}
private function show() {
- $config = self::readGlobalArcConfig();
+ $config = $this->getConfigurationManager()->readUserArcConfig();
$settings = new ArcanistSettings();
$keys = $settings->getAllKeys();
sort($keys);
foreach ($keys as $key) {
$type = $settings->getType($key);
$example = $settings->getExample($key);
$help = $settings->getHelp($key);
$value = idx($config, $key);
$value = $settings->formatConfigValueForDisplay($key, $value);
echo phutil_console_format("**__%s__** (%s)\n\n", $key, $type);
if ($example !== null) {
echo phutil_console_format(" Example: %s\n", $example);
}
if (strlen($value)) {
- echo phutil_console_format(" Global Setting: %s\n", $value);
+ echo phutil_console_format(" User Setting: %s\n", $value);
}
echo "\n";
echo phutil_console_wrap($help, 4);
echo "\n\n\n";
}
return 0;
}
}
diff --git a/src/workflow/ArcanistShellCompleteWorkflow.php b/src/workflow/ArcanistShellCompleteWorkflow.php
index 76b81c88..266ff486 100644
--- a/src/workflow/ArcanistShellCompleteWorkflow.php
+++ b/src/workflow/ArcanistShellCompleteWorkflow.php
@@ -1,191 +1,194 @@
<?php
/**
* Powers shell-completion scripts.
*
* @group workflow
*/
final class ArcanistShellCompleteWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'shell-complete';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**shell-complete** __--current__ __N__ -- [__argv__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: bash, etc.
Implements shell completion. To use shell completion, source the
appropriate script from 'resources/shell/' in your .shellrc.
EOTEXT
);
}
public function getArguments() {
return array(
'current' => array(
'help' => 'Current term in the argument list being completed.',
'param' => 'cursor_position',
),
'*' => 'argv',
);
}
public function shouldShellComplete() {
return false;
}
public function run() {
$pos = $this->getArgument('current');
$argv = $this->getArgument('argv', array());
$argc = count($argv);
if ($pos === null) {
$pos = $argc - 1;
}
// Determine which revision control system the working copy uses, so we
// can filter out commands and flags which aren't supported. If we can't
// figure it out, just return all flags/commands.
$vcs = null;
// We have to build our own because if we requiresWorkingCopy() we'll throw
// if we aren't in a .arcconfig directory. We probably still can't do much,
// but commands can raise more detailed errors.
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(getcwd());
if ($working_copy->getProjectRoot()) {
- $repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
- $working_copy);
+ $configuration_manager = $this->getConfigurationManager();
+ $configuration_manager->setWorkingCopyIdentity($working_copy);
+ $repository_api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
+ $configuration_manager);
+
$vcs = $repository_api->getSourceControlSystemName();
}
$arc_config = $this->getArcanistConfiguration();
if ($pos == 1) {
$workflows = $arc_config->buildAllWorkflows();
$complete = array();
foreach ($workflows as $name => $workflow) {
if (!$workflow->shouldShellComplete()) {
continue;
}
$supported = $workflow->getSupportedRevisionControlSystems();
$ok = (in_array('any', $supported)) ||
(in_array($vcs, $supported));
if (!$ok) {
continue;
}
$complete[] = $name;
}
// Also permit autocompletion of "arc alias" commands.
- foreach (
- ArcanistAliasWorkflow::getAliases($working_copy) as $key => $value) {
+ $aliases = ArcanistAliasWorkflow::getAliases($configuration_manager);
+ foreach ($aliases as $key => $value) {
$complete[] = $key;
}
echo implode(' ', $complete)."\n";
return 0;
} else {
$workflow = $arc_config->buildWorkflow($argv[1]);
if (!$workflow) {
list($new_command, $new_args) = ArcanistAliasWorkflow::resolveAliases(
$argv[1],
$arc_config,
array_slice($argv, 2),
- $working_copy);
+ $configuration_manager);
if ($new_command) {
$workflow = $arc_config->buildWorkflow($new_command);
}
if (!$workflow) {
return 1;
} else {
$argv = array_merge(
array($argv[0]),
array($new_command),
$new_args);
}
}
$arguments = $workflow->getArguments();
$prev = idx($argv, $pos - 1, null);
if (!strncmp($prev, '--', 2)) {
$prev = substr($prev, 2);
} else {
$prev = null;
}
if ($prev !== null &&
isset($arguments[$prev]) &&
isset($arguments[$prev]['param'])) {
$type = idx($arguments[$prev], 'paramtype');
switch ($type) {
case 'file':
echo "FILE\n";
break;
case 'complete':
echo implode(' ', $workflow->getShellCompletions($argv))."\n";
break;
default:
echo "ARGUMENT\n";
break;
}
return 0;
} else {
$output = array();
foreach ($arguments as $argument => $spec) {
if ($argument == '*') {
continue;
}
if ($vcs &&
isset($spec['supports']) &&
!in_array($vcs, $spec['supports'])) {
continue;
}
$output[] = '--'.$argument;
}
$cur = idx($argv, $pos, '');
$any_match = false;
if (strlen($cur)) {
foreach ($output as $possible) {
if (!strncmp($possible, $cur, strlen($cur))) {
$any_match = true;
}
}
}
if (!$any_match && isset($arguments['*'])) {
// TODO: This is mega hacktown but something else probably breaks
// if we use a rich argument specification; fix it when we move to
// PhutilArgumentParser since everything will need to be tested then
// anyway.
if ($arguments['*'] == 'branch' && isset($repository_api)) {
$branches = $repository_api->getAllBranches();
$branches = ipull($branches, 'name');
$output = $branches;
} else {
$output = array("FILE");
}
}
echo implode(' ', $output)."\n";
return 0;
}
}
}
}
diff --git a/src/workflow/ArcanistSvnHookPreCommitWorkflow.php b/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
index 3ec3af08..f4ac5bb9 100644
--- a/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
+++ b/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
@@ -1,231 +1,232 @@
<?php
/**
* Installable as an SVN "pre-commit" hook.
*
* @group workflow
*/
final class ArcanistSvnHookPreCommitWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'svn-hook-pre-commit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**svn-hook-pre-commit** __repository__ __transaction__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: svn
You can install this as an SVN pre-commit hook. For more information,
see the article "Installing Arcanist SVN Hooks" in the Arcanist
documentation.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'svnargs',
);
}
public function shouldShellComplete() {
return false;
}
public function run() {
$svnargs = $this->getArgument('svnargs');
$repository = $svnargs[0];
$transaction = $svnargs[1];
list($commit_message) = execx(
'svnlook log --transaction %s %s',
$transaction,
$repository);
if (strpos($commit_message, '@bypass-lint') !== false) {
return 0;
}
// TODO: Do stuff with commit message.
list($changed) = execx(
'svnlook changed --transaction %s %s',
$transaction,
$repository);
$paths = array();
$changed = explode("\n", trim($changed));
foreach ($changed as $line) {
$matches = null;
preg_match('/^..\s*(.*)$/', $line, $matches);
$paths[$matches[1]] = strlen($matches[1]);
}
$resolved = array();
$failed = array();
$missing = array();
$found = array();
asort($paths);
foreach ($paths as $path => $length) {
foreach ($resolved as $rpath => $root) {
if (!strncmp($path, $rpath, strlen($rpath))) {
$resolved[$path] = $root;
continue 2;
}
}
$config = $path;
if (basename($config) == '.arcconfig') {
$resolved[$config] = $config;
continue;
}
$config = rtrim($config, '/');
$last_config = $config;
do {
if (!empty($missing[$config])) {
break;
} else if (!empty($found[$config])) {
$resolved[$path] = $found[$config];
break;
}
list($err) = exec_manual(
'svnlook cat --transaction %s %s %s',
$transaction,
$repository,
$config ? $config.'/.arcconfig' : '.arcconfig');
if ($err) {
$missing[$path] = true;
} else {
$resolved[$path] = $config ? $config.'/.arcconfig' : '.arcconfig';
$found[$config] = $resolved[$path];
break;
}
$config = dirname($config);
if ($config == '.') {
$config = '';
}
if ($config == $last_config) {
break;
}
$last_config = $config;
} while (true);
if (empty($resolved[$path])) {
$failed[] = $path;
}
}
if ($failed && $resolved) {
$failed_paths = ' '.implode("\n ", $failed);
$resolved_paths = ' '.implode("\n ", array_keys($resolved));
throw new ArcanistUsageException(
"This commit includes a mixture of files in Arcanist projects and ".
"outside of Arcanist projects. A commit which affects an Arcanist ".
"project must affect only that project.\n\n".
"Files in projects:\n\n".
$resolved_paths."\n\n".
"Files not in projects:\n\n".
$failed_paths);
}
if (!$resolved) {
// None of the affected paths are beneath a .arcconfig file.
return 0;
}
$groups = array();
foreach ($resolved as $path => $project) {
$groups[$project][] = $path;
}
if (count($groups) > 1) {
$message = array();
foreach ($groups as $project => $group) {
$message[] = "Files underneath '{$project}':\n\n";
$message[] = " ".implode("\n ", $group)."\n\n";
}
$message = implode('', $message);
throw new ArcanistUsageException(
"This commit includes a mixture of files from different Arcanist ".
"projects. A commit which affects an Arcanist project must affect ".
"only that project.\n\n".
$message);
}
$config_file = key($groups);
$project_root = dirname($config_file);
$paths = reset($groups);
list($config) = execx(
'svnlook cat --transaction %s %s %s',
$transaction,
$repository,
$config_file);
$working_copy = ArcanistWorkingCopyIdentity::newFromRootAndConfigFile(
$project_root,
+ $this->getConfigurationManager(),
$config,
$config_file." (svnlook: {$transaction} {$repository})");
$repository_api = new ArcanistSubversionHookAPI(
$project_root,
$transaction,
$repository);
$lint_engine = $working_copy->getConfig('lint.engine');
if (!$lint_engine) {
return 0;
}
$engine = newv($lint_engine, array());
$engine->setWorkingCopy($working_copy);
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ERROR);
$engine->setPaths($paths);
$engine->setCommitHookMode(true);
$engine->setHookAPI($repository_api);
try {
$results = $engine->run();
} catch (ArcanistNoEffectException $no_effect) {
// Nothing to do, bail out.
return 0;
}
$failures = array();
foreach ($results as $result) {
if (!$result->getMessages()) {
continue;
}
$failures[] = $result;
}
if ($failures) {
$at = "@";
$msg = phutil_console_format(
"\n**LINT ERRORS**\n\n".
"This changeset has lint errors. You must fix all lint errors before ".
"you can commit.\n\n".
"You can add '{$at}bypass-lint' to your commit message to disable ".
"lint checks for this commit, or '{$at}nolint' to the file with ".
"errors to disable lint for that file.\n\n");
echo phutil_console_wrap($msg);
$renderer = new ArcanistLintConsoleRenderer();
foreach ($failures as $result) {
echo $renderer->renderLintResult($result);
}
return 1;
}
return 0;
}
}
diff --git a/src/workflow/ArcanistUnitWorkflow.php b/src/workflow/ArcanistUnitWorkflow.php
index a00817c9..64e7fc19 100644
--- a/src/workflow/ArcanistUnitWorkflow.php
+++ b/src/workflow/ArcanistUnitWorkflow.php
@@ -1,356 +1,356 @@
<?php
/**
* Runs unit tests which cover your changes.
*
* @group workflow
*/
final class ArcanistUnitWorkflow extends ArcanistBaseWorkflow {
const RESULT_OKAY = 0;
const RESULT_UNSOUND = 1;
const RESULT_FAIL = 2;
const RESULT_SKIP = 3;
const RESULT_POSTPONED = 4;
private $unresolvedTests;
private $testResults;
private $engine;
public function getWorkflowName() {
return 'unit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**unit** [__options__] [__paths__]
**unit** [__options__] --rev [__rev__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
Run unit tests that cover specified paths. If no paths are specified,
unit tests covering all modified files will be run.
EOTEXT
);
}
public function getArguments() {
return array(
'rev' => array(
'param' => 'revision',
'help' => "Run unit tests covering changes since a specific revision.",
'supports' => array(
'git',
'hg',
),
'nosupport' => array(
'svn' => "Arc unit does not currently support --rev in SVN.",
),
),
'engine' => array(
'param' => 'classname',
'help' =>
"Override configured unit engine for this project."
),
'coverage' => array(
'help' => 'Always enable coverage information.',
'conflicts' => array(
'no-coverage' => null,
),
),
'no-coverage' => array(
'help' => 'Always disable coverage information.',
),
'detailed-coverage' => array(
'help' => "Show a detailed coverage report on the CLI. Implies ".
"--coverage.",
),
'json' => array(
'help' => 'Report results in JSON format.',
),
'output' => array(
'param' => 'format',
'help' =>
"With 'full', show full pretty report (Default). ".
"With 'json', report results in JSON format. ".
"With 'ugly', use uglier (but more efficient) JSON formatting. ".
"With 'none', don't print results. ",
'conflicts' => array(
'json' => 'Only one output format allowed',
'ugly' => 'Only one output format allowed',
)
),
'everything' => array(
'help' => 'Run every test.',
'conflicts' => array(
'rev' => '--everything runs all tests.',
),
),
'ugly' => array(
'help' => 'With --json, use uglier (but more efficient) formatting.',
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function getEngine() {
return $this->engine;
}
public function run() {
$working_copy = $this->getWorkingCopy();
$engine_class = $this->getArgument(
'engine',
- $working_copy->getConfigFromAnySource('unit.engine'));
+ $this->getConfigurationManager()->getConfigFromAnySource('unit.engine'));
if (!$engine_class) {
throw new ArcanistNoEngineException(
"No unit test engine is configured for this project. Edit .arcconfig ".
"to specify a unit test engine.");
}
$paths = $this->getArgument('paths');
$rev = $this->getArgument('rev');
$everything = $this->getArgument('everything');
if ($everything && $paths) {
throw new ArcanistUsageException(
"You can not specify paths with --everything. The --everything ".
"flag runs every test.");
}
$paths = $this->selectPathsForWorkflow($paths, $rev);
if (!class_exists($engine_class) ||
!is_subclass_of($engine_class, 'ArcanistBaseUnitTestEngine')) {
throw new ArcanistUsageException(
"Configured unit test engine '{$engine_class}' is not a subclass of ".
"'ArcanistBaseUnitTestEngine'.");
}
$this->engine = newv($engine_class, array());
$this->engine->setWorkingCopy($working_copy);
if ($everything) {
$this->engine->setRunAllTests(true);
} else {
$this->engine->setPaths($paths);
}
$this->engine->setArguments($this->getPassthruArgumentsAsMap('unit'));
$renderer = new ArcanistUnitConsoleRenderer();
$this->engine->setRenderer($renderer);
$enable_coverage = null; // Means "default".
if ($this->getArgument('coverage') ||
$this->getArgument('detailed-coverage')) {
$enable_coverage = true;
} else if ($this->getArgument('no-coverage')) {
$enable_coverage = false;
}
$this->engine->setEnableCoverage($enable_coverage);
// Enable possible async tests only for 'arc diff' not 'arc unit'
if ($this->getParentWorkflow()) {
$this->engine->setEnableAsyncTests(true);
} else {
$this->engine->setEnableAsyncTests(false);
}
$results = $this->engine->run();
$this->testResults = $results;
$console = PhutilConsole::getConsole();
$output_format = $this->getOutputFormat();
if ($output_format !== 'full') {
$console->disableOut();
}
$unresolved = array();
$coverage = array();
$postponed_count = 0;
foreach ($results as $result) {
$result_code = $result->getResult();
if ($result_code == ArcanistUnitTestResult::RESULT_POSTPONED) {
$postponed_count++;
$unresolved[] = $result;
} else {
if ($this->engine->shouldEchoTestResults()) {
$console->writeOut('%s', $renderer->renderUnitResult($result));
}
if ($result_code != ArcanistUnitTestResult::RESULT_PASS) {
$unresolved[] = $result;
}
}
if ($result->getCoverage()) {
foreach ($result->getCoverage() as $file => $report) {
$coverage[$file][] = $report;
}
}
}
if ($postponed_count) {
$console->writeOut(
'%s',
$renderer->renderPostponedResult($postponed_count));
}
if ($coverage) {
$file_coverage = array_fill_keys(
$paths,
0);
$file_reports = array();
foreach ($coverage as $file => $reports) {
$report = ArcanistUnitTestResult::mergeCoverage($reports);
$cov = substr_count($report, 'C');
$uncov = substr_count($report, 'U');
if ($cov + $uncov) {
$coverage = $cov / ($cov + $uncov);
} else {
$coverage = 0;
}
$file_coverage[$file] = $coverage;
$file_reports[$file] = $report;
}
$console->writeOut("\n__COVERAGE REPORT__\n");
asort($file_coverage);
foreach ($file_coverage as $file => $coverage) {
$console->writeOut(
" **%s%%** %s\n",
sprintf('% 3d', (int)(100 * $coverage)),
$file);
$full_path = $working_copy->getProjectRoot().'/'.$file;
if ($this->getArgument('detailed-coverage') &&
Filesystem::pathExists($full_path) &&
is_file($full_path) &&
array_key_exists($file, $file_reports)) {
$console->writeOut(
'%s',
$this->renderDetailedCoverageReport(
Filesystem::readFile($full_path),
$file_reports[$file]));
}
}
}
$this->unresolvedTests = $unresolved;
$overall_result = self::RESULT_OKAY;
foreach ($results as $result) {
$result_code = $result->getResult();
if ($result_code == ArcanistUnitTestResult::RESULT_FAIL ||
$result_code == ArcanistUnitTestResult::RESULT_BROKEN) {
$overall_result = self::RESULT_FAIL;
break;
} else if ($result_code == ArcanistUnitTestResult::RESULT_UNSOUND) {
$overall_result = self::RESULT_UNSOUND;
} else if ($result_code == ArcanistUnitTestResult::RESULT_POSTPONED &&
$overall_result != self::RESULT_UNSOUND) {
$overall_result = self::RESULT_POSTPONED;
}
}
if ($output_format !== 'full') {
$console->enableOut();
}
$data = array_values(mpull($results, 'toDictionary'));
switch ($output_format) {
case 'ugly':
$console->writeOut('%s', json_encode($data));
break;
case 'json':
$json = new PhutilJSON();
$console->writeOut('%s', $json->encodeFormatted($data));
break;
case 'full':
// already printed
break;
case 'none':
// do nothing
break;
}
return $overall_result;
}
public function getUnresolvedTests() {
return $this->unresolvedTests;
}
public function getTestResults() {
return $this->testResults;
}
private function renderDetailedCoverageReport($data, $report) {
$data = explode("\n", $data);
$out = '';
$n = 0;
foreach ($data as $line) {
$out .= sprintf('% 5d ', $n + 1);
$line = str_pad($line, 80, ' ');
if (empty($report[$n])) {
$c = 'N';
} else {
$c = $report[$n];
}
switch ($c) {
case 'C':
$out .= phutil_console_format(
'<bg:green> %s </bg>',
$line);
break;
case 'U':
$out .= phutil_console_format(
'<bg:red> %s </bg>',
$line);
break;
case 'X':
$out .= phutil_console_format(
'<bg:magenta> %s </bg>',
$line);
break;
default:
$out .= ' '.$line.' ';
break;
}
$out .= "\n";
$n++;
}
return $out;
}
private function getOutputFormat() {
if ($this->getArgument('ugly')) {
return 'ugly';
}
if ($this->getArgument('json')) {
return 'json';
}
$format = $this->getArgument('output');
$known_formats = array(
'none' => 'none',
'json' => 'json',
'ugly' => 'ugly',
'full' => 'full',
);
return idx($known_formats, $format, 'full');
}
}
diff --git a/src/workflow/ArcanistUpgradeWorkflow.php b/src/workflow/ArcanistUpgradeWorkflow.php
index 803fc0d2..c4d45072 100644
--- a/src/workflow/ArcanistUpgradeWorkflow.php
+++ b/src/workflow/ArcanistUpgradeWorkflow.php
@@ -1,82 +1,84 @@
<?php
/**
* Upgrade arcanist itself.
*
* @group workflow
*/
final class ArcanistUpgradeWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'upgrade';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**upgrade**
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Upgrade arcanist and libphutil to the latest versions.
EOTEXT
);
}
public function getArguments() {
return array();
}
public function run() {
$roots = array();
$roots['libphutil'] = dirname(phutil_get_library_root('phutil'));
$roots['arcanist'] = dirname(phutil_get_library_root('arcanist'));
foreach ($roots as $lib => $root) {
echo "Upgrading {$lib}...\n";
if (!Filesystem::pathExists($root.'/.git')) {
throw new ArcanistUsageException(
"{$lib} must be in its git working copy to be automatically ".
"upgraded. This copy of {$lib} (in '{$root}') is not in a git ".
"working copy.");
}
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($root);
- $repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
- $working_copy);
+ $configuration_manager = clone $this->getConfigurationManager();
+ $configuration_manager->setWorkingCopyIndentity($working_copy);
+ $repository_api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
+ $configuration_manager);
$this->setRepositoryAPI($repository_api);
// Require no local changes.
$this->requireCleanWorkingCopy();
// Require the library be on master.
$branch_name = $repository_api->getBranchName();
if ($branch_name != 'master') {
throw new ArcanistUsageException(
"{$lib} must be on branch 'master' to be automatically upgraded. ".
"This copy of {$lib} (in '{$root}') is on branch '{$branch_name}'.");
}
chdir($root);
try {
phutil_passthru('git pull --rebase');
} catch (Exception $ex) {
phutil_passthru('git rebase --abort');
throw $ex;
}
}
echo phutil_console_wrap(
phutil_console_format(
"**Updated!** Your copy of arc is now up to date.\n"));
return 0;
}
}
diff --git a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
index 7e731067..bf3fbd59 100644
--- a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
+++ b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
@@ -1,278 +1,247 @@
<?php
/**
* Interfaces with basic information about the working copy.
*
*
* @task config
*
* @group workingcopy
*/
final class ArcanistWorkingCopyIdentity {
protected $localConfig;
protected $projectConfig;
- protected $runtimeConfig;
protected $projectRoot;
+ protected $localMetaDir;
public static function newDummyWorkingCopy() {
- return new ArcanistWorkingCopyIdentity('/', array());
+ return new ArcanistWorkingCopyIdentity('/', array());
}
public static function newFromPath($path) {
$project_id = null;
$project_root = null;
$config = array();
foreach (Filesystem::walkToRoot($path) as $dir) {
$config_file = $dir.'/.arcconfig';
if (!Filesystem::pathExists($config_file)) {
continue;
}
$proj_raw = Filesystem::readFile($config_file);
$config = self::parseRawConfigFile($proj_raw, $config_file);
$project_root = $dir;
break;
}
if (!$project_root) {
foreach (Filesystem::walkToRoot($path) as $dir) {
$try = array(
$dir.'/.svn',
$dir.'/.hg',
$dir.'/.git',
);
foreach ($try as $trydir) {
if (Filesystem::pathExists($trydir)) {
$project_root = $dir;
break 2;
}
}
}
}
return new ArcanistWorkingCopyIdentity($project_root, $config);
}
public static function newFromRootAndConfigFile(
$root,
$config_raw,
$from_where) {
if ($config_raw === null) {
$config = array();
} else {
$config = self::parseRawConfigFile($config_raw, $from_where);
}
return new ArcanistWorkingCopyIdentity($root, $config);
}
private static function parseRawConfigFile($raw_config, $from_where) {
$proj = json_decode($raw_config, true);
if (!is_array($proj)) {
throw new Exception(
"Unable to parse '.arcconfig' file '{$from_where}'. The file contents ".
"should be valid JSON.\n\n".
"FILE CONTENTS\n".
substr($raw_config, 0, 2048));
}
$required_keys = array(
'project_id',
);
foreach ($required_keys as $key) {
if (!array_key_exists($key, $proj)) {
throw new Exception(
"Required key '{$key}' is missing from '.arcconfig' file ".
"'{$from_where}'.");
}
}
return $proj;
}
protected function __construct($root, array $config) {
$this->projectRoot = $root;
$this->projectConfig = $config;
$this->localConfig = array();
- $this->runtimeConfig = array();
+ $this->localMetaDir = null;
$vc_dirs = array(
'.git',
'.hg',
'.svn',
);
$found_meta_dir = false;
foreach ($vc_dirs as $dir) {
$meta_path = Filesystem::resolvePath(
$dir,
$this->projectRoot);
if (Filesystem::pathExists($meta_path)) {
$found_meta_dir = true;
+ $this->localMetaDir = $meta_path;
$local_path = Filesystem::resolvePath(
'arc/config',
$meta_path);
- if (Filesystem::pathExists($local_path)) {
- $file = Filesystem::readFile($local_path);
- if ($file) {
- $this->localConfig = json_decode($file, true);
- }
- }
+ $this->localConfig = $this->readLocalArcConfig();
break;
}
}
if (!$found_meta_dir) {
// Try for a single higher-level .svn directory as used by svn 1.7+
foreach (Filesystem::walkToRoot($this->projectRoot) as $parent_path) {
+ $meta_path = Filesystem::resolvePath(
+ '.svn',
+ $parent_path);
$local_path = Filesystem::resolvePath(
'.svn/arc/config',
$parent_path);
if (Filesystem::pathExists($local_path)) {
- $file = Filesystem::readFile($local_path);
- if ($file) {
- $this->localConfig = json_decode($file, true);
- }
+ $this->localMetaDir = $meta_path;
+ $this->localConfig = $this->readLocalArcConfig();
}
}
}
}
public function getProjectID() {
return $this->getConfig('project_id');
}
public function getProjectRoot() {
return $this->projectRoot;
}
public function getProjectPath($to_file) {
return $this->projectRoot.'/'.$to_file;
}
- public function getConduitURI() {
- return $this->getConfig('conduit_uri');
- }
-
/* -( Config )------------------------------------------------------------- */
- public function getProjectConfig() {
+ public function readProjectConfig() {
return $this->projectConfig;
}
/**
* Read a configuration directive from project configuration. This reads ONLY
* permanent project configuration (i.e., ".arcconfig"), not other
* configuration sources. See @{method:getConfigFromAnySource} to read from
* user configuration.
*
* @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 getConfig($key, $default = null) {
$settings = new ArcanistSettings();
$pval = idx($this->projectConfig, $key);
// Test for older names in the per-project config only, since
// they've only been used there.
if ($pval === null) {
$legacy = $settings->getLegacyName($key);
if ($legacy) {
$pval = $this->getConfig($legacy);
}
}
if ($pval === null) {
$pval = $default;
} else {
$pval = $settings->willReadValue($key, $pval);
}
return $pval;
}
-
/**
* Read a configuration directive from local configuration. This
* reads ONLY the per-working copy configuration,
* i.e. .(git|hg|svn)/arc/config, and not other configuration
* sources. See @{method:getConfigFromAnySource} to read from any
* config source or @{method:getConfig} to read permanent
* project-level config.
*
* @task config
*/
public function getLocalConfig($key, $default=null) {
return idx($this->localConfig, $key, $default);
}
- /**
- * Read a configuration directive from any available configuration source.
- * In contrast to @{method:getConfig}, this will look for the directive in
- * local and user configuration in addition to project configuration.
- * The precedence is local > project > user
- *
- * @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) {
- $settings = new ArcanistSettings();
-
- // try runtime config first
- $pval = idx($this->runtimeConfig, $key);
-
- // try local config
- if ($pval === null) {
- $pval = $this->getLocalConfig($key);
+ public function readLocalArcConfig() {
+ if (strlen($this->localMetaDir)) {
+ $local_path = Filesystem::resolvePath(
+ 'arc/config',
+ $this->localMetaDir);
+ if (Filesystem::pathExists($local_path)) {
+ $file = Filesystem::readFile($local_path);
+ if ($file) {
+ return json_decode($file, true);
+ }
+ }
}
+ return array();
+ }
- // then per-project config
- if ($pval === null) {
- $pval = $this->getConfig($key);
+ public function writeLocalArcConfig(array $config) {
+ $dir = $this->localMetaDir;
+ if (!strlen($dir)) {
+ return false;
}
- // now try global (i.e. user-level) config
- if ($pval === null) {
- $global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
- $pval = idx($global_config, $key);
+ if (!Filesystem::pathExists($dir)) {
+ try {
+ Filesystem::createDirectory($dir);
+ } catch (Exception $ex) {
+ return false;
+ }
}
- // Finally, try system-level config.
- if ($pval === null) {
- $system_config = ArcanistBaseWorkflow::readSystemArcConfig();
- $pval = idx($system_config, $key);
- }
+ $json_encoder = new PhutilJSON();
+ $json = $json_encoder->encodeFormatted($config);
- if ($pval === null) {
- $pval = $default;
- } else {
- $pval = $settings->willReadValue($key, $pval);
+ $config_file = Filesystem::resolvePath('arc/config', $dir);
+ try {
+ Filesystem::writeFile($config_file, $json);
+ } catch (FilesystemException $ex) {
+ return false;
}
- return $pval;
-
- }
-
- /**
- * 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;
+ return true;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 05:17 (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
547864
Default Alt Text
(425 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment