diff --git a/src/__tests__/PhutilLibraryTestCase.php b/src/__tests__/PhutilLibraryTestCase.php index 0e5c6261..ba849a52 100644 --- a/src/__tests__/PhutilLibraryTestCase.php +++ b/src/__tests__/PhutilLibraryTestCase.php @@ -1,191 +1,191 @@ setLibrary($this->getLibraryName()) ->selectAndLoadSymbols(); $this->assertTrue(true); } /** * This is more of an acceptance test case instead of a unit test. It verifies * that all the library map is up-to-date. */ public function testLibraryMap() { $root = $this->getLibraryRoot(); $library = phutil_get_library_name_for_root($root); $new_library_map = id(new PhutilLibraryMapBuilder($root)) ->buildMap(); $bootloader = PhutilBootloader::getInstance(); $old_library_map = $bootloader->getLibraryMapWithoutExtensions($library); unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]); $identical = ($new_library_map === $old_library_map); if (!$identical) { $differences = $this->getMapDifferences( $old_library_map, $new_library_map); sort($differences); } else { $differences = array(); } $this->assertTrue( $identical, pht( "The library map is out of date. Rebuild it with `%s`.\n". "These entries differ: %s.", 'arc liberate', implode(', ', $differences))); } private function getMapDifferences($old, $new) { $changed = array(); $all = $old + $new; foreach ($all as $key => $value) { $old_exists = array_key_exists($key, $old); $new_exists = array_key_exists($key, $new); // One map has it and the other does not, so mark it as changed. if ($old_exists != $new_exists) { $changed[] = $key; continue; } $oldv = idx($old, $key); $newv = idx($new, $key); if ($oldv === $newv) { continue; } if (is_array($oldv) && is_array($newv)) { $child_changed = $this->getMapDifferences($oldv, $newv); foreach ($child_changed as $child) { $changed[] = $key.'.'.$child; } } else { $changed[] = $key; } } return $changed; } /** * This is more of an acceptance test case instead of a unit test. It verifies * that methods in subclasses have the same visibility as the method in the * parent class. */ public function testMethodVisibility() { $symbols = id(new PhutilSymbolLoader()) ->setLibrary($this->getLibraryName()) ->selectSymbolsWithoutLoading(); $classes = array(); foreach ($symbols as $symbol) { if ($symbol['type'] == 'class') { $classes[$symbol['name']] = new ReflectionClass($symbol['name']); } } $failures = array(); foreach ($classes as $class_name => $class) { $parents = array(); $parent = $class; while ($parent = $parent->getParentClass()) { $parents[] = $parent; } $interfaces = $class->getInterfaces(); foreach ($class->getMethods() as $method) { $method_name = $method->getName(); foreach (array_merge($parents, $interfaces) as $extends) { if ($extends->hasMethod($method_name)) { $xmethod = $extends->getMethod($method_name); if (!$this->compareVisibility($xmethod, $method)) { $failures[] = pht( 'Class "%s" implements method "%s" with the wrong visibility. '. 'The method has visibility "%s", but it is defined in parent '. - '"%s" with visibility "%s". In Phabricator, a method which '. - 'overrides another must always have the same visibility.', + '"%s" with visibility "%s". A method which overrides another '. + 'must always have the same visibility.', $class_name, $method_name, $this->getVisibility($method), $extends->getName(), $this->getVisibility($xmethod)); } // We found a declaration somewhere, so stop looking. break; } } } } $this->assertTrue( empty($failures), "\n\n".implode("\n\n", $failures)); } /** * Get the name of the library currently being tested. */ protected function getLibraryName() { return phutil_get_library_name_for_root($this->getLibraryRoot()); } /** * Get the root directory for the library currently being tested. */ protected function getLibraryRoot() { $caller = id(new ReflectionClass($this))->getFileName(); return phutil_get_library_root_for_path($caller); } private function compareVisibility( ReflectionMethod $parent_method, ReflectionMethod $method) { static $bitmask; if ($bitmask === null) { $bitmask = ReflectionMethod::IS_PUBLIC; $bitmask += ReflectionMethod::IS_PROTECTED; $bitmask += ReflectionMethod::IS_PRIVATE; } $parent_modifiers = $parent_method->getModifiers(); $modifiers = $method->getModifiers(); return !(($parent_modifiers ^ $modifiers) & $bitmask); } private function getVisibility(ReflectionMethod $method) { if ($method->isPrivate()) { return 'private'; } else if ($method->isProtected()) { return 'protected'; } else { return 'public'; } } } diff --git a/src/configuration/ArcanistSettings.php b/src/configuration/ArcanistSettings.php index 67081703..8f67bf7e 100644 --- a/src/configuration/ArcanistSettings.php +++ b/src/configuration/ArcanistSettings.php @@ -1,315 +1,314 @@ array( 'type' => 'string', 'help' => pht( - 'The URI of a Phabricator install to connect to by default, if '. - '%s is run in a project without a Phabricator URI or run outside '. + 'The URI of a server to connect to by default, if '. + '%s is run in a project without a configured URI or run outside '. 'of a project.', 'arc'), - 'example' => '"http://phabricator.example.com/"', + 'example' => '"http://devtools.example.com/"', ), 'base' => array( 'type' => 'string', 'help' => pht( 'Base commit ruleset to invoke when determining the start of a '. 'commit range. See "Arcanist User Guide: Commit Ranges" for '. 'details.'), 'example' => '"arc:amended, arc:prompt"', ), 'load' => array( 'type' => 'list', 'legacy' => 'phutil_libraries', 'help' => pht( 'A list of paths to phutil libraries that should be loaded at '. 'startup. This can be used to make classes available, like lint '. 'or unit test engines.'), 'default' => array(), 'example' => '["/var/arc/customlib/src"]', ), 'repository.callsign' => array( 'type' => 'string', 'example' => '"X"', 'help' => pht( - 'Associate the working copy with a specific Phabricator repository. '. + 'Associate the working copy with a specific repository. '. 'Normally, %s can figure this association out on its own, but if '. 'your setup is unusual you can use this option to tell it what the '. 'desired value is.', 'arc'), ), 'phabricator.uri' => array( 'type' => 'string', 'legacy' => 'conduit_uri', - 'example' => '"https://phabricator.mycompany.com/"', + 'example' => '"https://devtools.example.com/"', 'help' => pht( - 'Associates this working copy with a specific installation of '. - 'Phabricator.'), + 'Associates this working copy with a specific server.'), ), 'lint.engine' => array( 'type' => 'string', 'legacy' => 'lint_engine', 'help' => pht( 'The name of a default lint engine to use, if no lint engine is '. 'specified by the current project.'), 'example' => '"ExampleLintEngine"', ), 'unit.engine' => array( 'type' => 'string', 'legacy' => 'unit_engine', 'help' => pht( 'The name of a default unit test engine to use, if no unit test '. 'engine is specified by the current project.'), 'example' => '"ExampleUnitTestEngine"', ), 'arc.land.onto.default' => array( 'type' => 'string', 'help' => pht( 'The name of the default branch to land changes onto when '. '`%s` is run.', 'arc land'), 'example' => '"develop"', ), 'history.immutable' => array( 'type' => 'bool', 'legacy' => 'immutable_history', 'help' => pht( 'If true, %s will never change repository history (e.g., through '. 'amending or rebasing). Defaults to true in Mercurial and false in '. 'Git. This setting has no effect in Subversion.', 'arc'), 'example' => 'false', ), 'editor' => array( 'type' => 'string', 'help' => pht( 'Command to use to invoke an interactive editor, like `%s` or `%s`. '. 'This setting overrides the %s environmental variable.', 'nano', 'vim', 'EDITOR'), 'example' => '"nano"', ), 'https.cabundle' => array( 'type' => 'string', 'help' => pht( - "Path to a custom CA bundle file to be used for arcanist's cURL ". - "calls. This is used primarily when your conduit endpoint is ". + "Path to a custom CA bundle file to be used for cURL calls. ". + "This is used primarily when your conduit endpoint is ". "behind HTTPS signed by your organization's internal CA."), 'example' => 'support/yourca.pem', ), 'browser' => array( 'type' => 'string', 'help' => pht('Command to use to invoke a web browser.'), 'example' => '"gnome-www-browser"', ), 'events.listeners' => array( 'type' => 'list', 'help' => pht('List of event listener classes to install at startup.'), 'default' => array(), 'example' => '["ExampleEventListener"]', ), 'arc.autostash' => array( 'type' => 'bool', 'help' => pht( 'Whether %s should permit the automatic stashing of changes in the '. 'working directory when requiring a clean working copy. This option '. 'should only be used when users understand how to restore their '. - 'working directory from the local stash if an Arcanist operation '. + 'working directory from the local stash if an operation '. 'causes an unrecoverable error.', 'arc'), 'default' => false, 'example' => 'false', ), 'aliases' => array( 'type' => 'aliases', 'help' => pht( 'Configured command aliases. Use "arc alias" to define aliases.'), ), ); $settings = ArcanistSetting::getAllSettings(); foreach ($settings as $key => $setting) { $settings[$key] = $setting->getLegacyDictionary(); } $results = $settings + $legacy_builtins; ksort($results); return $results; } private function getOption($key) { return idx($this->getOptions(), $key, array()); } public function getAllKeys() { return array_keys($this->getOptions()); } public function getHelp($key) { return idx($this->getOption($key), 'help'); } public function getExample($key) { return idx($this->getOption($key), 'example'); } public function getType($key) { return idx($this->getOption($key), 'type', 'wild'); } public function getLegacyName($key) { return idx($this->getOption($key), 'legacy'); } public function getDefaultSettings() { $defaults = array(); foreach ($this->getOptions() as $key => $option) { if (array_key_exists('default', $option)) { $defaults[$key] = $option['default']; } } return $defaults; } public function willWriteValue($key, $value) { $type = $this->getType($key); switch ($type) { case 'bool': if (strtolower($value) === 'false' || strtolower($value) === 'no' || strtolower($value) === 'off' || $value === '' || $value === '0' || $value === 0 || $value === false) { $value = false; } else if (strtolower($value) === 'true' || strtolower($value) === 'yes' || strtolower($value) === 'on' || $value === '1' || $value === 1 || $value === true) { $value = true; } else { throw new ArcanistUsageException( pht( "Type of setting '%s' must be boolean, like 'true' or 'false'.", $key)); } break; case 'list': if (is_array($value)) { break; } if (is_string($value)) { $list = json_decode($value, true); if (is_array($list)) { $value = $list; break; } } throw new ArcanistUsageException( pht( "Type of setting '%s' must be list. You can specify a list ". "in JSON, like: %s", $key, '["apple", "banana", "cherry"]')); case 'string': if (!is_scalar($value)) { throw new ArcanistUsageException( pht( "Type of setting '%s' must be string.", $key)); } $value = (string)$value; break; case 'wild': break; case 'aliases': throw new Exception( pht( 'Use "arc alias" to configure aliases, not "arc set-config".')); break; } return $value; } public function willReadValue($key, $value) { $type = $this->getType($key); switch ($type) { case 'string': if (!is_string($value)) { throw new ArcanistUsageException( pht( "Type of setting '%s' must be string.", $key)); } break; case 'bool': if ($value !== true && $value !== false) { throw new ArcanistUsageException( pht( "Type of setting '%s' must be boolean.", $key)); } break; case 'list': if (!is_array($value)) { throw new ArcanistUsageException( pht( "Type of setting '%s' must be list.", $key)); } break; case 'wild': case 'aliases': break; } return $value; } public function formatConfigValueForDisplay($key, $value) { if ($value === false) { return 'false'; } if ($value === true) { return 'true'; } if ($value === null) { return 'null'; } if (is_string($value)) { return '"'.$value.'"'; } if (is_array($value)) { // TODO: Both json_encode() and PhutilJSON do a bad job with one-liners. // PhutilJSON splits them across a bunch of lines, while json_encode() // escapes all kinds of stuff like "/". It would be nice if PhutilJSON // had a mode for pretty one-liners. $value = json_encode($value); // json_encode() unnecessarily escapes "/" to prevent "" stuff, // optimistically unescape it for display to improve readability. $value = preg_replace('@(?rawCorpus = $corpus; $obj->revisionID = $obj->parseRevisionIDFromRawCorpus($corpus); $pattern = '/^git-svn-id:\s*([^@]+)@(\d+)\s+(.*)$/m'; $match = null; if (preg_match($pattern, $corpus, $match)) { $obj->gitSVNBaseRevision = $match[1].'@'.$match[2]; $obj->gitSVNBasePath = $match[1]; $obj->gitSVNUUID = $match[3]; } return $obj; } public function getRawCorpus() { return $this->rawCorpus; } public function getRevisionID() { return $this->revisionID; } public function getRevisionMonogram() { if ($this->revisionID) { return 'D'.$this->revisionID; } return null; } public function pullDataFromConduit( ConduitClient $conduit, $partial = false) { $result = $conduit->callMethodSynchronous( 'differential.parsecommitmessage', array( 'corpus' => $this->rawCorpus, 'partial' => $partial, )); $this->fields = $result['fields']; // NOTE: This does not exist prior to late October 2017. $this->xactions = idx($result, 'transactions'); if (!empty($result['errors'])) { throw new ArcanistDifferentialCommitMessageParserException( $result['errors']); } return $this; } public function getFieldValue($key) { if (array_key_exists($key, $this->fields)) { return $this->fields[$key]; } return null; } public function setFieldValue($key, $value) { $this->fields[$key] = $value; return $this; } public function getFields() { return $this->fields; } public function getGitSVNBaseRevision() { return $this->gitSVNBaseRevision; } public function getGitSVNBasePath() { return $this->gitSVNBasePath; } public function getGitSVNUUID() { return $this->gitSVNUUID; } public function getChecksum() { $fields = array_filter($this->fields); ksort($fields); $fields = json_encode($fields); return md5($fields); } public function getTransactions() { return $this->xactions; } /** * Extract the revision ID from a commit message. * * @param string Raw commit message. * @return int|null Revision ID, if the commit message contains one. */ private function parseRevisionIDFromRawCorpus($corpus) { $match = null; if (!preg_match('/^Differential Revision:\s*(.+)/im', $corpus, $match)) { return null; } $revision_value = trim($match[1]); $revision_pattern = '/^[dD]([1-9]\d*)\z/'; // Accept a bare revision ID like "D123". if (preg_match($revision_pattern, $revision_value, $match)) { return (int)$match[1]; } // Otherwise, try to find a full URI. $uri = new PhutilURI($revision_value); $path = $uri->getPath(); $path = trim($path, '/'); if (preg_match($revision_pattern, $path, $match)) { return (int)$match[1]; } throw new ArcanistUsageException( pht( 'Invalid "Differential Revision" field in commit message. This field '. - 'should have a revision identifier like "%s" or a Phabricator URI '. + 'should have a revision identifier like "%s" or a server URI '. 'like "%s", but has "%s".', 'D123', - 'https://phabricator.example.com/D123', + 'https://devtools.example.com/D123', $revision_value)); } } diff --git a/src/lint/linter/ArcanistCSharpLinter.php b/src/lint/linter/ArcanistCSharpLinter.php index 6120a07b..e2b00b5a 100644 --- a/src/lint/linter/ArcanistCSharpLinter.php +++ b/src/lint/linter/ArcanistCSharpLinter.php @@ -1,261 +1,261 @@ 'map>', 'help' => pht('Provide a discovery map.'), ); // TODO: This should probably be replaced with "bin" when this moves // to extend ExternalLinter. $options['binary'] = array( 'type' => 'string', 'help' => pht('Override default binary.'), ); return $options; } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'discovery': $this->discoveryMap = $value; return; case 'binary': $this->cslintHintPath = $value; return; } parent::setLinterConfigurationValue($key, $value); } protected function getLintCodeFromLinterConfigurationKey($code) { return $code; } public function setCustomSeverityMap(array $map) { foreach ($map as $code => $severity) { if (substr($code, 0, 2) === 'SA' && $severity == 'disabled') { throw new Exception( pht( "In order to keep StyleCop integration with IDEs and other tools ". - "consistent with Arcanist results, you aren't permitted to ". + "consistent with lint results, you aren't permitted to ". "disable StyleCop rules within '%s'. Instead configure the ". "severity using the StyleCop settings dialog (usually accessible ". "from within your IDE). StyleCop settings for your project will ". - "be used when linting for Arcanist.", + "be used when linting.", '.arclint')); } } return parent::setCustomSeverityMap($map); } /** * Determines what executables and lint paths to use. Between platforms * this also changes whether the lint engine is run under .NET or Mono. It * also ensures that all of the required binaries are available for the lint * to run successfully. * * @return void */ private function loadEnvironment() { if ($this->loaded) { return; } // Determine runtime engine (.NET or Mono). if (phutil_is_windows()) { $this->runtimeEngine = ''; } else if (Filesystem::binaryExists('mono')) { $this->runtimeEngine = 'mono '; } else { throw new Exception( pht('Unable to find Mono and you are not on Windows!')); } // Determine cslint path. $cslint = $this->cslintHintPath; if ($cslint !== null && file_exists($cslint)) { $this->cslintEngine = Filesystem::resolvePath($cslint); } else if (Filesystem::binaryExists('cslint.exe')) { $this->cslintEngine = 'cslint.exe'; } else { throw new Exception(pht('Unable to locate %s.', 'cslint')); } // Determine cslint version. $ver_future = new ExecFuture( '%C -v', $this->runtimeEngine.$this->cslintEngine); list($err, $stdout, $stderr) = $ver_future->resolve(); if ($err !== 0) { throw new Exception( pht( 'You are running an old version of %s. Please '. 'upgrade to version %s.', 'cslint', self::SUPPORTED_VERSION)); } $ver = (int)$stdout; if ($ver < self::SUPPORTED_VERSION) { throw new Exception( pht( 'You are running an old version of %s. Please '. 'upgrade to version %s.', 'cslint', self::SUPPORTED_VERSION)); } else if ($ver > self::SUPPORTED_VERSION) { throw new Exception( pht( - 'Arcanist does not support this version of %s (it is newer). '. - 'You can try upgrading Arcanist with `%s`.', + 'This version of %s is not supported (it is too new). '. + 'You can try upgrading with `%s`.', 'cslint', 'arc upgrade')); } $this->loaded = true; } public function lintPath($path) {} public function willLintPaths(array $paths) { $this->loadEnvironment(); $futures = array(); // Bulk linting up into futures, where the number of files // is based on how long the command is. $current_paths = array(); foreach ($paths as $path) { // If the current paths for the command, plus the next path // is greater than 6000 characters (less than the Windows // command line limit), then finalize this future and add it. $total = 0; foreach ($current_paths as $current_path) { $total += strlen($current_path) + 3; // Quotes and space. } if ($total + strlen($path) > 6000) { // %s won't pass through the JSON correctly // under Windows. This is probably because not only // does the JSON have quotation marks in the content, // but because there'll be a lot of escaping and // double escaping because the JSON also contains // regular expressions. cslint supports passing the // settings JSON through base64-encoded to mitigate // this issue. $futures[] = new ExecFuture( '%C --settings-base64=%s -r=. %Ls', $this->runtimeEngine.$this->cslintEngine, base64_encode(json_encode($this->discoveryMap)), $current_paths); $current_paths = array(); } // Append the path to the current paths array. $current_paths[] = $this->getEngine()->getFilePathOnDisk($path); } // If we still have paths left in current paths, then we need to create // a future for those too. if (count($current_paths) > 0) { $futures[] = new ExecFuture( '%C --settings-base64=%s -r=. %Ls', $this->runtimeEngine.$this->cslintEngine, base64_encode(json_encode($this->discoveryMap)), $current_paths); $current_paths = array(); } $this->futures = $futures; } public function didLintPaths(array $paths) { if ($this->futures) { $futures = id(new FutureIterator($this->futures)) ->limit(8); foreach ($futures as $future) { $this->resolveFuture($future); } $this->futures = array(); } } protected function resolveFuture(Future $future) { list($stdout) = $future->resolvex(); $all_results = json_decode($stdout); foreach ($all_results as $results) { if ($results === null || $results->Issues === null) { return; } foreach ($results->Issues as $issue) { $message = new ArcanistLintMessage(); $message->setPath($results->FileName); $message->setLine($issue->LineNumber); $message->setCode($issue->Index->Code); $message->setName($issue->Index->Name); $message->setChar($issue->Column); $message->setOriginalText($issue->OriginalText); $message->setReplacementText($issue->ReplacementText); $desc = @vsprintf($issue->Index->Message, $issue->Parameters); if ($desc === false) { $desc = $issue->Index->Message; } $message->setDescription($desc); $severity = ArcanistLintSeverity::SEVERITY_ADVICE; switch ($issue->Index->Severity) { case 0: $severity = ArcanistLintSeverity::SEVERITY_ADVICE; break; case 1: $severity = ArcanistLintSeverity::SEVERITY_AUTOFIX; break; case 2: $severity = ArcanistLintSeverity::SEVERITY_WARNING; break; case 3: $severity = ArcanistLintSeverity::SEVERITY_ERROR; break; case 4: $severity = ArcanistLintSeverity::SEVERITY_DISABLED; break; } $severity_override = $this->getLintMessageSeverity($issue->Index->Code); if ($severity_override !== null) { $severity = $severity_override; } $message->setSeverity($severity); $this->addLintMessage($message); } } } protected function getDefaultMessageSeverity($code) { return null; } } diff --git a/src/lint/linter/ArcanistPhutilLibraryLinter.php b/src/lint/linter/ArcanistPhutilLibraryLinter.php index 3cef927b..955e2073 100644 --- a/src/lint/linter/ArcanistPhutilLibraryLinter.php +++ b/src/lint/linter/ArcanistPhutilLibraryLinter.php @@ -1,370 +1,372 @@ pht('Unknown Symbol'), self::LINT_DUPLICATE_SYMBOL => pht('Duplicate Symbol'), self::LINT_ONE_CLASS_PER_FILE => pht('One Class Per File'), self::LINT_NONCANONICAL_SYMBOL => pht('Noncanonical Symbol'), ); } public function getLinterPriority() { return 2.0; } public function willLintPaths(array $paths) { $libtype_map = array( 'class' => 'class', 'function' => 'function', 'interface' => 'class', 'class/interface' => 'class', ); // NOTE: For now, we completely ignore paths and just lint every library in // its entirety. This is simpler and relatively fast because we don't do any // detailed checks and all the data we need for this comes out of module // caches. $bootloader = PhutilBootloader::getInstance(); $libraries = $bootloader->getAllLibraries(); // Load all the builtin symbols first. $builtin_map = PhutilLibraryMapBuilder::newBuiltinMap(); $builtin_map = $builtin_map['have']; $normal_symbols = array(); $all_symbols = array(); foreach ($builtin_map as $type => $builtin_symbols) { $libtype = $libtype_map[$type]; foreach ($builtin_symbols as $builtin_symbol => $ignored) { $normal_symbol = $this->normalizeSymbol($builtin_symbol); $normal_symbols[$type][$normal_symbol] = $builtin_symbol; $all_symbols[$libtype][$builtin_symbol] = array( 'library' => null, 'file' => null, 'offset' => null, ); } } // Load the up-to-date map for each library, without loading the library // itself. This means lint results will accurately reflect the state of // the working copy. $symbols = array(); foreach ($libraries as $library) { $root = phutil_get_library_root($library); try { $symbols[$library] = id(new PhutilLibraryMapBuilder($root)) ->buildFileSymbolMap(); } catch (XHPASTSyntaxErrorException $ex) { // If the library contains a syntax error then there isn't much that we // can do. continue; } } foreach ($symbols as $library => $map) { // Check for files which declare more than one class/interface in the same // file, or mix function definitions with class/interface definitions. We // must isolate autoloadable symbols to one per file so the autoloader // can't end up in an unresolvable cycle. foreach ($map as $file => $spec) { $have = idx($spec, 'have', array()); $have_classes = idx($have, 'class', array()) + idx($have, 'interface', array()); $have_functions = idx($have, 'function'); if ($have_functions && $have_classes) { $function_list = implode(', ', array_keys($have_functions)); $class_list = implode(', ', array_keys($have_classes)); $this->raiseLintInLibrary( $library, $file, end($have_functions), self::LINT_ONE_CLASS_PER_FILE, pht( "File '%s' mixes function (%s) and class/interface (%s) ". "definitions in the same file. A file which declares a class ". "or an interface MUST declare nothing else.", $file, $function_list, $class_list)); } else if (count($have_classes) > 1) { $class_list = implode(', ', array_keys($have_classes)); $this->raiseLintInLibrary( $library, $file, end($have_classes), self::LINT_ONE_CLASS_PER_FILE, pht( "File '%s' declares more than one class or interface (%s). ". "A file which declares a class or interface MUST declare ". "nothing else.", $file, $class_list)); } } // Check for duplicate symbols: two files providing the same class or // function. While doing this, we also build a map of normalized symbol // names to original symbol names: we want a definition of "idx()" to // collide with a definition of "IdX()", and want to perform spelling // corrections later. foreach ($map as $file => $spec) { $have = idx($spec, 'have', array()); foreach (array('class', 'function', 'interface') as $type) { $libtype = $libtype_map[$type]; foreach (idx($have, $type, array()) as $symbol => $offset) { $normal_symbol = $this->normalizeSymbol($symbol); if (empty($normal_symbols[$libtype][$normal_symbol])) { $normal_symbols[$libtype][$normal_symbol] = $symbol; $all_symbols[$libtype][$symbol] = array( 'library' => $library, 'file' => $file, 'offset' => $offset, ); continue; } $old_symbol = $normal_symbols[$libtype][$normal_symbol]; $old_src = $all_symbols[$libtype][$old_symbol]['file']; $old_lib = $all_symbols[$libtype][$old_symbol]['library']; // If these values are "null", it means that the symbol is a // builtin symbol provided by PHP or a PHP extension. if ($old_lib === null) { $message = pht( 'Definition of symbol "%s" (of type "%s") in file "%s" in '. 'library "%s" duplicates builtin definition of the same '. 'symbol.', $symbol, $type, $file, $library); } else { $message = pht( 'Definition of symbol "%s" (of type "%s") in file "%s" in '. 'library "%s" duplicates prior definition in file "%s" in '. 'library "%s".', $symbol, $type, $file, $library, $old_src, $old_lib); } $this->raiseLintInLibrary( $library, $file, $offset, self::LINT_DUPLICATE_SYMBOL, $message); } } } } $types = array('class', 'function', 'interface', 'class/interface'); foreach ($symbols as $library => $map) { // Check for unknown symbols: uses of classes, functions or interfaces // which are not defined anywhere. We reference the list of all symbols // we built up earlier. foreach ($map as $file => $spec) { $need = idx($spec, 'need', array()); foreach ($types as $type) { $libtype = $libtype_map[$type]; foreach (idx($need, $type, array()) as $symbol => $offset) { if (!empty($all_symbols[$libtype][$symbol])) { // Symbol is defined somewhere. continue; } $normal_symbol = $this->normalizeSymbol($symbol); if (!empty($normal_symbols[$libtype][$normal_symbol])) { $proper_symbol = $normal_symbols[$libtype][$normal_symbol]; switch ($type) { case 'class': $summary = pht( 'Class symbol "%s" should be written as "%s".', $symbol, $proper_symbol); break; case 'function': $summary = pht( 'Function symbol "%s" should be written as "%s".', $symbol, $proper_symbol); break; case 'interface': $summary = pht( 'Interface symbol "%s" should be written as "%s".', $symbol, $proper_symbol); break; case 'class/interface': $summary = pht( 'Class or interface symbol "%s" should be written as "%s".', $symbol, $proper_symbol); break; default: throw new Exception( pht('Unknown symbol type "%s".', $type)); } $this->raiseLintInLibrary( $library, $file, $offset, self::LINT_NONCANONICAL_SYMBOL, $summary, $symbol, $proper_symbol); continue; } $arcanist_root = dirname(phutil_get_library_root('arcanist')); switch ($type) { case 'class': $summary = pht( 'Use of unknown class symbol "%s".', $symbol); break; case 'function': $summary = pht( 'Use of unknown function symbol "%s".', $symbol); break; case 'interface': $summary = pht( 'Use of unknown interface symbol "%s".', $symbol); break; case 'class/interface': $summary = pht( 'Use of unknown class or interface symbol "%s".', $symbol); break; } $details = pht( "Common causes are:\n". "\n". - " - Your copy of Arcanist is out of date.\n". + " - Your copy of %s is out of date.\n". " This is the most common cause.\n". - " Update this copy of Arcanist:\n". + " Update this copy of %s:\n". "\n". " %s\n". "\n". " - Some other library is out of date.\n". " Update the library this symbol appears in.\n". "\n". " - The symbol is misspelled.\n". " Spell the symbol name correctly.\n". "\n". " - You added the symbol recently, but have not updated\n". " the symbol map for the library.\n". " Run \"arc liberate\" in the library where the symbol is\n". " defined.\n". "\n". " - This symbol is defined in an external library.\n". " Use \"@phutil-external-symbol\" to annotate it.\n". " Use \"grep\" to find examples of usage.", + PlatformSymbols::getPlatformClientName(), + PlatformSymbols::getPlatformClientName(), $arcanist_root); $message = implode( "\n\n", array( $summary, $details, )); $this->raiseLintInLibrary( $library, $file, $offset, self::LINT_UNKNOWN_SYMBOL, $message); } } } } } private function raiseLintInLibrary( $library, $path, $offset, $code, $desc, $original = null, $replacement = null) { $root = phutil_get_library_root($library); $this->activePath = $root.'/'.$path; $this->raiseLintAtOffset($offset, $code, $desc, $original, $replacement); } public function lintPath($path) { return; } private function normalizeSymbol($symbol) { return phutil_utf8_strtolower($symbol); } } diff --git a/src/lint/linter/xhpast/rules/ArcanistProductNameLiteralXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistProductNameLiteralXHPASTLinterRule.php index 70b57486..20f3e453 100644 --- a/src/lint/linter/xhpast/rules/ArcanistProductNameLiteralXHPASTLinterRule.php +++ b/src/lint/linter/xhpast/rules/ArcanistProductNameLiteralXHPASTLinterRule.php @@ -1,67 +1,67 @@ selectDescendantsOfType('n_FUNCTION_CALL'); $product_names = PlatformSymbols::getProductNames(); foreach ($product_names as $k => $product_name) { $product_names[$k] = preg_quote($product_name); } $search_pattern = '(\b(?:'.implode('|', $product_names).')\b)i'; foreach ($calls as $call) { $name = $call->getChildByIndex(0)->getConcreteString(); if ($name !== 'pht') { continue; } $parameters = $call->getChildByIndex(1); if (!$parameters->getChildren()) { continue; } $identifier = $parameters->getChildByIndex(0); if (!$identifier->isConstantString()) { continue; } - $literal_value = $identifier->getStringLiteralValue(); + $literal_value = $identifier->evalStatic(); $matches = phutil_preg_match_all($search_pattern, $literal_value); if (!$matches[0]) { continue; } $name_list = array(); foreach ($matches[0] as $match) { $name_list[phutil_utf8_strtolower($match)] = $match; } $name_list = implode(', ', $name_list); $this->raiseLintAtNode( $identifier, pht( 'Avoid use of product name literals in "pht()": use generic '. 'language or an appropriate method from the "PlatformSymbols" class '. 'instead so the software can be forked. String uses names: %s.', $name_list)); } } } diff --git a/src/runtime/ArcanistRuntime.php b/src/runtime/ArcanistRuntime.php index 35099b2c..8e3d220f 100644 --- a/src/runtime/ArcanistRuntime.php +++ b/src/runtime/ArcanistRuntime.php @@ -1,914 +1,914 @@ checkEnvironment(); } catch (Exception $ex) { echo "CONFIGURATION ERROR\n\n"; echo $ex->getMessage(); echo "\n\n"; return 1; } PhutilTranslator::getInstance() ->setLocale(PhutilLocale::loadLocale('en_US')) ->setTranslations(PhutilTranslation::getTranslationMapForLocale('en_US')); $log = new ArcanistLogEngine(); $this->logEngine = $log; try { return $this->executeCore($argv); } catch (ArcanistConduitException $ex) { $log->writeError(pht('CONDUIT'), $ex->getMessage()); } catch (PhutilArgumentUsageException $ex) { $log->writeError(pht('USAGE EXCEPTION'), $ex->getMessage()); } catch (ArcanistUserAbortException $ex) { $log->writeError(pht('---'), $ex->getMessage()); } catch (ArcanistConduitAuthenticationException $ex) { $log->writeError($ex->getTitle(), $ex->getBody()); } return 1; } private function executeCore(array $argv) { $log = $this->getLogEngine(); $config_args = array( array( 'name' => 'library', 'param' => 'path', 'help' => pht('Load a library.'), 'repeat' => true, ), array( 'name' => 'config', 'param' => 'key=value', 'repeat' => true, 'help' => pht('Specify a runtime configuration value.'), ), array( 'name' => 'config-file', 'param' => 'path', 'repeat' => true, 'help' => pht( 'Load one or more configuration files. If this flag is provided, '. 'the system and user configuration files are ignored.'), ), ); $args = id(new PhutilArgumentParser($argv)) ->parseStandardArguments(); // If we can test whether STDIN is a TTY, and it isn't, require that "--" // appear in the argument list. This is intended to make it very hard to // write unsafe scripts on top of Arcanist. if (phutil_is_noninteractive()) { $args->setRequireArgumentTerminator(true); } $is_trace = $args->getArg('trace'); $log->setShowTraceMessages($is_trace); $log->writeTrace(pht('ARGV'), csprintf('%Ls', $argv)); // We're installing the signal handler after parsing "--trace" so that it // can emit debugging messages. This means there's a very small window at // startup where signals have no special handling, but we couldn't really // route them or do anything interesting with them anyway. $this->installSignalHandler(); $args->parsePartial($config_args, true); $config_engine = $this->loadConfiguration($args); $config = $config_engine->newConfigurationSourceList(); $this->loadLibraries($config_engine, $config, $args); // Now that we've loaded libraries, we can validate configuration. // Do this before continuing since configuration can impact other // behaviors immediately and we want to catch any issues right away. $config->setConfigOptions($config_engine->newConfigOptionsMap()); $config->validateConfiguration($this); $toolset = $this->newToolset($argv); $this->setToolset($toolset); $args->parsePartial($toolset->getToolsetArguments()); $workflows = $this->newWorkflows($toolset); $this->workflows = $workflows; $conduit_engine = $this->newConduitEngine($config, $args); $this->conduitEngine = $conduit_engine; $phutil_workflows = array(); foreach ($workflows as $key => $workflow) { $workflow ->setRuntime($this) ->setConfigurationEngine($config_engine) ->setConfigurationSourceList($config) ->setConduitEngine($conduit_engine); $phutil_workflows[$key] = $workflow->newPhutilWorkflow(); } $unconsumed_argv = $args->getUnconsumedArgumentVector(); if (!$unconsumed_argv) { // TOOLSETS: This means the user just ran "arc" or some other top-level // toolset without any workflow argument. We should give them a summary // of the toolset, a list of workflows, and a pointer to "arc help" for // more details. // A possible exception is "arc --help", which should perhaps pass // through and act like "arc help". throw new PhutilArgumentUsageException(pht('Choose a workflow!')); } $alias_effects = id(new ArcanistAliasEngine()) ->setRuntime($this) ->setToolset($toolset) ->setWorkflows($workflows) ->setConfigurationSourceList($config) ->resolveAliases($unconsumed_argv); foreach ($alias_effects as $alias_effect) { if ($alias_effect->getType() === ArcanistAliasEffect::EFFECT_SHELL) { return $this->executeShellAlias($alias_effect); } } $result_argv = $this->applyAliasEffects($alias_effects, $unconsumed_argv); $args->setUnconsumedArgumentVector($result_argv); // TOOLSETS: Some day, stop falling through to the old "arc" runtime. $help_workflows = $this->getHelpWorkflows($phutil_workflows); $args->setHelpWorkflows($help_workflows); try { return $args->parseWorkflowsFull($phutil_workflows); } catch (ArcanistMissingArgumentTerminatorException $terminator_exception) { $log->writeHint( pht('USAGE'), pht( '"%s" is being run noninteractively, but the argument list is '. 'missing "--" to indicate end of flags.', $toolset->getToolsetKey())); $log->writeHint( pht('USAGE'), pht( 'When running noninteractively, you MUST provide "--" to all '. 'commands (even if they take no arguments).')); $log->writeHint( pht('USAGE'), tsprintf( '%s <__%s__>', pht('Learn More:'), 'https://phurl.io/u/noninteractive')); throw new PhutilArgumentUsageException( pht('Missing required "--" in argument list.')); } catch (PhutilArgumentUsageException $usage_exception) { // TODO: This is very, very hacky; we're trying to let errors like // "you passed the wrong arguments" through but fall back to classic // mode if the workflow itself doesn't exist. if (!preg_match('/invalid command/i', $usage_exception->getMessage())) { throw $usage_exception; } } $arcanist_root = phutil_get_library_root('arcanist'); $arcanist_root = dirname($arcanist_root); $bin = $arcanist_root.'/scripts/arcanist.php'; $err = phutil_passthru( 'php -f %R -- %Ls', $bin, array_slice($argv, 1)); return $err; } /** * 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. */ private function checkEnvironment() { // NOTE: We don't have phutil_is_windows() yet here. $is_windows = (DIRECTORY_SEPARATOR != '/'); // NOTE: There's a hard PHP version check earlier, in "init-script.php". 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]; } } list($what, $which) = $resolution; if ($what == 'flag' && strpos($config, $which) !== false) { $show_config = true; $problems[] = sprintf( 'The build of PHP you are running was compiled with the configure '. 'flag "%s", which means it does not support the function "%s()". '. - 'This function is required for Arcanist to run. Install a standard '. - 'build of PHP or rebuild it without this flag. You may also be '. - 'able to build or install the relevant extension separately.', + 'This function is required for this software to run. Install a '. + 'standard build of PHP or rebuild it without this flag. You may '. + 'also be able to build or install the relevant extension separately.', $which, $fname); continue; } if ($what == 'builtin-dll') { $problems[] = sprintf( 'The build of PHP you are running does not have the "%s" extension '. 'enabled. Edit your php.ini file and uncomment the line which '. 'reads "extension=%s".', $which, $which); continue; } if ($what == 'text') { $problems[] = $which; continue; } $problems[] = sprintf( 'The build of PHP you are running is missing the required function '. '"%s()". Rebuild PHP or install the extension which provides "%s()".', $fname, $fname); } if ($problems) { if ($show_config) { $problems[] = "PHP was built with this configure command:\n\n{$config}"; } $problems = implode("\n\n", $problems); throw new Exception($problems); } } private function loadConfiguration(PhutilArgumentParser $args) { $engine = id(new ArcanistConfigurationEngine()) ->setArguments($args); $working_copy = ArcanistWorkingCopy::newFromWorkingDirectory(getcwd()); $engine->setWorkingCopy($working_copy); $this->workingCopy = $working_copy; $working_copy ->getRepositoryAPI() ->setRuntime($this); return $engine; } private function loadLibraries( ArcanistConfigurationEngine $engine, ArcanistConfigurationSourceList $config, PhutilArgumentParser $args) { $sources = array(); $cli_libraries = $args->getArg('library'); if ($cli_libraries) { $sources = array(); foreach ($cli_libraries as $cli_library) { $sources[] = array( 'type' => 'flag', 'library-source' => $cli_library, ); } } else { $items = $config->getStorageValueList('load'); foreach ($items as $item) { foreach ($item->getValue() as $library_path) { $sources[] = array( 'type' => 'config', 'config-source' => $item->getConfigurationSource(), 'library-source' => $library_path, ); } } } foreach ($sources as $spec) { $library_source = $spec['library-source']; switch ($spec['type']) { case 'flag': $description = pht('runtime --library flag'); break; case 'config': $config_source = $spec['config-source']; $description = pht( 'Configuration (%s)', $config_source->getSourceDisplayName()); break; } $this->loadLibrary($engine, $library_source, $description); } } private function loadLibrary( ArcanistConfigurationEngine $engine, $location, $description) { // TODO: This is a legacy system that should be replaced with package // management. $log = $this->getLogEngine(); $working_copy = $engine->getWorkingCopy(); if ($working_copy) { $working_copy_root = $working_copy->getPath(); $working_directory = $working_copy->getWorkingDirectory(); } else { $working_copy_root = null; $working_directory = getcwd(); } // 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. if ($working_copy_root !== null) { $resolved_location = Filesystem::resolvePath( $location, $working_copy_root); 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_root)); if (Filesystem::pathExists($resolved_location)) { $location = $resolved_location; $resolved = true; } } } // Look beside "arcanist/". This is rule (3) above. if (!$resolved) { $arcanist_root = phutil_get_library_root('arcanist'); $arcanist_root = dirname(dirname($arcanist_root)); $resolved_location = Filesystem::resolvePath( $location, $arcanist_root); if (Filesystem::pathExists($resolved_location)) { $location = $resolved_location; $resolved = true; } } $log->writeTrace( pht('LOAD'), pht('Loading library from "%s"...', $location)); $error = null; try { phutil_load_library($location); } catch (PhutilLibraryConflictException $ex) { if ($ex->getLibrary() != 'arcanist') { throw $ex; } // NOTE: If you are running `arc` against itself, we ignore the library // conflict created by loading the local `arc` library (in the current // working directory) and continue without loading it. // This means we only execute code in the `arcanist/` directory which is // associated with the binary you are running, whereas we would normally // execute local code. // This can make `arc` development slightly confusing if your setup is // especially bizarre, but it allows `arc` to be used in automation // workflows more easily. For some context, see PHI13. $executing_directory = dirname(dirname(__FILE__)); $log->writeWarn( pht('VERY META'), pht( - 'You are running one copy of Arcanist (at path "%s") against '. - 'another copy of Arcanist (at path "%s"). Code in the current '. + 'You are running one copy of this software (at path "%s") against '. + 'another copy of this software (at path "%s"). Code in the current '. 'working directory will not be loaded or executed.', $executing_directory, $working_directory)); } catch (PhutilBootloaderException $ex) { $log->writeError( pht('LIBRARY ERROR'), pht( 'Failed to load library at location "%s". This library '. 'is specified by "%s". Check that the library is up to date.', $location, $description)); $prompt = pht('Continue without loading library?'); if (!phutil_console_confirm($prompt)) { throw $ex; } } catch (Exception $ex) { $log->writeError( pht('LOAD ERROR'), pht( 'Failed to load library at location "%s". This library is '. 'specified by "%s". Check that the setting is correct and the '. 'library is located in the right place.', $location, $description)); $prompt = pht('Continue without loading library?'); if (!phutil_console_confirm($prompt)) { throw $ex; } } } private function newToolset(array $argv) { $binary = basename($argv[0]); $toolsets = ArcanistToolset::newToolsetMap(); if (!isset($toolsets[$binary])) { throw new PhutilArgumentUsageException( pht( - 'Arcanist toolset "%s" is unknown. The Arcanist binary should '. - 'be executed so that "argv[0]" identifies a supported toolset. '. - 'Rename the binary or install the library that provides the '. - 'desired toolset. Current available toolsets: %s.', + 'Toolset "%s" is unknown. The binary should be executed so that '. + '"argv[0]" identifies a supported toolset. Rename the binary or '. + 'install the library that provides the desired toolset. Current '. + 'available toolsets: %s.', $binary, implode(', ', array_keys($toolsets)))); } return $toolsets[$binary]; } private function newWorkflows(ArcanistToolset $toolset) { $workflows = id(new PhutilClassMapQuery()) ->setAncestorClass('ArcanistWorkflow') ->setContinueOnFailure(true) ->execute(); foreach ($workflows as $key => $workflow) { if (!$workflow->supportsToolset($toolset)) { unset($workflows[$key]); } } $map = array(); foreach ($workflows as $workflow) { $key = $workflow->getWorkflowName(); if (isset($map[$key])) { throw new Exception( pht( 'Two workflows ("%s" and "%s") both have the same name ("%s") '. 'and both support the current toolset ("%s", "%s"). Each '. 'workflow in a given toolset must have a unique name.', get_class($workflow), get_class($map[$key]), $key, get_class($toolset), $toolset->getToolsetKey())); } $map[$key] = id(clone $workflow) ->setToolset($toolset); } return $map; } public function getWorkflows() { return $this->workflows; } public function getLogEngine() { return $this->logEngine; } private function applyAliasEffects(array $effects, array $argv) { assert_instances_of($effects, 'ArcanistAliasEffect'); $log = $this->getLogEngine(); $command = null; $arguments = null; foreach ($effects as $effect) { $message = $effect->getMessage(); if ($message !== null) { $log->writeHint(pht('ALIAS'), $message); } if ($effect->getCommand()) { $command = $effect->getCommand(); $arguments = $effect->getArguments(); } } if ($command !== null) { $argv = array_merge(array($command), $arguments); } return $argv; } private function installSignalHandler() { $log = $this->getLogEngine(); if (!function_exists('pcntl_signal')) { $log->writeTrace( pht('PCNTL'), pht( 'Unable to install signal handler, pcntl_signal() unavailable. '. 'Continuing without signal handling.')); return; } // NOTE: SIGHUP, SIGTERM and SIGWINCH are handled by "PhutilSignalRouter". // This logic is largely similar to the logic there, but more specific to // Arcanist workflows. pcntl_signal(SIGINT, array($this, 'routeSignal')); } public function routeSignal($signo) { switch ($signo) { case SIGINT: $this->routeInterruptSignal($signo); break; } } private function routeInterruptSignal($signo) { $log = $this->getLogEngine(); $last_interrupt = $this->lastInterruptTime; $now = microtime(true); $this->lastInterruptTime = $now; $should_exit = false; // If we received another SIGINT recently, always exit. This implements // "press ^C twice in quick succession to exit" regardless of what the // workflow may decide to do. $interval = 2; if ($last_interrupt !== null) { if ($now - $last_interrupt < $interval) { $should_exit = true; } } $handler = null; if (!$should_exit) { // Look for an interrupt handler in the current workflow stack. $stack = $this->getWorkflowStack(); foreach ($stack as $workflow) { if ($workflow->canHandleSignal($signo)) { $handler = $workflow; break; } } // If no workflow in the current execution stack can handle an interrupt // signal, just exit on the first interrupt. if (!$handler) { $should_exit = true; } } // It's common for users to ^C on prompts. Write a newline before writing // a response to the interrupt so the behavior is a little cleaner. This // also avoids lines that read "^C [ INTERRUPT ] ...". $log->writeNewline(); if ($should_exit) { $log->writeHint( pht('INTERRUPT'), pht('Interrupted by SIGINT (^C).')); exit(128 + $signo); } $log->writeHint( pht('INTERRUPT'), pht('Press ^C again to exit.')); $handler->handleSignal($signo); } public function pushWorkflow(ArcanistWorkflow $workflow) { $this->stack[] = $workflow; return $this; } public function popWorkflow() { if (!$this->stack) { throw new Exception(pht('Trying to pop an empty workflow stack!')); } return array_pop($this->stack); } public function getWorkflowStack() { return $this->stack; } public function getCurrentWorkflow() { return last($this->stack); } private function newConduitEngine( ArcanistConfigurationSourceList $config, PhutilArgumentParser $args) { try { $force_uri = $args->getArg('conduit-uri'); } catch (PhutilArgumentSpecificationException $ex) { $force_uri = null; } try { $force_token = $args->getArg('conduit-token'); } catch (PhutilArgumentSpecificationException $ex) { $force_token = null; } if ($force_uri !== null) { $conduit_uri = $force_uri; } else { $conduit_uri = $config->getConfig('phabricator.uri'); if ($conduit_uri === null) { // For now, read this older config from raw storage. There is currently // no definition of this option in the "toolsets" config list, and it // would be nice to get rid of it. $default_list = $config->getStorageValueList('default'); if ($default_list) { $conduit_uri = last($default_list)->getValue(); } } } 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 = phutil_string_cast($conduit_uri); } $engine = new ArcanistConduitEngine(); if ($conduit_uri !== null) { $engine->setConduitURI($conduit_uri); } // TODO: This isn't using "getConfig()" because we aren't defining a // real config entry for the moment. if ($force_token !== null) { $conduit_token = $force_token; } else { $hosts = array(); $hosts_list = $config->getStorageValueList('hosts'); foreach ($hosts_list as $hosts_config) { $hosts += $hosts_config->getValue(); } $host_config = idx($hosts, $conduit_uri, array()); $conduit_token = idx($host_config, 'token'); } if ($conduit_token !== null) { $engine->setConduitToken($conduit_token); } return $engine; } private function executeShellAlias(ArcanistAliasEffect $effect) { $log = $this->getLogEngine(); $message = $effect->getMessage(); if ($message !== null) { $log->writeHint(pht('SHELL ALIAS'), $message); } return phutil_passthru('%Ls', $effect->getArguments()); } public function getSymbolEngine() { if ($this->symbolEngine === null) { $this->symbolEngine = $this->newSymbolEngine(); } return $this->symbolEngine; } private function newSymbolEngine() { return id(new ArcanistSymbolEngine()) ->setWorkflow($this); } public function getHardpointEngine() { if ($this->hardpointEngine === null) { $this->hardpointEngine = $this->newHardpointEngine(); } return $this->hardpointEngine; } private function newHardpointEngine() { $engine = new ArcanistHardpointEngine(); $queries = ArcanistRuntimeHardpointQuery::getAllQueries(); foreach ($queries as $key => $query) { $queries[$key] = id(clone $query) ->setRuntime($this); } $engine->setQueries($queries); return $engine; } public function getViewer() { if (!$this->viewer) { $viewer = $this->getSymbolEngine() ->loadUserForSymbol('viewer()'); // TODO: Deal with anonymous stuff. if (!$viewer) { throw new Exception(pht('No viewer!')); } $this->viewer = $viewer; } return $this->viewer; } public function loadHardpoints($objects, $requests) { if (!is_array($objects)) { $objects = array($objects); } if (!is_array($requests)) { $requests = array($requests); } $engine = $this->getHardpointEngine(); $requests = $engine->requestHardpoints( $objects, $requests); // TODO: Wait for only the required requests. $engine->waitForRequests(array()); } public function getWorkingCopy() { return $this->workingCopy; } public function getConduitEngine() { return $this->conduitEngine; } public function setToolset($toolset) { $this->toolset = $toolset; return $this; } public function getToolset() { return $this->toolset; } private function getHelpWorkflows(array $workflows) { if ($this->getToolset()->getToolsetKey() === 'arc') { $legacy = array(); $legacy[] = new ArcanistCloseRevisionWorkflow(); $legacy[] = new ArcanistCommitWorkflow(); $legacy[] = new ArcanistCoverWorkflow(); $legacy[] = new ArcanistDiffWorkflow(); $legacy[] = new ArcanistExportWorkflow(); $legacy[] = new ArcanistGetConfigWorkflow(); $legacy[] = new ArcanistSetConfigWorkflow(); $legacy[] = new ArcanistInstallCertificateWorkflow(); $legacy[] = new ArcanistLintersWorkflow(); $legacy[] = new ArcanistLintWorkflow(); $legacy[] = new ArcanistListWorkflow(); $legacy[] = new ArcanistPatchWorkflow(); $legacy[] = new ArcanistPasteWorkflow(); $legacy[] = new ArcanistTasksWorkflow(); $legacy[] = new ArcanistTodoWorkflow(); $legacy[] = new ArcanistUnitWorkflow(); $legacy[] = new ArcanistWhichWorkflow(); foreach ($legacy as $workflow) { // If this workflow has been updated but not removed from the list // above yet, just skip it. if ($workflow instanceof ArcanistArcWorkflow) { continue; } $workflows[] = $workflow->newLegacyPhutilWorkflow(); } } return $workflows; } } diff --git a/src/unit/engine/PhutilUnitTestEngine.php b/src/unit/engine/PhutilUnitTestEngine.php index 42481434..dc88db4a 100644 --- a/src/unit/engine/PhutilUnitTestEngine.php +++ b/src/unit/engine/PhutilUnitTestEngine.php @@ -1,224 +1,222 @@ getRunAllTests()) { $run_tests = $this->getAllTests(); } else { $run_tests = $this->getTestsForPaths(); } if (!$run_tests) { throw new ArcanistNoEffectException(pht('No tests to run.')); } $enable_coverage = $this->getEnableCoverage(); if ($enable_coverage !== false) { if (!function_exists('xdebug_start_code_coverage')) { if ($enable_coverage === true) { throw new ArcanistUsageException( pht( 'You specified %s but %s is not available, so '. 'coverage can not be enabled for %s.', '--coverage', 'XDebug', __CLASS__)); } } else { $enable_coverage = true; } } $test_cases = array(); foreach ($run_tests as $test_class) { $test_case = newv($test_class, array()) ->setEnableCoverage($enable_coverage) ->setWorkingCopy($this->getWorkingCopy()); if ($this->getPaths()) { $test_case->setPaths($this->getPaths()); } if ($this->renderer) { $test_case->setRenderer($this->renderer); } $test_cases[] = $test_case; } foreach ($test_cases as $test_case) { $test_case->willRunTestCases($test_cases); } $results = array(); foreach ($test_cases as $test_case) { $results[] = $test_case->run(); } $results = array_mergev($results); foreach ($test_cases as $test_case) { $test_case->didRunTestCases($test_cases); } return $results; } private function getAllTests() { $project_root = $this->getWorkingCopy()->getProjectRoot(); $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass('PhutilTestCase') ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); $in_working_copy = array(); $run_tests = array(); foreach ($symbols as $symbol) { if (!preg_match('@(?:^|/)__tests__/@', $symbol['where'])) { continue; } $library = $symbol['library']; if (!isset($in_working_copy[$library])) { $library_root = phutil_get_library_root($library); $in_working_copy[$library] = Filesystem::isDescendant( $library_root, $project_root); } if ($in_working_copy[$library]) { $run_tests[] = $symbol['name']; } } return $run_tests; } /** * Retrieve all relevant test cases. * * Looks for any class that extends @{class:PhutilTestCase} inside a * `__tests__` directory in any parent directory of every affected file. * * The idea is that "infrastructure/__tests__/" tests defines general tests * for all of "infrastructure/", and those tests run for any change in * "infrastructure/". However, "infrastructure/concrete/rebar/__tests__/" * defines more specific tests that run only when "rebar/" (or some * subdirectory) changes. * * @return list The names of the test case classes to be executed. */ private function getTestsForPaths() { $look_here = $this->getTestPaths(); $run_tests = array(); foreach ($look_here as $path_info) { $library = $path_info['library']; $path = $path_info['path']; $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setLibrary($library) ->setPathPrefix($path) ->setAncestorClass('PhutilTestCase') ->setConcreteOnly(true) ->selectAndLoadSymbols(); foreach ($symbols as $symbol) { $run_tests[$symbol['name']] = true; } } return array_keys($run_tests); } /** * Returns the paths in which we should look for tests to execute. * * @return list A list of paths in which to search for test cases. */ public function getTestPaths() { $root = $this->getWorkingCopy()->getProjectRoot(); $paths = array(); foreach ($this->getPaths() as $path) { $library_root = phutil_get_library_root_for_path($path); if (!$library_root) { continue; } $library_name = phutil_get_library_name_for_root($library_root); if (!$library_name) { throw new Exception( pht( - "Attempting to run unit tests on a libphutil library which has ". + "Attempting to run unit tests on a library which has ". "not been loaded, at:\n\n". " %s\n\n". - "This probably means one of two things:\n\n". - " - You may need to add this library to %s.\n". - " - You may be running tests on a copy of libphutil or ". - "arcanist using a different copy of libphutil or arcanist. ". - "This operation is not supported.\n", - $library_root, - '.arcconfig.')); + "Make sure this library is configured to load.\n\n". + "(In rare cases, this may be because you are attempting to run ". + "one copy of this software against a different copy of this ". + "software. This operation is not supported.)", + $library_root)); } $path = Filesystem::resolvePath($path, $root); $library_path = Filesystem::readablePath($path, $library_root); if (!Filesystem::isDescendant($path, $library_root)) { // We have encountered some kind of symlink maze -- for instance, $path // is some symlink living outside the library that links into some file // inside the library. Just ignore these cases, since the affected file // does not actually lie within the library. continue; } if (is_file($path) && preg_match('@(?:^|/)__tests__/@', $path)) { $paths[$library_name.':'.$library_path] = array( 'library' => $library_name, 'path' => $library_path, ); continue; } foreach (Filesystem::walkToRoot($path, $library_root) as $subpath) { if ($subpath == $library_root) { $paths[$library_name.':.'] = array( 'library' => $library_name, 'path' => '__tests__/', ); } else { $library_subpath = Filesystem::readablePath($subpath, $library_root); $paths[$library_name.':'.$library_subpath] = array( 'library' => $library_name, 'path' => $library_subpath.'/__tests__/', ); } } } return $paths; } } diff --git a/src/workflow/ArcanistPatchWorkflow.php b/src/workflow/ArcanistPatchWorkflow.php index 66d5c32c..acfb06ee 100644 --- a/src/workflow/ArcanistPatchWorkflow.php +++ b/src/workflow/ArcanistPatchWorkflow.php @@ -1,1146 +1,1146 @@ array( 'param' => 'revision_id', 'paramtype' => 'complete', 'help' => pht( "Apply changes from a Differential revision, using the most recent ". "diff that has been attached to it. You can run '%s' as a shorthand.", 'arc patch D12345'), ), 'diff' => array( 'param' => 'diff_id', 'help' => pht( 'Apply changes from a Differential diff. Normally you want to use '. '%s to get the most recent changes, but you can specifically apply '. 'an out-of-date diff or a diff which was never attached to a '. 'revision by using this flag.', '--revision'), ), 'arcbundle' => array( 'param' => 'bundlefile', 'paramtype' => 'file', 'help' => pht( "Apply changes from an arc bundle generated with '%s'.", 'arc export'), ), 'patch' => array( 'param' => 'patchfile', 'paramtype' => 'file', 'help' => pht( 'Apply changes from a git patchfile or unified patchfile.'), ), 'encoding' => array( 'param' => 'encoding', 'help' => pht( 'Attempt to convert non UTF-8 patch into specified encoding.'), ), 'update' => array( 'supports' => array('git', 'svn', 'hg'), 'help' => pht( 'Update the local working copy before applying the patch.'), 'conflicts' => array( 'nobranch' => true, 'bookmark' => true, ), ), 'nocommit' => array( 'supports' => array('git', 'hg'), 'help' => pht( 'Normally under git/hg, if the patch is successful, the changes '. 'are committed to the working copy. This flag prevents the commit.'), ), 'skip-dependencies' => array( 'supports' => array('git', 'hg'), 'help' => pht( 'Normally, if a patch has dependencies that are not present in the '. 'working copy, arc tries to apply them as well. This flag prevents '. 'such work.'), ), 'nobranch' => array( 'supports' => array('git', 'hg'), 'help' => pht( 'Normally, a new branch (git) or bookmark (hg) is created and then '. 'the patch is applied and committed in the new branch/bookmark. '. 'This flag cherry-picks the resultant commit onto the original '. 'branch and deletes the temporary branch.'), 'conflicts' => array( 'update' => true, ), ), 'force' => array( 'help' => pht('Do not run any sanity checks.'), ), '*' => 'name', ); } protected function didParseArguments() { $arguments = array( 'revision' => self::SOURCE_REVISION, 'diff' => self::SOURCE_DIFF, 'arcbundle' => self::SOURCE_BUNDLE, 'patch' => self::SOURCE_PATCH, 'name' => self::SOURCE_REVISION, ); $sources = array(); foreach ($arguments as $key => $source_type) { $value = $this->getArgument($key); if (!$value) { continue; } switch ($key) { case 'revision': $value = $this->normalizeRevisionID($value); break; case 'name': if (count($value) > 1) { throw new ArcanistUsageException( pht('Specify at most one revision name.')); } $value = $this->normalizeRevisionID(head($value)); break; } $sources[] = array( $source_type, $value, ); } if (!$sources) { throw new ArcanistUsageException( pht( 'You must specify changes to apply to the working copy with '. '"D12345", "--revision", "--diff", "--arcbundle", or "--patch".')); } if (count($sources) > 1) { throw new ArcanistUsageException( pht( 'Options "D12345", "--revision", "--diff", "--arcbundle" and '. '"--patch" are mutually exclusive. Choose exactly one patch '. 'source.')); } $source = head($sources); $this->source = $source[0]; $this->sourceParam = $source[1]; } public function requiresConduit() { return ($this->getSource() != self::SOURCE_PATCH); } public function requiresRepositoryAPI() { return true; } public function requiresWorkingCopy() { return true; } private function getSource() { return $this->source; } private function getSourceParam() { return $this->sourceParam; } private function shouldCommit() { return !$this->getArgument('nocommit', false); } private function canBranch() { $repository_api = $this->getRepositoryAPI(); return ($repository_api instanceof ArcanistGitAPI) || ($repository_api instanceof ArcanistMercurialAPI); } private function shouldBranch() { $no_branch = $this->getArgument('nobranch', false); if ($no_branch) { return false; } return true; } private function getBranchName(ArcanistBundle $bundle) { $branch_name = null; $repository_api = $this->getRepositoryAPI(); $revision_id = $bundle->getRevisionID(); $base_name = 'arcpatch'; if ($revision_id) { $base_name .= "-D{$revision_id}"; } $suffixes = array(null, '_1', '_2', '_3'); foreach ($suffixes as $suffix) { $proposed_name = $base_name.$suffix; list($err) = $repository_api->execManualLocal( 'rev-parse --verify %s', $proposed_name); // no error means git rev-parse found a branch if (!$err) { echo phutil_console_format( "%s\n", pht( 'Branch name %s already exists; trying a new name.', $proposed_name)); continue; } else { $branch_name = $proposed_name; break; } } if (!$branch_name) { throw new Exception( pht( 'Arc was unable to automagically make a name for this patch. '. 'Please clean up your working copy and try again.')); } return $branch_name; } private function getBookmarkName(ArcanistBundle $bundle) { $bookmark_name = null; $repository_api = $this->getRepositoryAPI(); $revision_id = $bundle->getRevisionID(); $base_name = 'arcpatch'; if ($revision_id) { $base_name .= "-D{$revision_id}"; } $suffixes = array(null, '-1', '-2', '-3'); foreach ($suffixes as $suffix) { $proposed_name = $base_name.$suffix; list($err) = $repository_api->execManualLocal( 'log -r %s', hgsprintf('%s', $proposed_name)); // no error means hg log found a bookmark if (!$err) { echo phutil_console_format( "%s\n", pht( 'Bookmark name %s already exists; trying a new name.', $proposed_name)); continue; } else { $bookmark_name = $proposed_name; break; } } if (!$bookmark_name) { throw new Exception( pht( 'Arc was unable to automagically make a name for this patch. '. 'Please clean up your working copy and try again.')); } return $bookmark_name; } private function createBranch(ArcanistBundle $bundle, $has_base_revision) { $repository_api = $this->getRepositoryAPI(); if ($repository_api instanceof ArcanistGitAPI) { $branch_name = $this->getBranchName($bundle); $base_revision = $bundle->getBaseRevision(); if ($base_revision && $has_base_revision) { $base_revision = $repository_api->getCanonicalRevisionName( $base_revision); $repository_api->execxLocal( 'checkout -b %s %s', $branch_name, $base_revision); } else { $repository_api->execxLocal('checkout -b %s', $branch_name); } // Synchronize submodule state, since the checkout may have modified // submodule references. See PHI1083. // Note that newer versions of "git checkout" include a // "--recurse-submodules" flag which accomplishes this goal a little // more simply. For now, use the more compatible form. $repository_api->execPassthru('submodule update --init --recursive'); echo phutil_console_format( "%s\n", pht( 'Created and checked out branch %s.', $branch_name)); } else if ($repository_api instanceof ArcanistMercurialAPI) { $branch_name = $this->getBookmarkName($bundle); $base_revision = $bundle->getBaseRevision(); if ($base_revision && $has_base_revision) { $base_revision = $repository_api->getCanonicalRevisionName( $base_revision); echo pht("Updating to the revision's base commit")."\n"; $repository_api->execPassthru('update %s', $base_revision); } $repository_api->execxLocal('bookmark %s', $branch_name); echo phutil_console_format( "%s\n", pht( 'Created and checked out bookmark %s.', $branch_name)); } return $branch_name; } private function shouldApplyDependencies() { return !$this->getArgument('skip-dependencies', false); } private function shouldUpdateWorkingCopy() { return $this->getArgument('update', false); } private function updateWorkingCopy() { echo pht('Updating working copy...')."\n"; $this->getRepositoryAPI()->updateWorkingCopy(); echo pht('Done.')."\n"; } public function run() { $source = $this->getSource(); $param = $this->getSourceParam(); try { switch ($source) { case self::SOURCE_PATCH: if ($param == '-') { $patch = @file_get_contents('php://stdin'); if (!strlen($patch)) { throw new ArcanistUsageException( pht('Failed to read patch from stdin!')); } } else { $patch = Filesystem::readFile($param); } $bundle = ArcanistBundle::newFromDiff($patch); break; case self::SOURCE_BUNDLE: $path = $this->getArgument('arcbundle'); $bundle = ArcanistBundle::newFromArcBundle($path); break; case self::SOURCE_REVISION: $bundle = $this->loadRevisionBundleFromConduit( $this->getConduit(), $param); break; case self::SOURCE_DIFF: $bundle = $this->loadDiffBundleFromConduit( $this->getConduit(), $param); break; } } catch (ConduitClientException $ex) { if ($ex->getErrorCode() == 'ERR-INVALID-SESSION') { // Phabricator is not configured to allow anonymous access to // Differential. $this->authenticateConduit(); return $this->run(); } else { throw $ex; } } $try_encoding = nonempty($this->getArgument('encoding'), null); if (!$try_encoding) { if ($this->requiresConduit()) { try { $try_encoding = $this->getRepositoryEncoding(); } catch (ConduitClientException $e) { $try_encoding = null; } } } if ($try_encoding) { $bundle->setEncoding($try_encoding); } $sanity_check = !$this->getArgument('force', false); // we should update the working copy before we do ANYTHING else to // the working copy if ($this->shouldUpdateWorkingCopy()) { $this->updateWorkingCopy(); } if ($sanity_check) { $this->requireCleanWorkingCopy(); } $repository_api = $this->getRepositoryAPI(); $has_base_revision = $repository_api->hasLocalCommit( $bundle->getBaseRevision()); if (!$has_base_revision) { if ($repository_api instanceof ArcanistGitAPI) { echo phutil_console_format( "** %s ** %s\n", pht('INFO'), pht('Base commit is not in local repository; trying to fetch.')); $repository_api->execManualLocal('fetch --quiet --all'); $has_base_revision = $repository_api->hasLocalCommit( $bundle->getBaseRevision()); } } if ($this->canBranch() && ($this->shouldBranch() || ($this->shouldCommit() && $has_base_revision))) { if ($repository_api instanceof ArcanistGitAPI) { $original_branch = $repository_api->getBranchName(); } else if ($repository_api instanceof ArcanistMercurialAPI) { $original_branch = $repository_api->getActiveBookmark(); } // If we weren't on a branch, then record the ref we'll return to // instead. if ($original_branch === null) { if ($repository_api instanceof ArcanistGitAPI) { $original_branch = $repository_api->getCanonicalRevisionName('HEAD'); } else if ($repository_api instanceof ArcanistMercurialAPI) { $original_branch = $repository_api->getCanonicalRevisionName('.'); } } $new_branch = $this->createBranch($bundle, $has_base_revision); } if (!$has_base_revision && $this->shouldApplyDependencies()) { $this->applyDependencies($bundle); } if ($sanity_check) { $this->sanityCheck($bundle); } if ($repository_api instanceof ArcanistSubversionAPI) { $patch_err = 0; $copies = array(); $deletes = array(); $patches = array(); $propset = array(); $adds = array(); $symlinks = array(); $changes = $bundle->getChanges(); foreach ($changes as $change) { $type = $change->getType(); $should_patch = true; $filetype = $change->getFileType(); switch ($filetype) { case ArcanistDiffChangeType::FILE_SYMLINK: $should_patch = false; $symlinks[] = $change; break; } switch ($type) { case ArcanistDiffChangeType::TYPE_MOVE_AWAY: case ArcanistDiffChangeType::TYPE_MULTICOPY: case ArcanistDiffChangeType::TYPE_DELETE: $path = $change->getCurrentPath(); $fpath = $repository_api->getPath($path); if (!@file_exists($fpath)) { $ok = phutil_console_confirm( pht( "Patch deletes file '%s', but the file does not exist in ". "the working copy. Continue anyway?", $path)); if (!$ok) { throw new ArcanistUserAbortException(); } } else { $deletes[] = $change->getCurrentPath(); } $should_patch = false; break; case ArcanistDiffChangeType::TYPE_COPY_HERE: case ArcanistDiffChangeType::TYPE_MOVE_HERE: $path = $change->getOldPath(); $fpath = $repository_api->getPath($path); if (!@file_exists($fpath)) { $cpath = $change->getCurrentPath(); if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) { $verbs = pht('copies'); } else { $verbs = pht('moves'); } $ok = phutil_console_confirm( pht( "Patch %s '%s' to '%s', but source path does not exist ". "in the working copy. Continue anyway?", $verbs, $path, $cpath)); if (!$ok) { throw new ArcanistUserAbortException(); } } else { $copies[] = array( $change->getOldPath(), $change->getCurrentPath(), ); } break; case ArcanistDiffChangeType::TYPE_ADD: $adds[] = $change->getCurrentPath(); break; } if ($should_patch) { $cbundle = ArcanistBundle::newFromChanges(array($change)); $patches[$change->getCurrentPath()] = $cbundle->toUnifiedDiff(); $prop_old = $change->getOldProperties(); $prop_new = $change->getNewProperties(); $props = $prop_old + $prop_new; foreach ($props as $key => $ignored) { if (idx($prop_old, $key) !== idx($prop_new, $key)) { $propset[$change->getCurrentPath()][$key] = idx($prop_new, $key); } } } } // Before we start doing anything, create all the directories we're going // to add files to if they don't already exist. foreach ($copies as $copy) { list($src, $dst) = $copy; $this->createParentDirectoryOf($dst); } foreach ($patches as $path => $patch) { $this->createParentDirectoryOf($path); } foreach ($adds as $add) { $this->createParentDirectoryOf($add); } // TODO: The SVN patch workflow likely does not work on windows because // of the (cd ...) stuff. foreach ($copies as $copy) { list($src, $dst) = $copy; passthru( csprintf( '(cd %s; svn cp %s %s)', $repository_api->getPath(), ArcanistSubversionAPI::escapeFileNameForSVN($src), ArcanistSubversionAPI::escapeFileNameForSVN($dst))); } foreach ($deletes as $delete) { passthru( csprintf( '(cd %s; svn rm %s)', $repository_api->getPath(), ArcanistSubversionAPI::escapeFileNameForSVN($delete))); } foreach ($symlinks as $symlink) { $link_target = $symlink->getSymlinkTarget(); $link_path = $symlink->getCurrentPath(); switch ($symlink->getType()) { case ArcanistDiffChangeType::TYPE_ADD: case ArcanistDiffChangeType::TYPE_CHANGE: case ArcanistDiffChangeType::TYPE_MOVE_HERE: case ArcanistDiffChangeType::TYPE_COPY_HERE: execx( '(cd %s && ln -sf %s %s)', $repository_api->getPath(), $link_target, $link_path); break; } } foreach ($patches as $path => $patch) { $err = null; if ($patch) { $tmp = new TempFile(); Filesystem::writeFile($tmp, $patch); passthru( csprintf( '(cd %s; patch -p0 < %s)', $repository_api->getPath(), $tmp), $err); } else { passthru( csprintf( '(cd %s; touch %s)', $repository_api->getPath(), $path), $err); } if ($err) { $patch_err = max($patch_err, $err); } } foreach ($adds as $add) { passthru( csprintf( '(cd %s; svn add %s)', $repository_api->getPath(), ArcanistSubversionAPI::escapeFileNameForSVN($add))); } foreach ($propset as $path => $changes) { foreach ($changes as $prop => $value) { if ($prop == 'unix:filemode') { // Setting this property also changes the file mode. $prop = 'svn:executable'; $value = (octdec($value) & 0111 ? 'on' : null); } if ($value === null) { passthru( csprintf( '(cd %s; svn propdel %s %s)', $repository_api->getPath(), $prop, ArcanistSubversionAPI::escapeFileNameForSVN($path))); } else { passthru( csprintf( '(cd %s; svn propset %s %s %s)', $repository_api->getPath(), $prop, $value, ArcanistSubversionAPI::escapeFileNameForSVN($path))); } } } if ($patch_err == 0) { echo phutil_console_format( "** %s ** %s\n", pht('OKAY'), pht('Successfully applied patch to the working copy.')); } else { echo phutil_console_format( "\n\n** %s ** %s\n", pht('WARNING'), pht( "Some hunks could not be applied cleanly by the unix '%s' ". "utility. Your working copy may be different from the revision's ". "base, or you may be in the wrong subdirectory. You can export ". "the raw patch file using '%s', and then try to apply it by ". "fiddling with options to '%s' (particularly, %s), or manually. ". "The output above, from '%s', may be helpful in ". "figuring out what went wrong.", 'patch', 'arc export --unified', 'patch', '-p', 'patch')); } return $patch_err; } else if ($repository_api instanceof ArcanistGitAPI) { $patchfile = new TempFile(); Filesystem::writeFile($patchfile, $bundle->toGitPatch()); $passthru = new PhutilExecPassthru( 'git apply --whitespace nowarn --index --reject -- %s', $patchfile); $passthru->setCWD($repository_api->getPath()); $err = $passthru->resolve(); if ($err) { echo phutil_console_format( "\n** %s **\n", pht('Patch Failed!')); // NOTE: Git patches may fail if they change the case of a filename // (for instance, from 'example.c' to 'Example.c'). As of now, Git // can not apply these patches on case-insensitive filesystems and // there is no way to build a patch which works. throw new ArcanistUsageException(pht('Unable to apply patch!')); } // See PHI1083 and PHI648. If the patch applied changes to submodules, // it only updates the submodule pointer, not the actual submodule. We're // left with the pointer update staged in the index, and the unmodified // submodule on disk. // If we then "git commit --all" or "git add --all", the unmodified // submodule on disk is added to the index as a change, which effectively // undoes the patch we just applied and reverts the submodule back to // the previous state. // To avoid this, do a submodule update before we continue. // We could also possibly skip the "--all" flag so we don't have to do // this submodule update, but we want to leave the working copy in a // clean state anyway, so we're going to have to do an update at some // point. This usually doesn't cost us anything. $repository_api->execPassthru('submodule update --init --recursive'); if ($this->shouldCommit()) { $flags = array(); if ($bundle->getFullAuthor()) { $flags[] = csprintf('--author=%s', $bundle->getFullAuthor()); } $commit_message = $this->getCommitMessage($bundle); $future = $repository_api->execFutureLocal( 'commit -a %Ls -F - --no-verify', $flags); $future->write($commit_message); $future->resolvex(); $this->writeOkay( pht('COMMITTED'), pht('Successfully committed patch.')); } else { $this->writeOkay( pht('APPLIED'), pht('Successfully applied patch.')); } if ($this->canBranch() && !$this->shouldBranch() && $this->shouldCommit() && $has_base_revision) { // See PHI1083 and PHI648. Synchronize submodule state after mutating // the working copy. $repository_api->execxLocal('checkout %s --', $original_branch); $repository_api->execPassthru('submodule update --init --recursive'); $ex = null; try { $repository_api->execxLocal('cherry-pick -- %s', $new_branch); $repository_api->execPassthru('submodule update --init --recursive'); } catch (Exception $ex) { // do nothing } $repository_api->execxLocal('branch -D -- %s', $new_branch); if ($ex) { echo phutil_console_format( "\n** %s**\n", pht('Cherry Pick Failed!')); throw $ex; } } } else if ($repository_api instanceof ArcanistMercurialAPI) { $future = $repository_api->execFutureLocal('import --no-commit -'); $future->write($bundle->toGitPatch()); try { $future->resolvex(); } catch (CommandException $ex) { echo phutil_console_format( "\n** %s **\n", pht('Patch Failed!')); $stderr = $ex->getStderr(); if (preg_match('/case-folding collision/', $stderr)) { echo phutil_console_wrap( phutil_console_format( "\n** %s ** %s\n", pht('WARNING'), pht( "This patch may have failed because it attempts to change ". "the case of a filename (for instance, from '%s' to '%s'). ". "Mercurial cannot apply patches like this on case-insensitive ". "filesystems. You must apply this patch manually.", 'example.c', 'Example.c'))); } throw $ex; } if ($this->shouldCommit()) { $author = coalesce($bundle->getFullAuthor(), $bundle->getAuthorName()); if ($author !== null) { $author_cmd = csprintf('-u %s', $author); } else { $author_cmd = ''; } $commit_message = $this->getCommitMessage($bundle); $future = $repository_api->execFutureLocal( 'commit %C -l -', $author_cmd); $future->write($commit_message); $future->resolvex(); if (!$this->shouldBranch() && $has_base_revision) { $original_rev = $repository_api->getCanonicalRevisionName( $original_branch); $current_parent = $repository_api->getCanonicalRevisionName( hgsprintf('%s^', $new_branch)); $err = 0; if ($original_rev != $current_parent) { list($err) = $repository_api->execManualLocal( 'rebase --dest %s --rev %s', hgsprintf('%s', $original_branch), hgsprintf('%s', $new_branch)); } $repository_api->execxLocal('bookmark --delete %s', $new_branch); if ($err) { $repository_api->execManualLocal('rebase --abort'); throw new ArcanistUsageException( phutil_console_format( "\n** %s**\n", pht('Rebase onto %s failed!', $original_branch))); } } $verb = pht('committed'); } else { $verb = pht('applied'); } echo phutil_console_format( "** %s ** %s\n", pht('OKAY'), pht('Successfully %s patch.', $verb)); } else { throw new Exception(pht('Unknown version control system.')); } return 0; } private function getCommitMessage(ArcanistBundle $bundle) { $revision_id = $bundle->getRevisionID(); $commit_message = null; $prompt_message = null; // if we have a revision id the commit message is in differential // TODO: See T848 for the authenticated stuff. if ($revision_id && $this->isConduitAuthenticated()) { $conduit = $this->getConduit(); $commit_message = $conduit->callMethodSynchronous( 'differential.getcommitmessage', array( 'revision_id' => $revision_id, )); $prompt_message = pht( - ' Note arcanist failed to load the commit message '. - 'from differential for revision %s.', + ' NOTE: Failed to load the commit message from Differential (for '. + 'revision "%s".)', "D{$revision_id}"); } // no revision id or failed to fetch commit message so get it from the // user on the command line if (!$commit_message) { $template = sprintf( "\n\n# %s%s\n", pht( 'Enter a commit message for this patch. If you just want to apply '. 'the patch to the working copy without committing, re-run arc patch '. 'with the %s flag.', '--nocommit'), $prompt_message); $commit_message = $this->newInteractiveEditor($template) ->setName('arcanist-patch-commit-message') ->setTaskMessage(pht( 'Supply a commit message for this patch, then save and exit.')) ->editInteractively(); $commit_message = ArcanistCommentRemover::removeComments($commit_message); if (!strlen(trim($commit_message))) { throw new ArcanistUserAbortException(); } } return $commit_message; } protected function getShellCompletions(array $argv) { // TODO: Pull open diffs from 'arc list'? return array('ARGUMENT'); } private function applyDependencies(ArcanistBundle $bundle) { // check for (and automagically apply on the user's be-hest) any revisions // this patch depends on $graph = $this->buildDependencyGraph($bundle); if ($graph) { $start_phid = $graph->getStartPHID(); $cycle_phids = $graph->detectCycles($start_phid); if ($cycle_phids) { $phids = array_keys($graph->getNodes()); $issue = pht( 'The dependencies for this patch have a cycle. Applying them '. 'is not guaranteed to work. Continue anyway?'); $okay = phutil_console_confirm($issue, true); } else { $phids = $graph->getNodesInTopologicalOrder(); $phids = array_reverse($phids); $okay = true; } if (!$okay) { return; } $dep_on_revs = $this->getConduit()->callMethodSynchronous( 'differential.query', array( 'phids' => $phids, )); $revs = array(); foreach ($dep_on_revs as $dep_on_rev) { $revs[$dep_on_rev['phid']] = 'D'.$dep_on_rev['id']; } // order them in case we got a topological sort earlier $revs = array_select_keys($revs, $phids); if (!empty($revs)) { $base_args = array( '--force', '--skip-dependencies', '--nobranch', ); if (!$this->shouldCommit()) { $base_args[] = '--nocommit'; } foreach ($revs as $phid => $diff_id) { // we'll apply this, the actual patch, later // this should be the last in the list if ($phid == $start_phid) { continue; } $args = $base_args; $args[] = $diff_id; $apply_workflow = $this->buildChildWorkflow( 'patch', $args); $apply_workflow->run(); } } } } /** * Do the best we can to prevent PEBKAC and id10t issues. */ private function sanityCheck(ArcanistBundle $bundle) { $repository_api = $this->getRepositoryAPI(); // Check to see if the bundle's base revision matches the working copy // base revision if ($repository_api->supportsLocalCommits()) { $bundle_base_rev = $bundle->getBaseRevision(); if (empty($bundle_base_rev)) { // this means $source is SOURCE_PATCH || SOURCE_BUNDLE w/ $version < 2 // they don't have a base rev so just do nothing $commit_exists = true; } else { $commit_exists = $repository_api->hasLocalCommit($bundle_base_rev); } if (!$commit_exists) { // we have a problem...! lots of work because we need to ask // differential for revision information for these base revisions // to improve our error message. $bundle_base_rev_str = null; $source_base_rev = $repository_api->getWorkingCopyRevision(); $source_base_rev_str = null; if ($repository_api instanceof ArcanistGitAPI) { $hash_type = ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT; } else if ($repository_api instanceof ArcanistMercurialAPI) { $hash_type = ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT; } else { $hash_type = null; } if ($hash_type) { // 2 round trips because even though we could send off one query // we wouldn't be able to tell which revisions were for which hash $hash = array($hash_type, $bundle_base_rev); $bundle_revision = $this->loadRevisionFromHash($hash); $hash = array($hash_type, $source_base_rev); $source_revision = $this->loadRevisionFromHash($hash); if ($bundle_revision) { $bundle_base_rev_str = $bundle_base_rev. ' \ D'.$bundle_revision['id']; } if ($source_revision) { $source_base_rev_str = $source_base_rev. ' \ D'.$source_revision['id']; } } $bundle_base_rev_str = nonempty( $bundle_base_rev_str, $bundle_base_rev); $source_base_rev_str = nonempty( $source_base_rev_str, $source_base_rev); $ok = phutil_console_confirm( pht( 'This diff is against commit %s, but the commit is nowhere '. 'in the working copy. Try to apply it against the current '. 'working copy state? (%s)', $bundle_base_rev_str, $source_base_rev_str), $default_no = false); if (!$ok) { throw new ArcanistUserAbortException(); } } } } /** * Create parent directories one at a time, since we need to "svn add" each * one. (Technically we could "svn add" just the topmost new directory.) */ private function createParentDirectoryOf($path) { $repository_api = $this->getRepositoryAPI(); $dir = dirname($path); if (Filesystem::pathExists($dir)) { return; } else { // Make sure the parent directory exists before we make this one. $this->createParentDirectoryOf($dir); execx( '(cd %s && mkdir %s)', $repository_api->getPath(), $dir); passthru( csprintf( '(cd %s && svn add %s)', $repository_api->getPath(), $dir)); } } private function loadRevisionFromHash($hash) { // TODO -- de-hack this as permissions become more clear with things // like T848 (add scope to OAuth) if (!$this->isConduitAuthenticated()) { return null; } $conduit = $this->getConduit(); $revisions = $conduit->callMethodSynchronous( 'differential.query', array( 'commitHashes' => array($hash), )); // grab the latest closed revision only $found_revision = null; $revisions = isort($revisions, 'dateModified'); foreach ($revisions as $revision) { if ($revision['status'] == ArcanistDifferentialRevisionStatus::CLOSED) { $found_revision = $revision; } } return $found_revision; } private function buildDependencyGraph(ArcanistBundle $bundle) { $graph = null; if ($this->getRepositoryAPI() instanceof ArcanistSubversionAPI) { return $graph; } $revision_id = $bundle->getRevisionID(); if ($revision_id) { $revisions = $this->getConduit()->callMethodSynchronous( 'differential.query', array( 'ids' => array($revision_id), )); if ($revisions) { $revision = head($revisions); $rev_auxiliary = idx($revision, 'auxiliary', array()); $phids = idx($rev_auxiliary, 'phabricator:depends-on', array()); if ($phids) { $revision_phid = $revision['phid']; $graph = id(new ArcanistDifferentialDependencyGraph()) ->setConduit($this->getConduit()) ->setRepositoryAPI($this->getRepositoryAPI()) ->setStartPHID($revision_phid) ->addNodes(array($revision_phid => $phids)) ->loadGraph(); } } } return $graph; } } diff --git a/src/workflow/ArcanistUpgradeWorkflow.php b/src/workflow/ArcanistUpgradeWorkflow.php index 2013a201..14f45af9 100644 --- a/src/workflow/ArcanistUpgradeWorkflow.php +++ b/src/workflow/ArcanistUpgradeWorkflow.php @@ -1,133 +1,133 @@ newWorkflowInformation() ->setSynopsis(pht('Upgrade this program to the latest version.')) ->addExample(pht('**upgrade**')) ->setHelp($help); } public function getWorkflowArguments() { return array(); } public function runWorkflow() { $log = $this->getLogEngine(); $roots = array( 'arcanist' => dirname(phutil_get_library_root('arcanist')), ); $supported_branches = array( 'master', 'stable', ); $supported_branches = array_fuse($supported_branches); foreach ($roots as $library => $root) { $log->writeStatus( pht('PREPARING'), pht( 'Preparing to upgrade "%s"...', $library)); $working_copy = ArcanistWorkingCopy::newFromWorkingDirectory($root); $repository_api = $working_copy->getRepositoryAPI(); $is_git = ($repository_api instanceof ArcanistGitAPI); if (!$is_git) { throw new PhutilArgumentUsageException( pht( - 'The "arc upgrade" workflow uses "git pull" to upgrade, but '. - 'the "arcanist/" directory (in "%s") is not a Git working '. - 'copy. You must leave "arcanist/" as a Git working copy to '. + 'The "upgrade" workflow uses "git pull" to upgrade, but '. + 'the software directory (in "%s") is not a Git working '. + 'copy. You must leave this directory as a Git working copy to '. 'use "arc upgrade".', $root)); } // NOTE: Don't use requireCleanWorkingCopy() here because it tries to // amend changes and generally move the workflow forward. We just want to // abort if there are local changes and make the user sort things out. $uncommitted = $repository_api->getUncommittedStatus(); if ($uncommitted) { $message = pht( 'You have uncommitted changes in the working copy ("%s") for this '. 'library ("%s"):', $root, $library); $list = id(new PhutilConsoleList()) ->setWrap(false) ->addItems(array_keys($uncommitted)); id(new PhutilConsoleBlock()) ->addParagraph($message) ->addList($list) ->addParagraph( pht( 'Discard these changes before running "arc upgrade".')) ->draw(); throw new PhutilArgumentUsageException( pht('"arc upgrade" can only upgrade clean working copies.')); } $branch_name = $repository_api->getBranchName(); if (!isset($supported_branches[$branch_name])) { throw new PhutilArgumentUsageException( pht( 'Library "%s" (in "%s") is on branch "%s", but this branch is '. 'not supported for automatic upgrades. Supported branches are: '. '%s.', $library, $root, $branch_name, implode(', ', array_keys($supported_branches)))); } $log->writeStatus( pht('UPGRADING'), pht( 'Upgrading "%s" (on branch "%s").', $library, $branch_name)); $command = csprintf( 'git pull --rebase origin -- %R', $branch_name); $future = (new PhutilExecPassthru($command)) ->setCWD($root); try { $this->newCommand($future) ->execute(); } catch (Exception $ex) { // If we failed, try to go back to the old state, then throw the // original exception. exec_manual('git rebase --abort'); throw $ex; } } $log->writeSuccess( pht('UPGRADED'), pht('This software is now up to date.')); return 0; } }