1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-21 22:32:41 +01:00

Merge Phacility/master into phorge

This commit is contained in:
Aviv Eyal 2022-07-25 11:39:47 -07:00
commit 9b4bcc8349
102 changed files with 3619 additions and 2353 deletions

File diff suppressed because it is too large Load diff

View file

@ -68,7 +68,10 @@ $base_args->parsePartial(
array(
'name' => 'conduit-uri',
'param' => 'uri',
'help' => pht('Connect to Phabricator install specified by __uri__.'),
'help' => pht(
'Connect to the %s (or compatible software) server specified by '.
'__uri__.',
PlatformSymbols::getPlatformServerName()),
),
array(
'name' => 'conduit-token',
@ -85,7 +88,8 @@ $base_args->parsePartial(
'repeat' => true,
'help' => pht(
'Specify a runtime configuration value. This will take precedence '.
'over static values, and only affect the current arcanist invocation.'),
'over static values, and only affect the current process: the '.
'setting is not saved anywhere.'),
),
));
@ -310,9 +314,13 @@ try {
$message = phutil_console_format(
"%s\n\n - %s\n - %s\n - %s\n",
pht(
'This command requires arc to connect to a Phabricator install, '.
'but no Phabricator installation is configured. To configure a '.
'Phabricator URI:'),
'This command requires %s to connect to a %s (or compatible '.
'software) server, but no %s server is configured. To configure a '.
'%s server URI:',
PlatformSymbols::getPlatformClientName(),
PlatformSymbols::getPlatformServerName(),
PlatformSymbols::getPlatformServerName(),
PlatformSymbols::getPlatformServerName()),
pht(
'set a default location with `%s`; or',
'arc set-config default <uri>'),
@ -688,10 +696,12 @@ function arcanist_load_libraries(
"**<bg:yellow> %s </bg>** %s\n",
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 %s (at path "%s") against '.
'another copy of %s (at path "%s"). Code in the current '.
'working directory will not be loaded or executed.',
PlatformSymbols::getPlatformClientName(),
$executing_directory,
PlatformSymbols::getPlatformClientName(),
$working_directory)));
}
}

View file

@ -188,6 +188,8 @@ phutil_register_library_map(array(
'ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase.php',
'ArcanistDynamicDefineXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDynamicDefineXHPASTLinterRule.php',
'ArcanistDynamicDefineXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDynamicDefineXHPASTLinterRuleTestCase.php',
'ArcanistEachUseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistEachUseXHPASTLinterRule.php',
'ArcanistEachUseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistEachUseXHPASTLinterRuleTestCase.php',
'ArcanistElseIfUsageXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistElseIfUsageXHPASTLinterRule.php',
'ArcanistElseIfUsageXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistElseIfUsageXHPASTLinterRuleTestCase.php',
'ArcanistEmptyFileXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistEmptyFileXHPASTLinterRule.php',
@ -264,6 +266,8 @@ phutil_register_library_map(array(
'ArcanistHgProxyClient' => 'hgdaemon/ArcanistHgProxyClient.php',
'ArcanistHgProxyServer' => 'hgdaemon/ArcanistHgProxyServer.php',
'ArcanistHgServerChannel' => 'hgdaemon/ArcanistHgServerChannel.php',
'ArcanistHostMemorySnapshot' => 'filesystem/memory/ArcanistHostMemorySnapshot.php',
'ArcanistHostMemorySnapshotTestCase' => 'filesystem/memory/__tests__/ArcanistHostMemorySnapshotTestCase.php',
'ArcanistImplicitConstructorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplicitConstructorXHPASTLinterRule.php',
'ArcanistImplicitConstructorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistImplicitConstructorXHPASTLinterRuleTestCase.php',
'ArcanistImplicitFallthroughXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplicitFallthroughXHPASTLinterRule.php',
@ -346,6 +350,7 @@ phutil_register_library_map(array(
'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php',
'ArcanistMercurialCommitGraphQuery' => 'repository/graph/query/ArcanistMercurialCommitGraphQuery.php',
'ArcanistMercurialCommitMessageHardpointQuery' => 'query/ArcanistMercurialCommitMessageHardpointQuery.php',
'ArcanistMercurialCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistMercurialCommitSymbolCommitHardpointQuery.php',
'ArcanistMercurialLandEngine' => 'land/engine/ArcanistMercurialLandEngine.php',
'ArcanistMercurialLocalState' => 'repository/state/ArcanistMercurialLocalState.php',
'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php',
@ -419,6 +424,7 @@ phutil_register_library_map(array(
'ArcanistPhutilXHPASTLinterStandard' => 'lint/linter/standards/phutil/ArcanistPhutilXHPASTLinterStandard.php',
'ArcanistPlusOperatorOnStringsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPlusOperatorOnStringsXHPASTLinterRule.php',
'ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase.php',
'ArcanistProductNameLiteralXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistProductNameLiteralXHPASTLinterRule.php',
'ArcanistProjectConfigurationSource' => 'config/source/ArcanistProjectConfigurationSource.php',
'ArcanistPrompt' => 'toolset/ArcanistPrompt.php',
'ArcanistPromptResponse' => 'toolset/ArcanistPromptResponse.php',
@ -632,6 +638,8 @@ phutil_register_library_map(array(
'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
'MethodCallFuture' => 'future/MethodCallFuture.php',
'MethodCallFutureTestCase' => 'future/__tests__/MethodCallFutureTestCase.php',
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php',
'PhageAction' => 'phage/action/PhageAction.php',
@ -907,6 +915,7 @@ phutil_register_library_map(array(
'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php',
'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php',
'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php',
'PlatformSymbols' => 'platform/PlatformSymbols.php',
'PytestTestEngine' => 'unit/engine/PytestTestEngine.php',
'TempFile' => 'filesystem/TempFile.php',
'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
@ -959,6 +968,7 @@ phutil_register_library_map(array(
'nonempty' => 'utils/utils.php',
'phlog' => 'error/phlog.php',
'pht' => 'internationalization/pht.php',
'pht_list' => 'internationalization/pht.php',
'phutil_build_http_querystring' => 'utils/utils.php',
'phutil_build_http_querystring_from_pairs' => 'utils/utils.php',
'phutil_censor_credentials' => 'utils/utils.php',
@ -972,7 +982,6 @@ phutil_register_library_map(array(
'phutil_count' => 'internationalization/pht.php',
'phutil_date_format' => 'utils/viewutils.php',
'phutil_decode_mime_header' => 'utils/utils.php',
'phutil_deprecated' => 'init/lib/moduleutils.php',
'phutil_describe_type' => 'utils/utils.php',
'phutil_encode_log' => 'utils/utils.php',
'phutil_error_listener_example' => 'error/phlog.php',
@ -1008,6 +1017,9 @@ phutil_register_library_map(array(
'phutil_load_library' => 'init/lib/moduleutils.php',
'phutil_loggable_string' => 'utils/utils.php',
'phutil_microseconds_since' => 'utils/utils.php',
'phutil_nonempty_scalar' => 'utils/utils.php',
'phutil_nonempty_string' => 'utils/utils.php',
'phutil_nonempty_stringlike' => 'utils/utils.php',
'phutil_parse_bytes' => 'utils/viewutils.php',
'phutil_partition' => 'utils/utils.php',
'phutil_passthru' => 'future/exec/execx.php',
@ -1245,6 +1257,8 @@ phutil_register_library_map(array(
'ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistDynamicDefineXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistDynamicDefineXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistEachUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistEachUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistElseIfUsageXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistElseIfUsageXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistEmptyFileXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
@ -1321,6 +1335,8 @@ phutil_register_library_map(array(
'ArcanistHgProxyClient' => 'Phobject',
'ArcanistHgProxyServer' => 'Phobject',
'ArcanistHgServerChannel' => 'PhutilProtocolChannel',
'ArcanistHostMemorySnapshot' => 'Phobject',
'ArcanistHostMemorySnapshotTestCase' => 'PhutilTestCase',
'ArcanistImplicitConstructorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistImplicitConstructorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistImplicitFallthroughXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
@ -1403,6 +1419,7 @@ phutil_register_library_map(array(
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
'ArcanistMercurialCommitGraphQuery' => 'ArcanistCommitGraphQuery',
'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery',
'ArcanistMercurialCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery',
'ArcanistMercurialLandEngine' => 'ArcanistLandEngine',
'ArcanistMercurialLocalState' => 'ArcanistRepositoryLocalState',
'ArcanistMercurialParser' => 'Phobject',
@ -1476,6 +1493,7 @@ phutil_register_library_map(array(
'ArcanistPhutilXHPASTLinterStandard' => 'ArcanistLinterStandard',
'ArcanistPlusOperatorOnStringsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistProductNameLiteralXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistProjectConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource',
'ArcanistPrompt' => 'Phobject',
'ArcanistPromptResponse' => 'Phobject',
@ -1696,6 +1714,8 @@ phutil_register_library_map(array(
'LinesOfALargeFile' => 'LinesOfALarge',
'LinesOfALargeFileTestCase' => 'PhutilTestCase',
'MFilterTestHelper' => 'Phobject',
'MethodCallFuture' => 'Future',
'MethodCallFutureTestCase' => 'PhutilTestCase',
'NoseTestEngine' => 'ArcanistUnitTestEngine',
'PHPASTParserTestCase' => 'PhutilTestCase',
'PhageAction' => 'Phobject',
@ -1991,6 +2011,7 @@ phutil_register_library_map(array(
'PhutilVeryWowEnglishLocale' => 'PhutilLocale',
'PhutilWordPressFuture' => 'FutureProxy',
'PhutilXHPASTBinary' => 'Phobject',
'PlatformSymbols' => 'Phobject',
'PytestTestEngine' => 'ArcanistUnitTestEngine',
'TempFile' => 'Phobject',
'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',

View file

@ -125,8 +125,8 @@ class PhutilLibraryTestCase extends PhutilTestCase {
$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),

View file

@ -89,8 +89,8 @@ final class ArcanistConduitEngine
$block = id(new PhutilConsoleBlock())
->addParagraph(
pht(
'This command needs to communicate with Phabricator, but no '.
'Phabricator URI is configured.'))
'This command needs to communicate with a server, but no '.
'server URI is configured.'))
->addList($list);
throw new ArcanistUsageException($block->drawConsoleString());

View file

@ -71,10 +71,10 @@ final class ArcanistArcConfigurationEngineExtension
->setSummary(pht('Repository for the current working copy.'))
->setHelp(
pht(
'Associate the working copy with a specific Phabricator '.
'repository. Normally, Arcanist 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.'))
'Associate the working copy with a specific repository. Normally, '.
'this association can be determined automatically, but if your '.
'setup is unusual you can use this option to tell it what the '.
'desired value is.'))
->setExamples(
array(
'libexample',
@ -89,14 +89,15 @@ final class ArcanistArcConfigurationEngineExtension
'conduit_uri',
'default',
))
->setSummary(pht('Phabricator install to connect to.'))
->setSummary(pht('Server to connect to.'))
->setHelp(
pht(
'Associates this working copy with a specific installation of '.
'Phabricator.'))
'%s (or compatible software).',
PlatformSymbols::getPlatformServerName()))
->setExamples(
array(
'https://phabricator.mycompany.com/',
'https://devtools.example.com/',
)),
id(new ArcanistAliasesConfigOption())
->setKey(self::KEY_ALIASES)

View file

@ -15,7 +15,7 @@ final class ArcanistBlindlyTrustHTTPEngineExtension
}
public function getExtensionName() {
return pht('Arcanist HTTPS Trusted Domains');
return pht('HTTPS Trusted Domains');
}
public function shouldTrustAnySSLAuthorityForURI(PhutilURI $uri) {

View file

@ -258,7 +258,7 @@ final class ArcanistConfigurationManager extends Phobject {
}
public function getUserConfigurationFileLocation() {
if (strlen($this->customArcrcFilename)) {
if ($this->customArcrcFilename !== null) {
return $this->customArcrcFilename;
}

View file

@ -7,11 +7,11 @@ final class ArcanistSettings extends Phobject {
'default' => 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',
@ -35,7 +35,7 @@ final class ArcanistSettings extends Phobject {
'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.',
@ -44,10 +44,9 @@ final class ArcanistSettings extends Phobject {
'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',
@ -96,8 +95,8 @@ final class ArcanistSettings extends Phobject {
'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',
),
@ -118,7 +117,7 @@ final class ArcanistSettings extends Phobject {
'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,

View file

@ -22,23 +22,38 @@ final class PhutilConsoleFormatter extends Phobject {
public static function getDisableANSI() {
if (self::$disableANSI === null) {
self::$disableANSI = self::newShouldDisableAnsi();
}
return self::$disableANSI;
}
private static function newShouldDisableANSI() {
$term = phutil_utf8_strtolower(getenv('TERM'));
// ansicon enables ANSI support on Windows
if (!$term && getenv('ANSICON')) {
$term = 'ansi';
}
if (phutil_is_windows() && $term !== 'cygwin' && $term !== 'ansi') {
self::$disableANSI = true;
} else if (!defined('STDOUT')) {
self::$disableANSI = true;
} else if (function_exists('posix_isatty') && !posix_isatty(STDOUT)) {
self::$disableANSI = true;
} else {
self::$disableANSI = false;
if (phutil_is_windows()) {
if ($term !== 'cygwin' && $term !== 'ansi') {
return true;
}
}
return self::$disableANSI;
$stdout = PhutilSystem::getStdoutHandle();
if ($stdout === null) {
return true;
}
if (function_exists('posix_isatty')) {
if (!posix_isatty($stdout)) {
return true;
}
}
return false;
}
public static function formatString($format /* ... */) {

View file

@ -22,6 +22,7 @@ final class PhutilInteractiveEditor extends Phobject {
private $offset = 0;
private $preferred;
private $fallback;
private $taskMessage;
/* -( Creating a New Editor )---------------------------------------------- */
@ -74,6 +75,20 @@ final class PhutilInteractiveEditor extends Phobject {
$editor = $this->getEditor();
$offset = $this->getLineOffset();
$binary = basename($editor);
// This message is primarily an assistance to users with GUI-based
// editors configured. Users with terminal-based editors won't have a
// chance to see this prior to the editor being launched.
echo tsprintf(
"%s\n",
pht('Launching editor "%s"...', $binary));
$task_message = $this->getTaskMessage();
if ($task_message !== null) {
echo tsprintf("%s\n", $task_message);
}
$err = $this->invokeEditor($editor, $path, $offset);
if ($err) {
@ -85,7 +100,6 @@ final class PhutilInteractiveEditor extends Phobject {
'vim' => true,
);
$binary = basename($editor);
if (isset($vi_binaries[$binary])) {
// This runs "Q" (an invalid command), then "q" (a valid command,
// meaning "quit"). Vim binaries with behavior that makes them poor
@ -266,6 +280,33 @@ final class PhutilInteractiveEditor extends Phobject {
return $this;
}
/**
* Set the message that identifies the task for which the editor is being
* launched, displayed to the user prior to it being launched.
*
* @param string The message to display to the user.
* @return $this
*
* @task config
*/
public function setTaskMessage($task_message) {
$this->taskMessage = $task_message;
return $this;
}
/**
* Retrieve the current message that will display to the user just prior to
* invoking the editor.
*
* @return string The message that will display to the user, or null if no
* message will be displayed.
*
* @task config
*/
public function getTaskMessage() {
return $this->taskMessage;
}
/**
* Get the name of the editor program to use. The value of the environmental

View file

@ -139,10 +139,10 @@ final class ArcanistDifferentialCommitMessage extends Phobject {
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));
}

View file

@ -181,7 +181,6 @@ final class PhutilErrorHandler extends Phobject {
* @task internal
*/
public static function handleError($num, $str, $file, $line, $ctx = null) {
foreach (self::$traps as $trap) {
$trap->addError($num, $str, $file, $line);
}
@ -378,7 +377,7 @@ final class PhutilErrorHandler extends Phobject {
* @task internal
*/
public static function dispatchErrorMessage($event, $value, $metadata) {
$timestamp = strftime('%Y-%m-%d %H:%M:%S');
$timestamp = date('Y-m-d H:i:s');
switch ($event) {
case self::ERROR:
@ -425,16 +424,6 @@ final class PhutilErrorHandler extends Phobject {
$metadata['file'],
$metadata['line']);
$metadata['default_message'] = $default_message;
error_log($default_message);
break;
case self::DEPRECATED:
$default_message = sprintf(
'[%s] DEPRECATED: %s is deprecated; %s',
$timestamp,
$value,
$metadata['why']);
$metadata['default_message'] = $default_message;
error_log($default_message);
break;

View file

@ -28,6 +28,8 @@ final class PhutilDirectoryFixture extends Phobject {
}
public function getPath($to_file = null) {
$to_file = phutil_string_cast($to_file);
return $this->path.'/'.ltrim($to_file, '/');
}

View file

@ -94,7 +94,7 @@ final class PhutilErrorLog
if (strlen($message)) {
$message = tsprintf("%B\n", $message);
@fwrite(STDERR, $message);
PhutilSystem::writeStderr($message);
}
}

View file

@ -7,6 +7,8 @@ final class PhutilMercurialBinaryAnalyzer
const CAPABILITY_FILES = 'files';
const CAPABILITY_INJECTION = 'injection';
const CAPABILITY_TEMPLATE_PNODE = 'template_pnode';
const CAPABILTIY_ANNOTATE_TEMPLATES = 'annotate_templates';
protected function newBinaryVersion() {
$future = id(new ExecFuture('hg --version --quiet'))
@ -60,6 +62,33 @@ final class PhutilMercurialBinaryAnalyzer
self::CAPABILITY_INJECTION);
}
/**
* When using `--template` the format for accessing individual parents
* changed from `{p1node}` to `{p1.node}` in Mercurial 4.9.
*
* @return boolean True if the version of Mercurial is new enough to support
* the `{p1.node}` format in templates, or false if otherwise.
*/
public function isMercurialTemplatePnodeAvailable() {
return self::versionHasCapability(
$this->requireBinaryVersion(),
self::CAPABILITY_TEMPLATE_PNODE);
}
/**
* The `hg annotate` command did not accept the `--template` argument until
* version 4.6. It appears to function in version 4.5 however it's not
* documented and wasn't announced until the 4.6 release.
*
* @return boolean True if the version of Mercurial is new enough to support
* the `--template` option when using `hg annotate`, or false if otherwise.
*/
public function isMercurialAnnotateTemplatesAvailable() {
return self::versionHasCapability(
$this->requireBinaryVersion(),
self::CAPABILTIY_ANNOTATE_TEMPLATES);
}
public static function versionHasCapability(
$mercurial_version,
@ -70,6 +99,10 @@ final class PhutilMercurialBinaryAnalyzer
return version_compare($mercurial_version, '3.2', '>=');
case self::CAPABILITY_INJECTION:
return version_compare($mercurial_version, '3.2.4', '<');
case self::CAPABILITY_TEMPLATE_PNODE:
return version_compare($mercurial_version, '4.9', '>=');
case self::CAPABILTIY_ANNOTATE_TEMPLATES:
return version_compare($mercurial_version, '4.6', '>=');
default:
throw new Exception(
pht(

View file

@ -212,7 +212,7 @@ abstract class LinesOfALarge extends Phobject implements Iterator {
if (strlen($this->buf)) {
$this->num++;
$this->line = $this->buf;
$this->buf = null;
$this->buf = '';
} else {
$this->valid = false;
}

View file

@ -0,0 +1,156 @@
<?php
final class ArcanistHostMemorySnapshot
extends Phobject {
private $memorySnapshot;
public static function newFromRawMeminfo($meminfo_source, $meminfo_raw) {
$snapshot = new self();
$snapshot->memorySnapshot = $snapshot->readMeminfoSnapshot(
$meminfo_source,
$meminfo_raw);
return $snapshot;
}
public function getTotalSwapBytes() {
$info = $this->getMemorySnapshot();
return $info['swap.total'];
}
private function getMemorySnapshot() {
if ($this->memorySnapshot === null) {
$this->memorySnapshot = $this->newMemorySnapshot();
}
return $this->memorySnapshot;
}
private function newMemorySnapshot() {
$meminfo_source = '/proc/meminfo';
list($meminfo_raw) = execx('cat %s', $meminfo_source);
return $this->readMeminfoSnapshot($meminfo_source, $meminfo_raw);
}
private function readMeminfoSnapshot($meminfo_source, $meminfo_raw) {
$meminfo_pattern = '/^([^:]+):\s+(\S+)(?:\s+(kB))?\z/';
$meminfo_map = array();
$meminfo_lines = phutil_split_lines($meminfo_raw, false);
foreach ($meminfo_lines as $meminfo_line) {
$meminfo_parts = phutil_preg_match($meminfo_pattern, $meminfo_line);
if (!$meminfo_parts) {
throw new Exception(
pht(
'Unable to parse line in meminfo source "%s": "%s".',
$meminfo_source,
$meminfo_line));
}
$meminfo_key = $meminfo_parts[1];
$meminfo_value = $meminfo_parts[2];
$meminfo_unit = idx($meminfo_parts, 3);
if (isset($meminfo_map[$meminfo_key])) {
throw new Exception(
pht(
'Encountered duplicate meminfo key "%s" in meminfo source "%s".',
$meminfo_key,
$meminfo_source));
}
$meminfo_map[$meminfo_key] = array(
'value' => $meminfo_value,
'unit' => $meminfo_unit,
);
}
$swap_total_bytes = $this->readMeminfoBytes(
$meminfo_source,
$meminfo_map,
'SwapTotal');
return array(
'swap.total' => $swap_total_bytes,
);
}
private function readMeminfoBytes(
$meminfo_source,
$meminfo_map,
$meminfo_key) {
$meminfo_integer = $this->readMeminfoIntegerValue(
$meminfo_source,
$meminfo_map,
$meminfo_key);
$meminfo_unit = $meminfo_map[$meminfo_key]['unit'];
if ($meminfo_unit === null) {
throw new Exception(
pht(
'Expected to find a byte unit for meminfo key "%s" in meminfo '.
'source "%s", found no unit.',
$meminfo_key,
$meminfo_source));
}
if ($meminfo_unit !== 'kB') {
throw new Exception(
pht(
'Expected unit for meminfo key "%s" in meminfo source "%s" '.
'to be "kB", found "%s".',
$meminfo_key,
$meminfo_source,
$meminfo_unit));
}
$meminfo_bytes = ($meminfo_integer * 1024);
return $meminfo_bytes;
}
private function readMeminfoIntegerValue(
$meminfo_source,
$meminfo_map,
$meminfo_key) {
$meminfo_value = $this->readMeminfoValue(
$meminfo_source,
$meminfo_map,
$meminfo_key);
if (!phutil_preg_match('/^\d+\z/', $meminfo_value)) {
throw new Exception(
pht(
'Expected to find an integer value for meminfo key "%s" in '.
'meminfo source "%s", found "%s".',
$meminfo_key,
$meminfo_source,
$meminfo_value));
}
return (int)$meminfo_value;
}
private function readMeminfoValue(
$meminfo_source,
$meminfo_map,
$meminfo_key) {
if (!isset($meminfo_map[$meminfo_key])) {
throw new Exception(
pht(
'Expected to find meminfo key "%s" in meminfo source "%s".',
$meminfo_key,
$meminfo_source));
}
return $meminfo_map[$meminfo_key]['value'];
}
}

View file

@ -0,0 +1,51 @@
<?php
final class ArcanistHostMemorySnapshotTestCase
extends PhutilTestCase {
public function testSnapshotSwapTotalBytes() {
$test_cases = array(
'meminfo_swap_normal.txt' => 4294963200,
'meminfo_swap_zero.txt' => 0,
'meminfo_swap_missing.txt' => false,
'meminfo_swap_invalid.txt' => false,
'meminfo_swap_badunits.txt' => false,
'meminfo_swap_duplicate.txt' => false,
);
$test_dir = dirname(__FILE__).'/data/';
foreach ($test_cases as $test_file => $expect) {
$test_data = Filesystem::readFile($test_dir.$test_file);
$caught = null;
$actual = null;
try {
$snapshot = ArcanistHostMemorySnapshot::newFromRawMeminfo(
$test_file,
$test_data);
$actual = $snapshot->getTotalSwapBytes();
} catch (Exception $ex) {
if ($expect === false) {
$caught = $ex;
} else {
throw $ex;
}
} catch (Throwable $ex) {
throw $ex;
}
if ($expect === false) {
$this->assertTrue(
($caught instanceof Exception),
pht('Expected exception for "%s".', $test_file));
} else {
$this->assertEqual(
$expect,
$actual,
pht('Result for "%s".', $test_file));
}
}
}
}

View file

@ -0,0 +1,2 @@
MemTotal: 8140924 kB
SwapTotal: 4194 mB

View file

@ -0,0 +1,3 @@
MemTotal: 8140924 kB
SwapTotal: 4194300 kB
SwapTotal: 4194300 kB

View file

@ -0,0 +1,2 @@
MemTotal: 8140924 kB
SwapTotal: aardvark kB

View file

@ -0,0 +1 @@
MemTotal: 8140924 kB

View file

@ -0,0 +1,51 @@
MemTotal: 8140924 kB
MemFree: 1760456 kB
MemAvailable: 4264888 kB
Buffers: 36784 kB
Cached: 2654788 kB
SwapCached: 2132 kB
Active: 331128 kB
Inactive: 5677400 kB
Active(anon): 22692 kB
Inactive(anon): 3325560 kB
Active(file): 308436 kB
Inactive(file): 2351840 kB
Unevictable: 23012 kB
Mlocked: 18476 kB
SwapTotal: 4194300 kB
SwapFree: 2440444 kB
Dirty: 44 kB
Writeback: 0 kB
AnonPages: 3337944 kB
Mapped: 117424 kB
Shmem: 19872 kB
KReclaimable: 124728 kB
Slab: 240640 kB
SReclaimable: 124728 kB
SUnreclaim: 115912 kB
KernelStack: 6736 kB
PageTables: 42044 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 8264760 kB
Committed_AS: 6994880 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 16656 kB
VmallocChunk: 0 kB
Percpu: 9856 kB
HardwareCorrupted: 0 kB
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 270336 kB
DirectMap2M: 8118272 kB
DirectMap1G: 1048576 kB

View file

@ -0,0 +1,51 @@
MemTotal: 8140924 kB
MemFree: 1760456 kB
MemAvailable: 4264888 kB
Buffers: 36784 kB
Cached: 2654788 kB
SwapCached: 2132 kB
Active: 331128 kB
Inactive: 5677400 kB
Active(anon): 22692 kB
Inactive(anon): 3325560 kB
Active(file): 308436 kB
Inactive(file): 2351840 kB
Unevictable: 23012 kB
Mlocked: 18476 kB
SwapTotal: 0 kB
SwapFree: 2440444 kB
Dirty: 44 kB
Writeback: 0 kB
AnonPages: 3337944 kB
Mapped: 117424 kB
Shmem: 19872 kB
KReclaimable: 124728 kB
Slab: 240640 kB
SReclaimable: 124728 kB
SUnreclaim: 115912 kB
KernelStack: 6736 kB
PageTables: 42044 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 8264760 kB
Committed_AS: 6994880 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 16656 kB
VmallocChunk: 0 kB
Percpu: 9856 kB
HardwareCorrupted: 0 kB
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 270336 kB
DirectMap2M: 8118272 kB
DirectMap1G: 1048576 kB

View file

@ -0,0 +1,36 @@
<?php
/**
* Degenerate future which resolves by calling a method.
*
* $future = new MethodCallFuture($calculator, 'add', 1, 2);
*
* This future is similar to @{class:ImmediateFuture}, but may make it easier
* to implement exception behavior correctly. See T13666.
*/
final class MethodCallFuture extends Future {
private $callObject;
private $callMethod;
private $callArgv;
public function __construct($object, $method /* , ...*/ ) {
$argv = func_get_args();
$this->callObject = $object;
$this->callMethod = $method;
$this->callArgv = array_slice($argv, 2);
}
public function isReady() {
$call = array($this->callObject, $this->callMethod);
$argv = $this->callArgv;
$result = call_user_func_array($call, $argv);
$this->setResult($result);
return true;
}
}

View file

@ -0,0 +1,57 @@
<?php
final class MethodCallFutureTestCase extends PhutilTestCase {
public function testMethodCallFutureSums() {
$future = new MethodCallFuture($this, 'getSum', 1, 2, 3);
$result = $future->resolve();
$this->assertEqual(6, $result, pht('MethodCallFuture: getSum(1, 2, 3)'));
$future = new MethodCallFuture($this, 'getSum');
$result = $future->resolve();
$this->assertEqual(0, $result, pht('MethodCallFuture: getSum()'));
}
public function testMethodCallFutureExceptions() {
$future = new MethodCallFuture($this, 'raiseException');
// See T13666. Using "FutureIterator" to advance the future until it is
// ready to resolve should NOT throw an exception.
foreach (new FutureIterator(array($future)) as $resolvable) {
// Continue below...
}
$caught = null;
try {
$future->resolve();
} catch (PhutilMethodNotImplementedException $ex) {
$caught = $ex;
}
$this->assertTrue(
($caught instanceof PhutilMethodNotImplementedException),
pht('MethodCallFuture: exceptions raise at resolution.'));
}
public function getSum(/* ... */) {
$args = func_get_args();
$sum = 0;
foreach ($args as $arg) {
$sum += $arg;
}
return $sum;
}
public function raiseException() {
// We just want to throw any narrow exception so the test isn't catching
// too broad an exception type. This is simulating some exception during
// future resolution.
throw new PhutilMethodNotImplementedException();
}
}

View file

@ -49,4 +49,24 @@ final class PhutilAWSException extends Exception {
return $this->httpStatus;
}
public function isNotFoundError() {
if ($this->hasErrorCode('InvalidVolume.NotFound')) {
return true;
}
return false;
}
private function hasErrorCode($code) {
$errors = idx($this->params, 'Errors', array());
foreach ($errors as $error) {
if ($error[0] === $code) {
return true;
}
}
return false;
}
}

View file

@ -142,6 +142,7 @@ abstract class PhutilAWSFuture extends FutureProxy {
try {
$xml = @(new SimpleXMLElement($body));
} catch (Exception $ex) {
phlog($ex);
$xml = null;
}
@ -155,9 +156,28 @@ abstract class PhutilAWSFuture extends FutureProxy {
);
if ($xml) {
$params['RequestID'] = $xml->RequestID[0];
$errors = array($xml->Error);
foreach ($errors as $error) {
$params['Errors'][] = array($error->Code, $error->Message);
// NOTE: The S3 and EC2 APIs return slightly different error responses.
// In S3 responses, there's a simple top-level "<Error>" element.
$s3_error = $xml->Error;
if ($s3_error) {
$params['Errors'][] = array(
phutil_string_cast($s3_error->Code),
phutil_string_cast($s3_error->Message),
);
}
// In EC2 responses, there's an "<Errors>" element with "<Error>"
// children.
$ec2_errors = $xml->Errors[0];
if ($ec2_errors) {
foreach ($ec2_errors as $error) {
$params['Errors'][] = array(
phutil_string_cast($error->Code),
phutil_string_cast($error->Message),
);
}
}
}

View file

@ -192,14 +192,21 @@ final class ExecFuture extends PhutilExecutableFuture {
* @task interact
*/
public function read() {
$stdout = $this->readStdout();
$stdout_value = $this->readStdout();
$stderr = $this->stderr;
if ($stderr === null) {
$stderr_value = '';
} else {
$stderr_value = substr($stderr, $this->stderrPos);
}
$result = array(
$stdout,
(string)substr($this->stderr, $this->stderrPos),
$stdout_value,
$stderr_value,
);
$this->stderrPos = strlen($this->stderr);
$this->stderrPos = $this->getStderrBufferLength();
return $result;
}
@ -209,8 +216,16 @@ final class ExecFuture extends PhutilExecutableFuture {
$this->updateFuture(); // Sync
}
$result = (string)substr($this->stdout, $this->stdoutPos);
$this->stdoutPos = strlen($this->stdout);
$stdout = $this->stdout;
if ($stdout === null) {
$result = '';
} else {
$result = substr($stdout, $this->stdoutPos);
}
$this->stdoutPos = $this->getStdoutBufferLength();
return $result;
}
@ -475,7 +490,7 @@ final class ExecFuture extends PhutilExecutableFuture {
* @task internal
*/
public function isReadBufferEmpty() {
return !strlen($this->stdout);
return !$this->getStdoutBufferLength();
}
@ -757,14 +772,17 @@ final class ExecFuture extends PhutilExecutableFuture {
$max_stdout_read_bytes = PHP_INT_MAX;
$max_stderr_read_bytes = PHP_INT_MAX;
if ($read_buffer_size !== null) {
$max_stdout_read_bytes = $read_buffer_size - strlen($this->stdout);
$max_stderr_read_bytes = $read_buffer_size - strlen($this->stderr);
$stdout_len = $this->getStdoutBufferLength();
$stderr_len = $this->getStderrBufferLength();
$max_stdout_read_bytes = $read_buffer_size - $stdout_len;
$max_stderr_read_bytes = $read_buffer_size - $stderr_len;
}
if ($max_stdout_read_bytes > 0) {
$this->stdout .= $this->readAndDiscard(
$stdout,
$this->getStdoutSizeLimit() - strlen($this->stdout),
$this->getStdoutSizeLimit() - $this->getStdoutBufferLength(),
'stdout',
$max_stdout_read_bytes);
}
@ -772,7 +790,7 @@ final class ExecFuture extends PhutilExecutableFuture {
if ($max_stderr_read_bytes > 0) {
$this->stderr .= $this->readAndDiscard(
$stderr,
$this->getStderrSizeLimit() - strlen($this->stderr),
$this->getStderrSizeLimit() - $this->getStderrBufferLength(),
'stderr',
$max_stderr_read_bytes);
}
@ -1013,5 +1031,20 @@ final class ExecFuture extends PhutilExecutableFuture {
);
}
private function getStdoutBufferLength() {
if ($this->stdout === null) {
return 0;
}
return strlen($this->stdout);
}
private function getStderrBufferLength() {
if ($this->stderr === null) {
return 0;
}
return strlen($this->stderr);
}
}

View file

@ -10,8 +10,8 @@
* This is primarily useful for executing things like `$EDITOR` from command
* line scripts.
*
* $exec = new PhutilExecPassthru('ls %s', $dir);
* $err = $exec->execute();
* $exec = new PhutilExecPassthru('nano -- %s', $filename);
* $err = $exec->resolve();
*
* You can set the current working directory for the command with
* @{method:setCWD}, and set the environment with @{method:setEnv}.
@ -30,6 +30,14 @@ final class PhutilExecPassthru extends PhutilExecutableFuture {
/* -( Executing Passthru Commands )---------------------------------------- */
public function execute() {
phlog(
pht(
'The "execute()" method of "PhutilExecPassthru" is deprecated and '.
'calls should be replaced with "resolve()". See T13660.'));
return $this->resolve();
}
/**
* Execute this command.
*
@ -37,7 +45,7 @@ final class PhutilExecPassthru extends PhutilExecutableFuture {
*
* @task command
*/
public function execute() {
private function executeCommand() {
$command = $this->getCommand();
$is_write = ($this->stdinData !== null);
@ -45,10 +53,14 @@ final class PhutilExecPassthru extends PhutilExecutableFuture {
if ($is_write) {
$stdin_spec = array('pipe', 'r');
} else {
$stdin_spec = STDIN;
$stdin_spec = PhutilSystem::getStdinHandle();
}
$spec = array($stdin_spec, STDOUT, STDERR);
$spec = array(
$stdin_spec,
PhutilSystem::getStdoutHandle(),
PhutilSystem::getStderrHandle(),
);
$pipes = array();
$unmasked_command = $command->getUnmaskedString();
@ -116,7 +128,7 @@ final class PhutilExecPassthru extends PhutilExecutableFuture {
// make it easier to share code with ExecFuture.
if (!$this->hasResult()) {
$result = $this->execute();
$result = $this->executeCommand();
$this->setResult($result);
}

View file

@ -19,7 +19,7 @@ abstract class PhutilExecutableFuture extends Future {
pht(
'Command (of class "%s") was constructed with a '.
'"PhutilCommandString", but also passed arguments. '.
'When using a preprebuilt command, you must not pass '.
'When using a prebuilt command, you must not pass '.
'arguments.',
get_class($this)));
}

View file

@ -11,7 +11,7 @@ final class ExecPassthruTestCase extends PhutilTestCase {
$bin = $this->getSupportExecutable('exit');
$exec = new PhutilExecPassthru('php -f %R', $bin);
$err = $exec->execute();
$err = $exec->resolve();
$this->assertEqual(0, $err);
}

View file

@ -206,12 +206,14 @@ abstract class BaseHTTPFuture extends Future {
* @task config
*/
public function getHeaders($filter = null) {
$filter = strtolower($filter);
if ($filter !== null) {
$filter = phutil_utf8_strtolower($filter);
}
$result = array();
foreach ($this->headers as $header) {
list($name, $value) = $header;
if (!$filter || ($filter == strtolower($name))) {
if (($filter === null) || ($filter === phutil_utf8_strtolower($name))) {
$result[] = $header;
}
}

View file

@ -269,7 +269,7 @@ final class HTTPSFuture extends BaseHTTPFuture {
curl_setopt($curl, CURLOPT_REDIR_PROTOCOLS, $allowed_protocols);
}
if (strlen($this->rawBody)) {
if ($this->rawBody !== null) {
if ($this->getData()) {
throw new Exception(
pht(

View file

@ -36,18 +36,6 @@ function phutil_get_current_library_name() {
return phutil_get_library_name_for_root($root);
}
/**
* Warns about use of deprecated behavior.
*/
function phutil_deprecated($what, $why) {
PhutilErrorHandler::dispatchErrorMessage(
PhutilErrorHandler::DEPRECATED,
$what,
array(
'why' => $why,
));
}
function phutil_load_library($path) {
PhutilBootloader::getInstance()->loadLibrary($path);
}

View file

@ -44,3 +44,7 @@ function phutil_count($countable) {
function phutil_person(PhutilPerson $person) {
return $person;
}
function pht_list(array $items) {
return implode(', ', $items);
}

View file

@ -1407,6 +1407,21 @@ abstract class ArcanistLandEngine
ArcanistLandCommitSet $set,
$into_commit);
abstract protected function pushChange($into_commit);
/**
* Update all local refs that depend on refs selected-and-modified during the
* land. E.g. with branches named change1 -> change2 -> change3 and using
* `arc land change2`, in the general case the local change3 should be
* rebased onto the landed version of change2 so that it no longer has
* out-of-date ancestors.
*
* When multiple revisions are landed at once this will be called in a loop
* for each set, in order of max to min, where max is the latest descendant
* and min is the earliest ancestor. This is done so that non-landing commits
* that are descendants of the latest revision will only be rebased once.
*
* @param ArcanistLandCommitSet The current commit set to cascade.
*/
abstract protected function cascadeState(
ArcanistLandCommitSet $set,
$into_commit);
@ -1415,12 +1430,35 @@ abstract class ArcanistLandEngine
return ($this->getStrategy() === 'squash');
}
/**
* Prunes the given sets of commits. This should be called after the sets
* have been merged.
*
* @param array The list of ArcanistLandCommitSet to prune, in order of
* min to max commit set, where min is the earliest ancestor and max
* is the latest descendant.
*/
abstract protected function pruneBranches(array $sets);
/**
* Restore the local repository to an expected state after landing. This
* should only be called after all changes have been merged, pruned, and
* pushed.
*
* @param string The commit hash that was landed into.
* @param ArcanistRepositoryLocalState The local state that was captured
* at the beginning of the land process. This may include stashed changes.
*/
abstract protected function reconcileLocalState(
$into_commit,
ArcanistRepositoryLocalState $state);
/**
* Display information to the user about how to proceed since the land
* process was not fully completed. The merged branch has not been pushed.
*
* @param string The commit hash that was landed into.
*/
abstract protected function didHoldChanges($into_commit);
private function selectMergeStrategy() {

View file

@ -6,6 +6,8 @@ final class ArcanistMercurialLandEngine
private $ontoBranchMarker;
private $ontoMarkers;
private $rebasedActiveCommit;
protected function getDefaultSymbols() {
$api = $this->getRepositoryAPI();
$log = $this->getLogEngine();
@ -698,8 +700,14 @@ final class ArcanistMercurialLandEngine
$commit_map = array();
foreach ($symbols as $symbol) {
$symbol_commit = $symbol->getCommit();
$template = '{node}-{parents}-';
$template = '{node}-{parents % \'{node} \'}-{desc|firstline}\\n';
// The returned array of commits is expected to be ordered by max to min
// where the max commit has no descendants in the range and the min
// commit has no ancestors in the range. Use 'reverse()' in the template
// so the output is ordered with the max commit as the first line. The
// max/min terms are used in a topological sense as chronological terms
// for commits may be misleading or incorrect in some situations.
if ($into_commit === null) {
list($commits) = $api->execxLocal(
'log --rev %s --template %s --',
@ -789,8 +797,14 @@ final class ArcanistMercurialLandEngine
$commits = $set->getCommits();
$min_commit = last($commits)->getHash();
$max_commit = head($commits)->getHash();
// confirmCommits() reverses the order of the commits as they're ordered
// above in selectCommits(). Now the head of the list is the min commit and
// the last is the max commit, where within the range the max commit has no
// descendants and the min commit has no ancestors. The min/max terms are
// used in a topological sense as chronological terms for commits can be
// misleading or incorrect in certain situations.
$min_commit = head($commits)->getHash();
$max_commit = last($commits)->getHash();
$revision_ref = $set->getRevisionRef();
$commit_message = $revision_ref->getCommitMessage();
@ -820,13 +834,19 @@ final class ArcanistMercurialLandEngine
$argv[] = '--keep';
$argv[] = '--collapse';
$future = $api->execFutureLocal('rebase %Ls', $argv);
$future = $api->execFutureLocalWithExtension(
'rebase',
'rebase %Ls',
$argv);
$future->write($commit_message);
$future->resolvex();
} catch (CommandException $ex) {
// TODO
// $api->execManualLocal('rebase --abort');
// Aborting the rebase should restore the same state prior to running the
// rebase command.
$api->execManualLocalWithExtension(
'rebase',
'rebase --abort');
throw $ex;
}
@ -855,13 +875,21 @@ final class ArcanistMercurialLandEngine
list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}');
$new_cursor = trim($stdout);
// If any of the commits that were rebased was the active commit before the
// workflow started, track the new commit so it can be used as the working
// directory after the land has succeeded.
if (isset($obsolete_map[$this->getLocalState()->getLocalCommit()])) {
$this->rebasedActiveCommit = $new_cursor;
}
return $new_cursor;
}
protected function pushChange($into_commit) {
$api = $this->getRepositoryAPI();
list($head, $body, $tail) = $this->newPushCommands($into_commit);
list($head, $body, $tail_pass, $tail_fail) = $this->newPushCommands(
$into_commit);
foreach ($head as $command) {
$api->execxLocal('%Ls', $command);
@ -876,10 +904,20 @@ final class ArcanistMercurialLandEngine
'Push failed! Fix the error and run "arc land" again.'));
}
}
} finally {
foreach ($tail as $command) {
foreach ($tail_pass as $command) {
$api->execxLocal('%Ls', $command);
}
} catch (Exception $ex) {
foreach ($tail_fail as $command) {
$api->execxLocal('%Ls', $command);
}
throw $ex;
} catch (Throwable $ex) {
foreach ($tail_fail as $command) {
$api->execxLocal('%Ls', $command);
}
throw $ex;
}
}
@ -888,7 +926,8 @@ final class ArcanistMercurialLandEngine
$head_commands = array();
$body_commands = array();
$tail_commands = array();
$tail_pass_commands = array();
$tail_fail_commands = array();
$bookmarks = array();
foreach ($this->ontoMarkers as $onto_marker) {
@ -902,7 +941,7 @@ final class ArcanistMercurialLandEngine
// to the merge commit. (There doesn't seem to be any way to specify
// "push commit X as bookmark Y" in Mercurial.)
$restore = array();
$restore_bookmarks = array();
if ($bookmarks) {
$markers = $api->newMarkerRefQuery()
->withNames(mpull($bookmarks, 'getName'))
@ -934,7 +973,9 @@ final class ArcanistMercurialLandEngine
hgsprintf('%s', $new_position),
$bookmark_name);
$restore[$bookmark_name] = $old_position;
if ($old_position !== null) {
$restore_bookmarks[$bookmark_name] = $old_position;
}
}
}
@ -963,28 +1004,41 @@ final class ArcanistMercurialLandEngine
// Finally, restore the bookmarks.
foreach ($restore as $bookmark_name => $old_position) {
$tail = array();
$tail[] = 'bookmark';
if ($restore_bookmarks) {
// Instead of restoring the previous state, assume landing onto bookmarks
// also updates those bookmarks in the remote. After pushing, pull the
// latest state of these bookmarks. Mercurial allows pulling multiple
// bookmarks in a single pull command which will be faster than pulling
// them from a remote individually.
$tail = array(
'pull',
);
if ($old_position === null) {
$tail[] = '--delete';
} else {
$tail[] = '--force';
$tail[] = '--rev';
$tail[] = hgsprintf('%s', $api->getDisplayHash($old_position));
}
$tail[] = '--';
foreach ($restore_bookmarks as $bookmark_name => $old_position) {
$tail[] = '--bookmark';
$tail[] = $bookmark_name;
$tail_commands[] = $tail;
// In the failure case restore the state of the bookmark. Mercurial
// does not provide a way to move multiple bookmarks in a single
// command however these commands do not involve the remote.
$tail_fail_commands[] = array(
'bookmark',
'--force',
'--rev',
hgsprintf('%s', $api->getDisplayHash($old_position)),
);
}
if ($tail) {
$tail_pass_commands[] = $tail;
}
}
return array(
$head_commands,
$body_commands,
$tail_commands,
$tail_pass_commands,
$tail_fail_commands,
);
}
@ -1015,12 +1069,17 @@ final class ArcanistMercurialLandEngine
// be deleted, we can skip this rebase?
try {
$api->execxLocal(
$api->execxLocalWithExtension(
'rebase',
'rebase --source %s --dest %s --keep --keepbranches',
$child_hash,
$new_commit);
} catch (CommandException $ex) {
// TODO: Recover state.
// Aborting the rebase should restore the same state prior to running
// the rebase command.
$api->execManualLocalWithExtension(
'rebase',
'rebase --abort');
throw $ex;
}
}
@ -1040,6 +1099,8 @@ final class ArcanistMercurialLandEngine
$revs = array();
$obsolete_map = array();
$using_evolve = $api->getMercurialFeature('evolve');
// We've rebased all descendants already, so we can safely delete all
// of these commits.
@ -1047,10 +1108,22 @@ final class ArcanistMercurialLandEngine
foreach ($sets as $set) {
$commits = $set->getCommits();
// In the commit set the min commit should be the commit with no
// ancestors and the max commit should be the commit with no descendants.
// The min/max terms are used in a toplogical sense as chronological
// terms for commits may be misleading or incorrect in some situations.
$min_commit = head($commits)->getHash();
$max_commit = last($commits)->getHash();
if ($using_evolve) {
// If non-head series of commits are rebased while the evolve extension
// is in use, the rebase leaves behind the entire series of descendants
// in which case the entire chain needs removed, not just a section.
// Otherwise this results in the prune leaving behind orphaned commits.
$revs[] = hgsprintf('%s::', $min_commit);
} else {
$revs[] = hgsprintf('%s::%s', $min_commit, $max_commit);
}
foreach ($commits as $commit) {
$obsolete_map[$commit->getHash()] = true;
@ -1071,10 +1144,7 @@ final class ArcanistMercurialLandEngine
// bookmarks which point at these now-obsoleted commits.
$bookmark_refs = $api->newMarkerRefQuery()
->withMarkerTypes(
array(
ArcanistMarkerRef::TYPE_BOOKMARK,
))
->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK))
->execute();
foreach ($bookmark_refs as $bookmark_ref) {
$bookmark_hash = $bookmark_ref->getCommitHash();
@ -1093,13 +1163,14 @@ final class ArcanistMercurialLandEngine
$bookmark_name);
}
if ($api->getMercurialFeature('evolve')) {
if ($using_evolve) {
$api->execxLocal(
'prune --rev %s',
$rev_set);
} else {
$api->execxLocal(
'--config extensions.strip= strip --rev %s',
$api->execxLocalWithExtension(
'strip',
'strip --rev %s',
$rev_set);
}
}
@ -1108,7 +1179,54 @@ final class ArcanistMercurialLandEngine
$into_commit,
ArcanistRepositoryLocalState $state) {
// TODO: For now, just leave users wherever they ended up.
$api = $this->getRepositoryAPI();
// If the starting working state was not part of land process just update
// to that original working state.
if ($this->rebasedActiveCommit === null) {
$update_marker = $this->getLocalState()->getLocalCommit();
if ($this->getLocalState()->getLocalBookmark() !== null) {
$update_marker = $this->getLocalState()->getLocalBookmark();
}
$api->execxLocal(
'update -- %s',
$update_marker);
$state->discardLocalState();
return;
}
// If the working state was landed into multiple destinations then the
// resulting working state is ambiguous.
if (count($this->ontoMarkers) != 1) {
$state->discardLocalState();
return;
}
// Get the current state of bookmarks
$bookmark_refs = $api->newMarkerRefQuery()
->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK))
->execute();
$update_marker = $this->rebasedActiveCommit;
// Find any bookmarks which exist on the commit which is the result of the
// starting working directory's rebase. If any of those bookmarks are also
// the destination marker then we use that bookmark as the update in order
// for it to become active.
$onto_marker = $this->ontoMarkers[0]->getName();
foreach ($bookmark_refs as $bookmark_ref) {
if ($bookmark_ref->getCommitHash() == $this->rebasedActiveCommit &&
$bookmark_ref->getName() == $onto_marker) {
$update_marker = $onto_marker;
break;
}
}
$api->execxLocal(
'update -- %s',
$update_marker);
$state->discardLocalState();
}
@ -1120,8 +1238,9 @@ final class ArcanistMercurialLandEngine
$message = pht(
'Holding changes locally, they have not been pushed.');
list($head, $body, $tail) = $this->newPushCommands($into_commit);
$commands = array_merge($head, $body, $tail);
list($head, $body, $tail_pass, $tail_fail) = $this->newPushCommands(
$into_commit);
$commands = array_merge($head, $body, $tail_pass);
echo tsprintf(
"\n%!\n%s\n\n",

View file

@ -64,11 +64,11 @@ final class ArcanistCSharpLinter extends ArcanistLinter {
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'));
}
}
@ -132,8 +132,8 @@ final class ArcanistCSharpLinter extends ArcanistLinter {
} 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'));
}

View file

@ -304,9 +304,9 @@ final class ArcanistPhutilLibraryLinter extends ArcanistLinter {
$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".
@ -324,6 +324,8 @@ final class ArcanistPhutilLibraryLinter extends ArcanistLinter {
" - 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(

View file

@ -27,7 +27,11 @@ final class ArcanistXMLLinter extends ArcanistLinter {
}
public function getCacheVersion() {
if (defined('LIBXML_VERSION')) {
return LIBXML_VERSION;
} else {
return 'unavailable';
}
}
public function lintPath($path) {

View file

@ -51,6 +51,13 @@ abstract class ArcanistLinterTestCase extends PhutilTestCase {
private function lintFile($file, ArcanistLinter $linter) {
$linter = clone $linter;
if (!$linter->canRun()) {
$this->assertSkipped(
pht(
'Linter "%s" can not run.',
get_class($linter)));
}
$contents = Filesystem::readFile($file);
$contents = preg_split('/^~{4,}\n/m', $contents);
if (count($contents) < 2) {
@ -283,9 +290,12 @@ abstract class ArcanistLinterTestCase extends PhutilTestCase {
}
private function compareTransform($expected, $actual) {
$expected = phutil_string_cast($expected);
if (!strlen($expected)) {
return;
}
$this->assertEqual(
$expected,
$actual,

View file

@ -17,6 +17,14 @@ final class ArcanistCommentStyleXHPASTLinterRule
continue;
}
// Don't warn about PHP comment directives. In particular, we need
// to use "#[\ReturnTypeWillChange]" to implement "Iterator" in a way
// that is compatible with PHP 8.1 and older versions of PHP prior
// to the introduction of return types. See T13588.
if (preg_match('/^#\\[\\\\/', $value)) {
continue;
}
$this->raiseLintAtOffset(
$comment->getOffset(),
pht(

View file

@ -0,0 +1,24 @@
<?php
final class ArcanistEachUseXHPASTLinterRule
extends ArcanistXHPASTLinterRule {
const ID = 133;
public function getLintName() {
return pht('Use of Removed Function "each()"');
}
public function process(XHPASTNode $root) {
$calls = $this->getFunctionCalls($root, array('each'));
foreach ($calls as $call) {
$this->raiseLintAtNode(
$call,
pht(
'Do not use "each()". This function was deprecated in PHP 7.2 '.
'and removed in PHP 8.0'));
}
}
}

View file

@ -155,7 +155,9 @@ final class ArcanistPHPCompatibilityXHPASTLinterRule
if ($this->windowsVersion) {
$windows = idx($compat_info['functions_windows'], $name);
if ($windows === false) {
if ($windows === null) {
// This function has no special Windows considerations.
} else if ($windows === false) {
$this->raiseLintAtNode(
$node,
pht(

View file

@ -55,7 +55,12 @@ final class ArcanistParentMemberReferenceXHPASTLinterRule
}
}
if (version_compare($this->version, '5.4.0', '>=') || !$in_closure) {
$version_target = $this->version;
if ($version_target === null) {
$version_target = phpversion();
}
if (version_compare($version_target, '5.4.0', '>=') || !$in_closure) {
$this->raiseLintAtNode(
$class_ref,
pht(

View file

@ -0,0 +1,67 @@
<?php
final class ArcanistProductNameLiteralXHPASTLinterRule
extends ArcanistXHPASTLinterRule {
const ID = 134;
public function getLintName() {
return pht('Use of Product Name Literal');
}
public function getLintSeverity() {
return ArcanistLintSeverity::SEVERITY_WARNING;
}
public function process(XHPASTNode $root) {
$calls = $root->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->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));
}
}
}

View file

@ -42,7 +42,12 @@ final class ArcanistSelfMemberReferenceXHPASTLinterRule
}
}
if (version_compare($this->version, '5.4.0', '>=') || !$in_closure) {
$version_target = $this->version;
if (!$version_target) {
$version_target = phpversion();
}
if (version_compare($version_target, '5.4.0', '>=') || !$in_closure) {
$this->raiseLintAtNode(
$class_ref,
pht(

View file

@ -0,0 +1,10 @@
<?php
final class ArcanistEachUseXHPASTLinterRuleTestCase
extends ArcanistXHPASTLinterRuleTestCase {
public function testLinter() {
$this->executeTestsInDirectory(dirname(__FILE__).'/each-use/');
}
}

View file

@ -0,0 +1,23 @@
<?php
class X implements Iterator {
#[\ReturnTypeWillChange]
public function reset() {
# See T13588 for PHP8.1 compatibility information.
}
}
~~~~~~~~~~
error:7:5:XHP18:Comment Style
~~~~~~~~~~
<?php
class X implements Iterator {
#[\ReturnTypeWillChange]
public function reset() {
// See T13588 for PHP8.1 compatibility information.
}
}

View file

@ -0,0 +1,6 @@
<?php
$a = range(1, 2);
each($a);
~~~~~~~~~~
error:4:1:XHP133

View file

@ -112,6 +112,8 @@ final class ArcanistConsoleLintRenderer extends ArcanistLintRenderer {
$message = $message->newTrimmedMessage();
$original = $message->getOriginalText();
$original = phutil_string_cast($original);
$replacement = $message->getReplacementText();
$line = $message->getLine();

View file

@ -19,7 +19,7 @@ final class ArcanistLogEngine
}
private function writeBytes($bytes) {
fprintf(STDERR, '%s', $bytes);
PhutilSystem::writeStderr($bytes);
return $this;
}

View file

@ -12,7 +12,6 @@
final class PhutilLibraryMapBuilder extends Phobject {
private $root;
private $quiet = true;
private $subprocessLimit = 8;
private $fileSymbolMap;
@ -38,19 +37,6 @@ final class PhutilLibraryMapBuilder extends Phobject {
$this->root = $root;
}
/**
* Control status output. Use `--quiet` to set this.
*
* @param bool If true, don't show status output.
* @return this
*
* @task map
*/
public function setQuiet($quiet) {
$this->quiet = $quiet;
return $this;
}
/**
* Control subprocess parallelism limit. Use `--limit` to set this.
*
@ -108,25 +94,9 @@ final class PhutilLibraryMapBuilder extends Phobject {
public function buildAndWriteMap() {
$library_map = $this->buildMap();
$this->log(pht('Writing map...'));
$this->writeLibraryMap($library_map);
}
/**
* Write a status message to the user, if not running in quiet mode.
*
* @param string Message to write.
* @return this
*
* @task map
*/
private function log($message) {
if (!$this->quiet) {
@fprintf(STDERR, "%s\n", $message);
}
return $this;
}
/* -( Path Management )---------------------------------------------------- */
@ -236,11 +206,7 @@ final class PhutilLibraryMapBuilder extends Phobject {
}
$json = json_encode($cache);
try {
Filesystem::writeFile($cache_file, $json);
} catch (FilesystemException $ex) {
$this->log(pht('Unable to save the cache!'));
}
}
/**
@ -251,7 +217,6 @@ final class PhutilLibraryMapBuilder extends Phobject {
* @task symbol
*/
public function dropSymbolCache() {
$this->log(pht('Dropping symbol cache...'));
Filesystem::remove($this->getPathForSymbolCache());
}
@ -451,14 +416,10 @@ EOPHP;
*/
private function analyzeLibrary() {
// Identify all the ".php" source files in the library.
$this->log(pht('Finding source files...'));
$source_map = $this->loadSourceFileMap();
$this->log(
pht('Found %s files.', new PhutilNumber(count($source_map))));
// Load the symbol cache with existing parsed symbols. This allows us
// to remap libraries quickly by analyzing only changed files.
$this->log(pht('Loading symbol cache...'));
$symbol_cache = $this->loadSymbolCache();
// If the XHPAST binary is not up-to-date, build it now. Otherwise,
@ -481,23 +442,12 @@ EOPHP;
}
$futures[$file] = $this->buildSymbolAnalysisFuture($file);
}
$this->log(
pht('Found %s files in cache.', new PhutilNumber(count($symbol_map))));
// Run the analyzer on any files which need analysis.
if ($futures) {
$limit = $this->subprocessLimit;
$this->log(
pht(
'Analyzing %s file(s) with %s subprocess(es)...',
phutil_count($futures),
new PhutilNumber($limit)));
$progress = new PhutilConsoleProgressBar();
if ($this->quiet) {
$progress->setQuiet(true);
}
$progress->setTotal(count($futures));
$futures = id(new FutureIterator($futures))
@ -525,8 +475,6 @@ EOPHP;
$this->writeSymbolCache($symbol_map, $source_map);
// Our map is up to date, so either show it on stdout or write it to disk.
$this->log(pht('Building library map...'));
$this->librarySymbolMap = $this->buildLibraryMap($symbol_map);
}

View file

@ -33,22 +33,27 @@ abstract class Phobject implements Iterator {
get_class($this).'::'.$name));
}
#[\ReturnTypeWillChange]
public function current() {
$this->throwOnAttemptedIteration();
}
#[\ReturnTypeWillChange]
public function key() {
$this->throwOnAttemptedIteration();
}
#[\ReturnTypeWillChange]
public function next() {
$this->throwOnAttemptedIteration();
}
#[\ReturnTypeWillChange]
public function rewind() {
$this->throwOnAttemptedIteration();
}
#[\ReturnTypeWillChange]
public function valid() {
$this->throwOnAttemptedIteration();
}

View file

@ -33,7 +33,7 @@ final class ArcanistBaseCommitParser extends Phobject {
private function log($message) {
if ($this->verbose) {
fwrite(STDERR, $message."\n");
PhutilSystem::writeStderr($message."\n");
}
}

View file

@ -639,8 +639,7 @@ final class ArcanistBundle extends Phobject {
$old_path = $change->getOldPath();
$type = $change->getType();
if (!strlen($old_path) ||
$type == ArcanistDiffChangeType::TYPE_ADD) {
if ($old_path === '' || $type == ArcanistDiffChangeType::TYPE_ADD) {
$old_path = null;
}
@ -1023,7 +1022,7 @@ final class ArcanistBundle extends Phobject {
if ($is_64bit) {
for ($count = 4; $count >= 0; $count--) {
$val = $accum % 85;
$accum = $accum / 85;
$accum = (int)($accum / 85);
$slice .= $map[$val];
}
} else {

View file

@ -217,7 +217,7 @@ final class ArcanistDiffParser extends Phobject {
$line = $this->nextLine();
}
if (strlen($message)) {
if ($message !== null && strlen($message)) {
// If we found a message during pre-parse steps, add it to the resulting
// changes here.
$change = $this->buildChange(null)
@ -582,12 +582,15 @@ final class ArcanistDiffParser extends Phobject {
$ok = false;
$match = null;
if ($line !== null) {
foreach ($patterns as $pattern) {
$ok = preg_match('@^'.$pattern.'@', $line, $match);
if ($ok) {
break;
}
}
}
if (!$ok) {
if ($line === null ||
@ -774,7 +777,7 @@ final class ArcanistDiffParser extends Phobject {
$this->nextLine();
$this->parseGitBinaryPatch();
$line = $this->getLine();
if (preg_match('/^literal/', $line)) {
if ($line !== null && preg_match('/^literal/', $line)) {
// We may have old/new binaries (change) or just a new binary (hg add).
// If there are two blocks, parse both.
$this->parseGitBinaryPatch();
@ -920,11 +923,11 @@ final class ArcanistDiffParser extends Phobject {
$hunk->setNewOffset($matches[3]);
// Cover for the cases where length wasn't present (implying one line).
$old_len = idx($matches, 2);
$old_len = idx($matches, 2, '');
if (!strlen($old_len)) {
$old_len = 1;
}
$new_len = idx($matches, 4);
$new_len = idx($matches, 4, '');
if (!strlen($new_len)) {
$new_len = 1;
}
@ -1041,7 +1044,7 @@ final class ArcanistDiffParser extends Phobject {
$line = $this->nextNonemptyLine();
}
} while (preg_match('/^@@ /', $line));
} while (($line !== null) && preg_match('/^@@ /', $line));
}
protected function buildChange($path = null) {

View file

@ -85,7 +85,7 @@ final class PhutilBugtraqParser extends Phobject {
$captured_text = $capture['text'];
$captured_offset = $capture['at'];
if (strlen($select_regexp)) {
if ($select_regexp !== null) {
$selections = null;
preg_match_all(
$select_regexp,

View file

@ -13,6 +13,7 @@ final class PhutilEmailAddress extends Phobject {
private $domainName;
public function __construct($email_address = null) {
$email_address = phutil_string_cast($email_address);
$email_address = trim($email_address);
$matches = null;
@ -41,12 +42,13 @@ final class PhutilEmailAddress extends Phobject {
public function __toString() {
$address = $this->getAddress();
if (strlen($this->displayName)) {
if (phutil_nonempty_string($this->displayName)) {
$display_name = $this->encodeDisplayName($this->displayName);
return $display_name.' <'.$address.'>';
} else {
return $address;
}
return $address;
}
public function setDisplayName($display_name) {
@ -89,7 +91,7 @@ final class PhutilEmailAddress extends Phobject {
public function getAddress() {
$address = $this->localPart;
if (strlen($this->domainName)) {
if ($this->domainName !== null && strlen($this->domainName)) {
$address .= '@'.$this->domainName;
}
return $address;

View file

@ -154,9 +154,9 @@ final class PhutilURI extends Phobject {
$user = $this->user;
$pass = $this->pass;
if (strlen($user) && strlen($pass)) {
if (phutil_nonempty_string($user) && phutil_nonempty_string($pass)) {
$auth = rawurlencode($user).':'.rawurlencode($pass).'@';
} else if (strlen($user)) {
} else if (phutil_nonempty_string($user)) {
$auth = rawurlencode($user).'@';
} else {
$auth = null;
@ -166,19 +166,24 @@ final class PhutilURI extends Phobject {
if ($this->isGitURI()) {
$protocol = null;
} else {
if (strlen($auth)) {
if ($auth !== null) {
$protocol = nonempty($this->protocol, 'http');
}
}
if (strlen($protocol) || strlen($auth) || strlen($domain)) {
$has_protocol = ($protocol !== null) && strlen($protocol);
$has_auth = ($auth !== null);
$has_domain = ($domain !== null) && strlen($domain);
$has_port = ($port !== null) && strlen($port);
if ($has_protocol || $has_auth || $has_domain) {
if ($this->isGitURI()) {
$prefix = "{$auth}{$domain}";
} else {
$prefix = "{$protocol}://{$auth}{$domain}";
}
if (strlen($port)) {
if ($has_port) {
$prefix .= ':'.$port;
}
}
@ -189,7 +194,7 @@ final class PhutilURI extends Phobject {
$query = null;
}
if (strlen($this->getFragment())) {
if (phutil_nonempty_string($this->getFragment())) {
$fragment = '#'.$this->getFragment();
} else {
$fragment = null;
@ -428,7 +433,7 @@ final class PhutilURI extends Phobject {
if ($this->isGitURI()) {
// Git URIs use relative paths which do not need to begin with "/".
} else {
if ($this->domain && strlen($path) && $path[0] !== '/') {
if ($this->domain && phutil_nonempty_string($path) && $path[0] !== '/') {
$path = '/'.$path;
}
}

View file

@ -80,6 +80,7 @@ final class AASTNodeList
/* -( Countable )---------------------------------------------------------- */
#[\ReturnTypeWillChange]
public function count() {
return count($this->ids);
}
@ -87,22 +88,27 @@ final class AASTNodeList
/* -( Iterator )----------------------------------------------------------- */
#[\ReturnTypeWillChange]
public function current() {
return $this->list[$this->key()];
}
#[\ReturnTypeWillChange]
public function key() {
return $this->ids[$this->pos];
}
#[\ReturnTypeWillChange]
public function next() {
$this->pos++;
}
#[\ReturnTypeWillChange]
public function rewind() {
$this->pos = 0;
}
#[\ReturnTypeWillChange]
public function valid() {
return $this->pos < count($this->ids);
}

View file

@ -625,6 +625,33 @@ final class PhutilArgumentParser extends Phobject {
return $this->specs[$name]->getDefault();
}
public function getArgAsInteger($name) {
$value = $this->getArg($name);
if ($value === null) {
return $value;
}
if (!preg_match('/^-?\d+\z/', $value)) {
throw new PhutilArgumentUsageException(
pht(
'Parameter provided to argument "--%s" must be an integer.',
$name));
}
$intvalue = (int)$value;
if (phutil_string_cast($intvalue) !== phutil_string_cast($value)) {
throw new PhutilArgumentUsageException(
pht(
'Parameter provided to argument "--%s" is too large to '.
'parse as an integer.',
$name));
}
return $intvalue;
}
public function getUnconsumedArgumentVector() {
return $this->argv;
}
@ -769,7 +796,12 @@ final class PhutilArgumentParser extends Phobject {
pht('There is no **%s** workflow.', $workflow_name));
} else {
$out[] = $this->indent($indent, $workflow->getExamples());
$synopsis = $workflow->getSynopsis();
if ($synopsis !== null) {
$out[] = $this->indent($indent, $workflow->getSynopsis());
}
if ($show_details) {
$full_help = $workflow->getHelp();
if ($full_help) {
@ -800,7 +832,7 @@ final class PhutilArgumentParser extends Phobject {
private function logMessage($message) {
fwrite(STDERR, $message);
PhutilSystem::writeStderr($message);
}

View file

@ -0,0 +1,21 @@
<?php
final class PlatformSymbols
extends Phobject {
public static function getPlatformClientName() {
return 'Arcanist';
}
public static function getPlatformServerName() {
return 'Phabricator';
}
public static function getProductNames() {
return array(
self::getPlatformClientName(),
self::getPlatformServerName(),
);
}
}

View file

@ -110,6 +110,6 @@ final class PhutilConsoleProgressSink
}
private function printLine($line) {
fprintf(STDERR, '%s', $line);
PhutilSystem::writeStderr($line);
}
}

View file

@ -0,0 +1,204 @@
<?php
final class ArcanistMercurialCommitSymbolCommitHardpointQuery
extends ArcanistWorkflowMercurialHardpointQuery {
public function getHardpoints() {
return array(
ArcanistCommitSymbolRef::HARDPOINT_OBJECT,
);
}
protected function canLoadRef(ArcanistRef $ref) {
return ($ref instanceof ArcanistCommitSymbolRef);
}
public function loadHardpoint(array $refs, $hardpoint) {
$symbol_map = array();
foreach ($refs as $key => $ref) {
$symbol_map[$key] = $ref->getSymbol();
}
$symbol_set = array_fuse($symbol_map);
foreach ($symbol_set as $symbol) {
$this->validateSymbol($symbol);
}
$api = $this->getRepositoryAPI();
// Using "hg log" with repeated "--rev arguments will have the following
// behaviors which need accounted for:
// 1. If any one revision is invalid then the entire command will fail. To
// work around this the revset uses a trick where specifying a pattern
// for the bookmark() or tag() predicates instead of a literal won't
// result in failure if the pattern isn't found.
// 2. Multiple markers that resolve to the same node will only be included
// once in the output. Because of this the order of output can't be
// relied upon to match up with the requested symbol. To work around
// this, the template used must also output any associated symbols to
// match back to. Because of this there is no reasonable way to resolve
// symbols with Mercurial-supported modifiers such as 'symbol^'.
// 3. The working directory can't be identified directly, instead a special
// template conditional is used to include 'CWD' as the second item in
// the output if the node is also the working directory, or 'NOTCWD'
// otherwise. This needs included before the tags/bookmarks in order to
// distinguish it from some repository using that same name for a tag or
// bookmark.
$pattern = array();
$arguments = array();
$pattern[] = 'log';
$pattern[] = '--template %s';
$arguments[] = "{rev}\1".
"{node}\1".
"{ifcontains(rev, revset('parents()'), 'CWD', 'NOTCWD')}\1".
"{tags % '{tag}\2'}{bookmarks % '{bookmark}\2'}\3";
foreach ($symbol_set as $symbol) {
// This is the one symbol that wouldn't be a bookmark or tag
if ($symbol === '.') {
$pattern[] = '--rev .';
continue;
}
$predicates = array();
if (ctype_xdigit($symbol)) {
// Commit hashes are 40 characters
if (strlen($symbol) <= 40) {
$predicates[] = hgsprintf('id("%s")', $symbol);
}
}
if (ctype_digit($symbol)) {
// This is 2^32-1 which is (typically) the maximum size of an int in
// Python -- passing anything higher than this to rev() will result
// in a Python exception.
if ($symbol <= 2147483647) {
$predicates[] = hgsprintf('rev("%s")', $symbol);
}
} else {
// Mercurial disallows using numbers as marker names.
$re_symbol = preg_quote($symbol);
$predicates[] = hgsprintf('bookmark("re:^%s$")', $re_symbol);
$predicates[] = hgsprintf('tag("re:^%s$")', $re_symbol);
}
$pattern[] = '--rev %s';
$arguments[] = implode(' or ', $predicates);
}
$pattern = implode(' ', $pattern);
array_unshift($arguments, $pattern);
$future = call_user_func_array(
array($api, 'newFuture'),
$arguments);
list($stdout) = (yield $this->yieldFuture($future));
$lines = explode("\3", $stdout);
$hash_map = array();
$node_list = array();
foreach ($lines as $line) {
$parts = explode("\1", $line, 4);
if (empty(array_filter($parts))) {
continue;
} else if (count($parts) === 3) {
list($rev, $node, $cwd) = $parts;
$markers = array();
} else if (count($parts) === 4) {
list($rev, $node, $cwd, $markers) = $parts;
$markers = array_filter(explode("\2", $markers));
} else {
throw new Exception(
pht('Execution of "hg log" emitted an unexpected line ("%s").',
$line));
}
$node_list[] = $node;
if (in_array($rev, $symbol_set)) {
if (!isset($hash_map[$rev])) {
$hash_map[$rev] = $node;
} else if ($hash_map[$rev] !== $node) {
$hash_map[$rev] = '';
}
}
foreach ($markers as $marker) {
if (!isset($hash_map[$marker])) {
$hash_map[$marker] = $node;
} else if ($hash_map[$marker] !== $node) {
$hash_map[$marker] = '';
}
}
// The log template will mark the working directory node with 'CWD' which
// we insert for the special marker '.' for the working directory, used
// by ArcanistMercurialAPI::newCurrentCommitSymbol().
if ($cwd === 'CWD') {
if (!isset($hash_map['.'])) {
$hash_map['.'] = $node;
} else if ($hash_map['.'] !== $node) {
$hash_map['.'] = '';
}
}
}
// Changeset hashes can be prefixes but also collide with other markers.
// Consider 'cafe' which could be a bookmark or also a changeset hash
// prefix. Mercurial will always allow markers to take precedence over
// changeset hashes when resolving, so only populate symbols that match
// hashes after all other entries are populated, to avoid the hash taing
// a spot which a marker might match.
foreach ($node_list as $node) {
foreach ($symbol_set as $symbol) {
if (strncmp($node, $symbol, strlen($symbol)) === 0) {
if (!isset($hash_map[$symbol])) {
$hash_map[$symbol] = $node;
}
}
}
}
// Remove entries resulting in collisions, which set empty string values
$hash_map = array_filter($hash_map);
$results = array();
foreach ($symbol_map as $key => $symbol) {
if (isset($hash_map[$symbol])) {
$results[$key] = $hash_map[$symbol];
}
}
foreach ($results as $key => $result) {
if ($result === null) {
continue;
}
$ref = id(new ArcanistCommitRef())
->setCommitHash($result);
$results[$key] = $ref;
}
yield $this->yieldMap($results);
}
private function validateSymbol($symbol) {
if (strpos($symbol, "\n") !== false) {
throw new Exception(
pht(
'Commit symbol "%s" contains a newline. This is not a valid '.
'character in a Mercurial commit symbol.',
addcslashes($symbol, "\\\n")));
}
}
}

View file

@ -5,6 +5,13 @@
*/
final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
/**
* Mercurial deceptively indicates that the default encoding is UTF-8 however
* however the actual default appears to be "something else", at least on
* Windows systems. Force all mercurial commands to use UTF-8 encoding.
*/
const ROOT_HG_COMMAND = 'hg --encoding utf-8 ';
private $branch;
private $localCommitInfo;
private $rawDiffCache = array();
@ -13,25 +20,24 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private $featureFutures = array();
protected function buildLocalFuture(array $argv) {
$env = $this->getMercurialEnvironmentVariables();
$argv[0] = self::ROOT_HG_COMMAND.$argv[0];
$argv[0] = 'hg '.$argv[0];
$future = newv('ExecFuture', $argv)
->setEnv($env)
->setCWD($this->getPath());
return $future;
return $this->newConfiguredFuture(newv('ExecFuture', $argv));
}
public function newPassthru($pattern /* , ... */) {
$args = func_get_args();
$args[0] = self::ROOT_HG_COMMAND.$args[0];
return $this->newConfiguredFuture(newv('PhutilExecPassthru', $args));
}
private function newConfiguredFuture(PhutilExecutableFuture $future) {
$args = func_get_args();
$env = $this->getMercurialEnvironmentVariables();
$args[0] = 'hg '.$args[0];
return newv('PhutilExecPassthru', $args)
return $future
->setEnv($env)
->setCWD($this->getPath());
}
@ -448,6 +454,10 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
}
}
protected function newCurrentCommitSymbol() {
return $this->getWorkingCopyRevision();
}
public function getWorkingCopyRevision() {
return '.';
}
@ -655,37 +665,117 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
public function doCommit($message) {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$this->execxLocal('commit -l %s', $tmp_file);
$this->execxLocal('commit --logfile %s', $tmp_file);
$this->reloadWorkingCopy();
}
public function amendCommit($message = null) {
if ($message === null) {
$path_statuses = $this->buildUncommittedStatus();
$existing_message = $this->getCommitMessage(
$this->getWorkingCopyRevision());
if ($message === null || $message == $existing_message) {
if (empty($path_statuses)) {
// If there are no changes to the working directory and the message is
// not being changed then there's nothing to amend. Notably Mercurial
// will return an error code if trying to amend a commit with no change
// to the commit metadata or file changes.
return;
}
$message = $this->getCommitMessage('.');
}
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
if ($this->getMercurialFeature('evolve')) {
$this->execxLocal('amend --logfile %s --', $tmp_file);
try {
$this->execxLocal(
'commit --amend -l %s',
$tmp_file);
$this->execxLocal('evolve --all --');
} catch (CommandException $ex) {
if (preg_match('/nothing changed/', $ex->getStdout())) {
// NOTE: Mercurial considers it an error to make a no-op amend. Although
// we generally defer to the underlying VCS to dictate behavior, this
// one seems a little goofy, and we use amend as part of various
// workflows under the assumption that no-op amends are fine. If this
// amend failed because it's a no-op, just continue.
} else {
$this->execxLocal('evolve --abort --');
throw $ex;
}
$this->reloadWorkingCopy();
return;
}
// Get the child nodes of the current changeset.
list($children) = $this->execxLocal(
'log --template %s --rev %s --',
'{node} ',
'children(.)');
$child_nodes = array_filter(explode(' ', $children));
// For a head commit we can simply use `commit --amend` for both new commit
// message and amending changes from the working directory.
if (empty($child_nodes)) {
$this->execxLocal('commit --amend --logfile %s --', $tmp_file);
} else {
$this->amendNonHeadCommit($child_nodes, $tmp_file);
}
$this->reloadWorkingCopy();
}
/**
* Amends a non-head commit with a new message and file changes. This
* strategy is for Mercurial repositories without the evolve extension.
*
* 1. Run 'arc-amend' which uses Mercurial internals to amend the current
* commit with updated message/file-changes. It results in a new commit
* from the right parent
* 2. For each branch from the original commit, rebase onto the new commit,
* removing the original branch. Note that there is potential for this to
* cause a conflict but this is something the user has to address.
* 3. Strip the original commit.
*
* @param array The list of child changesets off the original commit.
* @param file The file containing the new commit message.
*/
private function amendNonHeadCommit($child_nodes, $tmp_file) {
list($current) = $this->execxLocal(
'log --template %s --rev . --',
'{node}');
$this->execxLocalWithExtension(
'arc-hg',
'arc-amend --logfile %s',
$tmp_file);
list($new_commit) = $this->execxLocal(
'log --rev tip --template %s --',
'{node}');
try {
$rebase_args = array(
'--dest',
$new_commit,
);
foreach ($child_nodes as $child) {
$rebase_args[] = '--source';
$rebase_args[] = $child;
}
$this->execxLocalWithExtension(
'rebase',
'rebase %Ls --',
$rebase_args);
} catch (CommandException $ex) {
$this->execxLocalWithExtension(
'rebase',
'rebase --abort --');
throw $ex;
}
$this->execxLocalWithExtension(
'strip',
'strip --rev %s --',
$current);
}
public function getCommitSummary($commit) {
if ($commit == 'null') {
return pht('(The Empty Void)');
@ -957,6 +1047,129 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
return $this->executeMercurialFeatureTest($feature, true);
}
/**
* Returns the necessary flag for using a Mercurial extension. This will
* enable Mercurial built-in extensions and the "arc-hg" extension that is
* included with Arcanist. This will not enable other extensions, e.g.
* "evolve".
*
* @param string The name of the extension to enable.
* @return string A new command pattern that includes the necessary flags to
* enable the specified extension.
*/
private function getMercurialExtensionFlag($extension) {
switch ($extension) {
case 'arc-hg':
$path = phutil_get_library_root('arcanist');
$path = dirname($path);
$path = $path.'/support/hg/arc-hg.py';
$ext_config = 'extensions.arc-hg='.$path;
break;
case 'rebase':
$ext_config = 'extensions.rebase=';
break;
case 'shelve':
$ext_config = 'extensions.shelve=';
break;
case 'strip':
$ext_config = 'extensions.strip=';
break;
default:
throw new Exception(
pht('Unknown Mercurial Extension: "%s".', $extension));
}
return csprintf('--config %s', $ext_config);
}
/**
* Produces the arguments that should be passed to Mercurial command
* execution that enables a desired extension.
*
* @param string The name of the extension to enable.
* @param string The command pattern that will be run with the extension
* enabled.
* @param array Parameters for the command pattern argument.
* @return array An array where the first item is a Mercurial command
* pattern that includes the necessary flag for enabling the
* desired extension, and all remaining items are parameters
* to that command pattern.
*/
private function buildMercurialExtensionCommand(
$extension,
$pattern /* , ... */) {
$args = func_get_args();
$pattern_args = array_slice($args, 2);
$ext_flag = $this->getMercurialExtensionFlag($extension);
$full_cmd = $ext_flag.' '.$pattern;
$args = array_merge(
array($full_cmd),
$pattern_args);
return $args;
}
public function execxLocalWithExtension(
$extension,
$pattern /* , ... */) {
$args = func_get_args();
$extended_args = call_user_func_array(
array($this, 'buildMercurialExtensionCommand'),
$args);
return call_user_func_array(
array($this, 'execxLocal'),
$extended_args);
}
public function execFutureLocalWithExtension(
$extension,
$pattern /* , ... */) {
$args = func_get_args();
$extended_args = call_user_func_array(
array($this, 'buildMercurialExtensionCommand'),
$args);
return call_user_func_array(
array($this, 'execFutureLocal'),
$extended_args);
}
public function execPassthruWithExtension(
$extension,
$pattern /* , ... */) {
$args = func_get_args();
$extended_args = call_user_func_array(
array($this, 'buildMercurialExtensionCommand'),
$args);
return call_user_func_array(
array($this, 'execPassthru'),
$extended_args);
}
public function execManualLocalWithExtension(
$extension,
$pattern /* , ... */) {
$args = func_get_args();
$extended_args = call_user_func_array(
array($this, 'buildMercurialExtensionCommand'),
$args);
return call_user_func_array(
array($this, 'execManualLocal'),
$extended_args);
}
private function executeMercurialFeatureTest($feature, $resolve) {
if (array_key_exists($feature, $this->featureResults)) {
return $this->featureResults[$feature];
@ -982,8 +1195,9 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private function newMercurialFeatureFuture($feature) {
switch ($feature) {
case 'shelve':
return $this->execFutureLocal(
'--config extensions.shelve= shelve --help --');
return $this->execFutureLocalWithExtension(
'shelve',
'shelve --help --');
case 'evolve':
return $this->execFutureLocal('prune --help --');
default:
@ -1017,17 +1231,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
return new ArcanistMercurialRepositoryRemoteQuery();
}
public function getMercurialExtensionArguments() {
$path = phutil_get_library_root('arcanist');
$path = dirname($path);
$path = $path.'/support/hg/arc-hg.py';
return array(
'--config',
'extensions.arc-hg='.$path,
);
}
protected function newNormalizedURI($uri) {
return new ArcanistRepositoryURINormalizer(
ArcanistRepositoryURINormalizer::TYPE_MERCURIAL,

View file

@ -19,12 +19,6 @@ final class ArcanistMercurialRepositoryMarkerQuery
// to provide a command which works like "git for-each-ref" locally and
// "git ls-remote" when given a remote.
$argv = array();
foreach ($api->getMercurialExtensionArguments() as $arg) {
$argv[] = $arg;
}
$argv[] = 'arc-ls-markers';
// NOTE: In remote mode, we're using passthru and a tempfile on this
// because it's a remote command and may prompt the user to provide
// credentials interactively. In local mode, we can just read stdout.
@ -33,20 +27,17 @@ final class ArcanistMercurialRepositoryMarkerQuery
$tmpfile = new TempFile();
Filesystem::remove($tmpfile);
$argv = array();
$argv[] = '--output';
$argv[] = phutil_string_cast($tmpfile);
}
$argv[] = '--';
if ($remote !== null) {
$argv[] = $remote->getRemoteName();
}
if ($remote !== null) {
$passthru = $api->newPassthru('%Ls', $argv);
$err = $api->execPassthruWithExtension(
'arc-hg',
'arc-ls-markers %Ls',
$argv);
$err = $passthru->execute();
if ($err) {
throw new Exception(
pht(
@ -57,8 +48,10 @@ final class ArcanistMercurialRepositoryMarkerQuery
$raw_data = Filesystem::readFile($tmpfile);
unset($tmpfile);
} else {
$future = $api->newFuture('%Ls', $argv);
list($raw_data) = $future->resolve();
$future = $api->execFutureLocalWithExtension(
'arc-hg',
'arc-ls-markers --');
list($err, $raw_data) = $future->resolve();
}
$items = phutil_json_decode($raw_data);

View file

@ -64,9 +64,11 @@ abstract class ArcanistRepositoryMarkerQuery
$marker->attachWorkingCopyStateRef($state_ref);
$hash = $marker->getCommitHash();
if ($hash !== null) {
$hash = $api->getDisplayHash($hash);
$marker->setDisplayHash($hash);
}
}
$types = $this->markerTypes;
if ($types !== null) {

View file

@ -7,6 +7,14 @@ final class ArcanistMercurialLocalState
private $localBranch;
private $localBookmark;
public function getLocalCommit() {
return $this->localCommit;
}
public function getLocalBookmark() {
return $this->localBookmark;
}
protected function executeSaveLocalState() {
$api = $this->getRepositoryAPI();
$log = $this->getWorkflow()->getLogEngine();
@ -152,8 +160,9 @@ final class ArcanistMercurialLocalState
'arc-%s',
Filesystem::readRandomCharacters(12));
$api->execxLocal(
'--config extensions.shelve= shelve --unknown --name %s --',
$api->execxLocalWithExtension(
'shelve',
'shelve --unknown --name %s --',
$stash_ref);
$log->writeStatus(
@ -171,16 +180,18 @@ final class ArcanistMercurialLocalState
pht('UNSHELVE'),
pht('Restoring uncommitted changes to working copy.'));
$api->execxLocal(
'--config extensions.shelve= unshelve --keep --name %s --',
$api->execxLocalWithExtension(
'shelve',
'unshelve --keep --name %s --',
$stash_ref);
}
protected function discardStash($stash_ref) {
$api = $this->getRepositoryAPI();
$api->execxLocal(
'--config extensions.shelve= shelve --delete %s --',
$api->execxLocalWithExtension(
'shelve',
'shelve --delete %s --',
$stash_ref);
}

View file

@ -192,10 +192,28 @@ abstract class ArcanistRepositoryLocalState
return false;
}
/**
* Stash uncommitted changes temporarily. Use {@method:restoreStash()} to
* bring these changes back.
*
* Note that saving and restoring changes may not behave as expected if used
* in a non-stack manner, i.e. proper use involves only restoring stashes in
* the reverse order they were saved.
*
* @return wild A reference object that refers to the changes which were
* saved. When restoring changes this should be passed to
* {@method:restoreStash()}.
*/
protected function saveStash() {
throw new PhutilMethodNotImplementedException();
}
/**
* Restores changes that were previously stashed by {@method:saveStash()}.
*
* @param wild A reference object referring to which previously stashed
* changes to restore, from invoking {@method:saveStash()}.
*/
protected function restoreStash($ref) {
throw new PhutilMethodNotImplementedException();
}

View file

@ -270,9 +270,9 @@ final class ArcanistRuntime {
$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;
@ -477,8 +477,8 @@ final class ArcanistRuntime {
$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));
@ -519,10 +519,10 @@ final class ArcanistRuntime {
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))));
}

View file

@ -137,7 +137,7 @@ final class PhutilServiceProfiler extends Phobject {
$uri = phutil_censor_credentials($data['uri']);
if (strlen($proxy)) {
if ($proxy !== null) {
$desc = "{$proxy} >> {$uri}";
} else {
$desc = $uri;
@ -203,6 +203,10 @@ final class PhutilServiceProfiler extends Phobject {
}
private static function escapeProfilerStringForDisplay($string) {
if ($string === null) {
return '';
}
// Convert tabs and newlines to spaces and collapse blocks of whitespace,
// most often formatting in queries.
$string = preg_replace('/\s{2,}/', ' ', $string);

View file

@ -226,8 +226,8 @@ final class PhutilClassMapQuery extends Phobject {
$unique = $this->uniqueMethod;
$sort = $this->sortMethod;
if (strlen($expand)) {
if (!strlen($unique)) {
if ($expand !== null) {
if ($unique === null) {
throw new Exception(
pht(
'Trying to execute a class map query for descendants of class '.
@ -245,7 +245,7 @@ final class PhutilClassMapQuery extends Phobject {
->loadObjects();
// Apply the "expand" mechanism, if it is configured.
if (strlen($expand)) {
if ($expand !== null) {
$list = array();
foreach ($objects as $object) {
foreach (call_user_func(array($object, $expand)) as $instance) {
@ -257,7 +257,7 @@ final class PhutilClassMapQuery extends Phobject {
}
// Apply the "unique" mechanism, if it is configured.
if (strlen($unique)) {
if ($unique !== null) {
$map = array();
foreach ($list as $object) {
$key = call_user_func(array($object, $unique));
@ -287,12 +287,12 @@ final class PhutilClassMapQuery extends Phobject {
}
// Apply the "filter" mechanism, if it is configured.
if (strlen($filter)) {
if ($filter !== null) {
$map = mfilter($map, $filter);
}
// Apply the "sort" mechanism, if it is configured.
if (strlen($sort)) {
if ($sort !== null) {
if ($map) {
// The "sort" method may return scalars (which we want to sort with
// "msort()"), or may return PhutilSortVector objects (which we want

View file

@ -296,11 +296,12 @@ final class PhutilSymbolLoader {
// library without breaking library startup.
if ($should_continue) {
// We may not have `pht()` yet.
fprintf(
STDERR,
$message = sprintf(
"%s: %s\n",
'IGNORING CLASS LOAD FAILURE',
$caught->getMessage());
@file_put_contents('php://stderr', $message);
} else {
throw $caught;
}

View file

@ -9,7 +9,7 @@ final class ArcanistArcToolset extends ArcanistToolset {
array(
'name' => 'conduit-uri',
'param' => 'uri',
'help' => pht('Connect to Phabricator install specified by __uri__.'),
'help' => pht('Connect to server specified by __uri__.'),
),
array(
'name' => 'conduit-token',

View file

@ -14,7 +14,7 @@ final class ArcanistShellCompleteWorkflow
public function getWorkflowInformation() {
$help = pht(<<<EOTEXT
Install shell completion so you can use the "tab" key to autocomplete
commands and flags in your shell for Arcanist toolsets and workflows.
commands and flags in your shell for toolsets and workflows.
The **bash** shell is supported.
@ -28,7 +28,7 @@ This will install shell completion into your current shell. After installing,
you may need to start a new shell (or open a new terminal window) to pick up
the updated configuration.
Once installed, completion should work across all Arcanist toolsets.
Once installed, completion should work across all toolsets.
**Using Completion**
@ -53,9 +53,9 @@ You can update shell completion without reinstalling it by running:
You may need to update shell completion if:
- you install new Arcanist toolsets; or
- you move the Arcanist directory; or
- you upgrade Arcanist and the new version fixes shell completion bugs.
- you install new toolsets; or
- you move this software on disk; or
- you upgrade this software and the new version fixes shell completion bugs.
EOTEXT
);

View file

@ -170,16 +170,14 @@ final class PhutilUnitTestEngine extends ArcanistUnitTestEngine {
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);

View file

@ -154,6 +154,147 @@ abstract class PhutilTestCase extends Phobject {
throw new PhutilTestSkippedException($message);
}
final protected function assertCaught(
$expect,
$actual,
$message = null) {
if ($message !== null) {
$message = phutil_string_cast($message);
}
if ($actual === null) {
// This is okay: no exception.
} else if ($actual instanceof Exception) {
// This is also okay.
} else if ($actual instanceof Throwable) {
// And this is okay too.
} else {
// Anything else is no good.
if ($message !== null) {
$output = pht(
'Call to "assertCaught(..., <junk>, ...)" for test case "%s" '.
'passed bad value for test result. Expected null, Exception, '.
'or Throwable; got: %s.',
$message,
phutil_describe_type($actual));
} else {
$output = pht(
'Call to "assertCaught(..., <junk>, ...)" passed bad value for '.
'test result. Expected null, Exception, or Throwable; got: %s.',
phutil_describe_type($actual));
}
$this->failTest($output);
throw new PhutilTestTerminatedException($output);
}
$expect_list = null;
if ($expect === false) {
$expect_list = array();
} else if ($expect === true) {
$expect_list = array(
'Exception',
'Throwable',
);
} else if (is_string($expect) || is_array($expect)) {
$list = (array)$expect;
$items_ok = true;
foreach ($list as $key => $item) {
if (!phutil_nonempty_stringlike($item)) {
$items_ok = false;
break;
}
$list[$key] = phutil_string_cast($item);
}
if ($items_ok) {
$expect_list = $list;
}
}
if ($expect_list === null) {
if ($message !== null) {
$output = pht(
'Call to "assertCaught(<junk>, ...)" for test case "%s" '.
'passed bad expected value. Expected bool, class name as a string, '.
'or a list of class names. Got: %s.',
$message,
phutil_describe_type($expect));
} else {
$output = pht(
'Call to "assertCaught(<junk>, ...)" passed bad expected value. '.
'expected result. Expected null, Exception, or Throwable; got: %s.',
phutil_describe_type($expect));
}
$this->failTest($output);
throw new PhutilTestTerminatedException($output);
}
if ($actual === null) {
$is_match = !$expect_list;
} else {
$is_match = false;
foreach ($expect_list as $exception_class) {
if ($actual instanceof $exception_class) {
$is_match = true;
break;
}
}
}
if ($is_match) {
$this->assertions++;
return;
}
$caller = self::getCallerInfo();
$file = $caller['file'];
$line = $caller['line'];
$output = array();
if ($message !== null) {
$output[] = pht(
'Assertion of caught exception failed (at %s:%d in test case "%s").',
$file,
$line,
$message);
} else {
$output[] = pht(
'Assertion of caught exception failed (at %s:%d).',
$file,
$line);
}
if ($actual === null) {
$output[] = pht('Expected any exception, got no exception.');
} else if (!$expect_list) {
$output[] = pht(
'Expected no exception, got exception of class "%s".',
get_class($actual));
} else {
$expected_classes = implode(', ', $expect_list);
$output[] = pht(
'Expected exception (in class(es): %s), got exception of class "%s".',
$expected_classes,
get_class($actual));
}
$output = implode("\n\n", $output);
$this->failTest($output);
throw new PhutilTestTerminatedException($output);
}
/* -( Exception Handling )------------------------------------------------- */

View file

@ -65,9 +65,10 @@ final class ArcanistUnitConsoleRenderer extends ArcanistUnitRenderer {
50 => "<fg:green>%s</fg><fg:yellow>{$star}</fg> ",
200 => '<fg:green>%s</fg> ',
500 => '<fg:yellow>%s</fg> ',
INF => '<fg:red>%s</fg> ',
);
$least_acceptable = '<fg:red>%s</fg> ';
$milliseconds = $seconds * 1000;
$duration = $this->formatTime($seconds);
foreach ($acceptableness as $upper_bound => $formatting) {
@ -75,7 +76,8 @@ final class ArcanistUnitConsoleRenderer extends ArcanistUnitRenderer {
return phutil_console_format($formatting, $duration);
}
}
return phutil_console_format(end($acceptableness), $duration);
return phutil_console_format($least_acceptable, $duration);
}
private function formatTime($seconds) {

View file

@ -313,7 +313,7 @@ final class ArcanistFileUploader extends Phobject {
* @task internal
*/
private function writeStatus($message) {
fwrite(STDERR, $message."\n");
PhutilSystem::writeStderr($message."\n");
}
}

View file

@ -29,6 +29,7 @@ abstract class PhutilArray
/* -( Countable Interface )------------------------------------------------ */
#[\ReturnTypeWillChange]
public function count() {
return count($this->data);
}
@ -37,22 +38,27 @@ abstract class PhutilArray
/* -( Iterator Interface )------------------------------------------------- */
#[\ReturnTypeWillChange]
public function current() {
return current($this->data);
}
#[\ReturnTypeWillChange]
public function key() {
return key($this->data);
}
#[\ReturnTypeWillChange]
public function next() {
return next($this->data);
}
#[\ReturnTypeWillChange]
public function rewind() {
reset($this->data);
}
#[\ReturnTypeWillChange]
public function valid() {
return (key($this->data) !== null);
}
@ -61,18 +67,22 @@ abstract class PhutilArray
/* -( ArrayAccess Interface )---------------------------------------------- */
#[\ReturnTypeWillChange]
public function offsetExists($key) {
return array_key_exists($key, $this->data);
}
#[\ReturnTypeWillChange]
public function offsetGet($key) {
return $this->data[$key];
}
#[\ReturnTypeWillChange]
public function offsetSet($key, $value) {
$this->data[$key] = $value;
}
#[\ReturnTypeWillChange]
public function offsetUnset($key) {
unset($this->data[$key]);
}

View file

@ -18,6 +18,7 @@ final class PhutilCallbackFilterIterator extends FilterIterator {
$this->callback = $callback;
}
#[\ReturnTypeWillChange]
public function accept() {
return call_user_func($this->callback, $this->current());
}

View file

@ -3,10 +3,73 @@
/**
* Interact with the operating system.
*
* @task stdio Interacting with Standard I/O
* @task memory Interacting with System Memory
*/
final class PhutilSystem extends Phobject {
private static $stdin = false;
private static $stderr = false;
private static $stdout = false;
/**
* @task stdio
*/
public static function getStdinHandle() {
if (self::$stdin === false) {
self::$stdin = self::getStdioHandle('STDIN');
}
return self::$stdin;
}
/**
* @task stdio
*/
public static function getStdoutHandle() {
if (self::$stdout === false) {
self::$stdout = self::getStdioHandle('STDOUT');
}
return self::$stdout;
}
/**
* @task stdio
*/
public static function getStderrHandle() {
if (self::$stderr === false) {
self::$stderr = self::getStdioHandle('STDERR');
}
return self::$stderr;
}
/**
* @task stdio
*/
public static function writeStderr($message) {
$stderr = self::getStderrHandle();
if ($stderr === null) {
return;
}
$message = phutil_string_cast($message);
@fwrite($stderr, $message);
}
/**
* @task stdio
*/
private static function getStdioHandle($ref) {
if (defined($ref)) {
return constant($ref);
}
return null;
}
/**
* Get information about total and free memory on the system.

View file

@ -1000,4 +1000,74 @@ final class PhutilUtilsTestCase extends PhutilTestCase {
}
}
public function testEmptyStringMethods() {
$uri = new PhutilURI('http://example.org/');
$map = array(
array(null, false, false, false, 'literal null'),
array('', false, false, false, 'empty string'),
array('x', true, true, true, 'nonempty string'),
array(false, null, null, null, 'bool'),
array(1, null, null, true, 'integer'),
array($uri, null, true, true, 'uri object'),
array(2.5, null, null, true, 'float'),
array(array(), null, null, null, 'array'),
array((object)array(), null, null, null, 'object'),
);
foreach ($map as $test_case) {
$input = $test_case[0];
$expect_string = $test_case[1];
$expect_stringlike = $test_case[2];
$expect_scalar = $test_case[3];
$test_name = $test_case[4];
$this->executeEmptyStringTest(
$input,
$expect_string,
'phutil_nonempty_string',
$test_name);
$this->executeEmptyStringTest(
$input,
$expect_stringlike,
'phutil_nonempty_stringlike',
$test_name);
$this->executeEmptyStringTest(
$input,
$expect_scalar,
'phutil_nonempty_scalar',
$test_name);
}
}
private function executeEmptyStringTest($input, $expect, $call, $name) {
$name = sprintf('%s(<%s>)', $call, $name);
$caught = null;
try {
$actual = call_user_func($call, $input);
} catch (Exception $ex) {
$caught = $ex;
} catch (Throwable $ex) {
$caught = $ex;
}
if ($expect === null) {
$expect_exceptions = array('InvalidArgumentException');
} else {
$expect_exceptions = false;
}
$this->assertCaught($expect_exceptions, $caught, $name);
if (!$caught) {
$this->assertEqual($expect, $actual, $name);
}
}
}

View file

@ -314,6 +314,8 @@ function phutil_utf8_strlen($string) {
* @return int The console display length of the string.
*/
function phutil_utf8_console_strlen($string) {
$string = phutil_string_cast($string);
// Formatting and colors don't contribute any width in the console.
$string = preg_replace("/\x1B\[\d*m/", '', $string);

View file

@ -2094,3 +2094,128 @@ function phutil_raise_preg_exception($function, array $argv) {
throw new PhutilRegexException($message);
}
/**
* Test if a value is a nonempty string.
*
* The value "null" and the empty string are considered empty; all other
* strings are considered nonempty.
*
* This method raises an exception if passed a value which is neither null
* nor a string.
*
* @param Value to test.
* @return bool True if the parameter is a nonempty string.
*/
function phutil_nonempty_string($value) {
if ($value === null) {
return false;
}
if ($value === '') {
return false;
}
if (is_string($value)) {
return true;
}
throw new InvalidArgumentException(
pht(
'Call to phutil_nonempty_string() expected null or a string, got: %s.',
phutil_describe_type($value)));
}
/**
* Test if a value is a nonempty, stringlike value.
*
* The value "null", the empty string, and objects which have a "__toString()"
* method which returns the empty string are empty.
*
* Other strings, and objects with a "__toString()" method that returns a
* string other than the empty string are considered nonempty.
*
* This method raises an exception if passed any other value.
*
* @param Value to test.
* @return bool True if the parameter is a nonempty, stringlike value.
*/
function phutil_nonempty_stringlike($value) {
if ($value === null) {
return false;
}
if ($value === '') {
return false;
}
if (is_string($value)) {
return true;
}
if (is_object($value)) {
try {
$string = phutil_string_cast($value);
return phutil_nonempty_string($string);
} catch (Exception $ex) {
// Continue below.
} catch (Throwable $ex) {
// Continue below.
}
}
throw new InvalidArgumentException(
pht(
'Call to phutil_nonempty_stringlike() expected a string or stringlike '.
'object, got: %s.',
phutil_describe_type($value)));
}
/**
* Test if a value is a nonempty, scalar value.
*
* The value "null", the empty string, and objects which have a "__toString()"
* method which returns the empty string are empty.
*
* Other strings, objects with a "__toString()" method which returns a
* string other than the empty string, integers, and floats are considered
* scalar.
*
* This method raises an exception if passed any other value.
*
* @param Value to test.
* @return bool True if the parameter is a nonempty, scalar value.
*/
function phutil_nonempty_scalar($value) {
if ($value === null) {
return false;
}
if ($value === '') {
return false;
}
if (is_string($value) || is_int($value) || is_float($value)) {
return true;
}
if (is_object($value)) {
try {
$string = phutil_string_cast($value);
return phutil_nonempty_string($string);
} catch (Exception $ex) {
// Continue below.
} catch (Throwable $ex) {
// Continue below.
}
}
throw new InvalidArgumentException(
pht(
'Call to phutil_nonempty_scalar() expected: a string; or stringlike '.
'object; or int; or float. Got: %s.',
phutil_describe_type($value)));
}

View file

@ -141,7 +141,7 @@ function phutil_format_units_generic(
$scale = array_shift($scales);
$label = array_shift($labels);
while ($n >= $scale && count($labels)) {
$remainder += ($n % $scale) * $accum;
$remainder += ((int)$n % $scale) * $accum;
$n /= $scale;
$accum *= $scale;
$label = array_shift($labels);

View file

@ -164,8 +164,6 @@ EOTEXT
->execute();
}
return;
if ($api->getUncommittedChanges()) {
// TODO: Make this class of error show the uncommitted changes.

View file

@ -15,8 +15,7 @@ Allows you to make a raw Conduit method call:
- Call parameters are required, and read as a JSON blob from stdin.
- Results are written to stdout as a JSON blob.
This workflow is primarily useful for writing scripts which integrate
with Phabricator. Examples:
This workflow is primarily useful for writing scripts. Examples:
$ echo '{}' | arc call-conduit -- conduit.ping
$ echo '{"phid":"PHID-FILE-xxxx"}' | arc call-conduit -- file.download

View file

@ -115,8 +115,7 @@ EOTEXT
'raw' => array(
'help' => pht(
'Read diff from stdin, not from the working copy. This disables '.
'many Arcanist/Phabricator features which depend on having access '.
'to the working copy.'),
'many features which depend on having access to the working copy.'),
'conflicts' => array(
'apply-patches' => pht('%s disables lint.', '--raw'),
'never-apply-patches' => pht('%s disables lint.', '--raw'),
@ -138,8 +137,8 @@ EOTEXT
'param' => 'command',
'help' => pht(
'Generate diff by executing a specified command, not from the '.
'working copy. This disables many Arcanist/Phabricator features '.
'which depend on having access to the working copy.'),
'working copy. This disables many features which depend on having '.
'access to the working copy.'),
'conflicts' => array(
'apply-patches' => pht('%s disables lint.', '--raw-command'),
'never-apply-patches' => pht('%s disables lint.', '--raw-command'),
@ -326,9 +325,8 @@ EOTEXT
'head' => array(
'param' => 'commit',
'help' => pht(
'Specify the end of the commit range. This disables many '.
'Arcanist/Phabricator features which depend on having access to '.
'the working copy.'),
'Specify the end of the commit range. This disables many features '.
'which depend on having access to the working copy.'),
'supports' => array('git'),
'nosupport' => array(
'svn' => pht('Subversion does not support commit ranges.'),
@ -517,7 +515,7 @@ EOTEXT
if ($is_draft) {
throw new ArcanistUsageException(
pht(
'You have specified "--draft", but the version of Phabricator '.
'You have specified "--draft", but the software version '.
'on the server is too old to support draft revisions. Omit '.
'the flag or upgrade the server software.'));
}
@ -674,6 +672,8 @@ EOTEXT
if ($should_edit) {
$edited = $this->newInteractiveEditor($remote_corpus)
->setName('differential-edit-revision-info')
->setTaskMessage(pht(
'Update the details for a revision, then save and exit.'))
->editInteractively();
if ($edited != $remote_corpus) {
$remote_corpus = $edited;
@ -699,7 +699,7 @@ EOTEXT
$this->revisionID = $revision_id;
$revision['message'] = $this->getArgument('message');
if (!strlen($revision['message'])) {
if ($revision['message'] === null) {
$update_messages = $this->readScratchJSONFile('update-messages.json');
$update_messages[$revision_id] = $this->getUpdateMessage(
@ -806,7 +806,10 @@ EOTEXT
if ($is_raw) {
if ($this->getArgument('raw')) {
fwrite(STDERR, pht('Reading diff from stdin...')."\n");
PhutilSystem::writeStderr(
tsprintf(
"%s\n",
pht('Reading diff from stdin...')));
$raw_diff = file_get_contents('php://stdin');
} else if ($this->getArgument('raw-command')) {
list($raw_diff) = execx('%C', $this->getArgument('raw-command'));
@ -947,7 +950,7 @@ EOTEXT
} catch (ConduitClientException $e) {
if ($e->getErrorCode() == 'ERR-BAD-ARCANIST-PROJECT') {
echo phutil_console_wrap(
pht('Lookup of encoding in arcanist project failed: %s',
pht('Lookup of encoding in project failed: %s',
$e->getMessage())."\n");
} else {
throw $e;
@ -988,10 +991,10 @@ EOTEXT
'these files will be marked as binary.',
phutil_count($utf8_problems)),
pht(
"You can learn more about how Phabricator handles character ".
"You can learn more about how this software handles character ".
"encodings (and how to configure encoding settings and detect and ".
"correct encoding problems) by reading 'User Guide: UTF-8 and ".
"Character Encoding' in the Phabricator documentation."),
"Character Encoding' in the documentation."),
pht(
'%s AFFECTED FILE(S)',
phutil_count($utf8_problems)));
@ -1476,6 +1479,8 @@ EOTEXT
} else {
$new_template = $this->newInteractiveEditor($template)
->setName('new-commit')
->setTaskMessage(pht(
'Provide the details for a new revision, then save and exit.'))
->editInteractively();
}
$first = false;
@ -1736,9 +1741,12 @@ EOTEXT
if ($template == '') {
$comments = $this->getDefaultUpdateMessage();
$comments = phutil_string_cast($comments);
$comments = rtrim($comments);
$template = sprintf(
"%s\n\n# %s\n#\n# %s\n# %s\n#\n# %s\n# $ %s\n\n",
rtrim($comments),
$comments,
pht(
'Updating %s: %s',
"D{$fields['revisionID']}",
@ -1752,6 +1760,8 @@ EOTEXT
$comments = $this->newInteractiveEditor($template)
->setName('differential-update-comments')
->setTaskMessage(pht(
'Update the revision comments, then save and exit.'))
->editInteractively();
return $comments;
@ -2354,7 +2364,7 @@ EOTEXT
if (strlen($branch)) {
$upstream_path = $api->getPathToUpstream($branch);
$remote_branch = $upstream_path->getRemoteBranchName();
if (strlen($remote_branch)) {
if ($remote_branch !== null) {
return array(
array(
'type' => 'branch',
@ -2368,7 +2378,7 @@ EOTEXT
// If "arc.land.onto.default" is configured, use that.
$config_key = 'arc.land.onto.default';
$onto = $this->getConfigFromAnySource($config_key);
if (strlen($onto)) {
if ($onto !== null) {
return array(
array(
'type' => 'branch',
@ -2643,7 +2653,7 @@ EOTEXT
if (!$supported) {
$this->writeInfo(
pht('SKIP STAGING'),
pht('Phabricator does not support staging areas for this repository.'));
pht('The server does not support staging areas for this repository.'));
return self::STAGING_REPOSITORY_UNSUPPORTED;
}

View file

@ -19,12 +19,12 @@ 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/").
Installs Conduit credentials into your ~/.arcrc for the given server.
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://devtools.example.com/").
EOTEXT
);
}
@ -91,12 +91,11 @@ EOTEXT
// Ignore.
}
echo phutil_console_format("**%s**\n", pht('LOGIN TO PHABRICATOR'));
echo phutil_console_format("**%s**\n", pht('LOG IN'));
echo phutil_console_format(
"%s\n\n%s\n\n%s",
pht(
'Open this page in your browser and login to '.
'Phabricator if necessary:'),
'Open this page in your browser and log in if necessary:'),
$token_uri,
pht('Then paste the API Token on that page below.'));
@ -204,7 +203,7 @@ EOTEXT
$uri = $conduit_uri;
}
$example = 'https://phabricator.example.com/';
$example = 'https://devtools.example.com/';
$uri_object = new PhutilURI($uri);
$protocol = $uri_object->getProtocol();

View file

@ -11,13 +11,13 @@ final class ArcanistLiberateWorkflow
// TOOLSETS: Expand this help.
$help = pht(<<<EOTEXT
Create or update an Arcanist library.
Create or update a library.
EOTEXT
);
return $this->newWorkflowInformation()
->setSynopsis(
pht('Create or update an Arcanist library.'))
pht('Create or update a library.'))
->addExample(pht('**liberate**'))
->addExample(pht('**liberate** [__path__]'))
->setHelp($help);
@ -79,22 +79,35 @@ EOTEXT
);
}
$any_errors = false;
foreach ($paths as $path) {
$log->writeStatus(
pht('WORK'),
pht(
'Updating library: %s',
Filesystem::readablePath($path).DIRECTORY_SEPARATOR));
$this->liberatePath($path);
$exit_code = $this->liberatePath($path);
if ($exit_code !== 0) {
$any_errors = true;
$log->writeError(
pht('ERROR'),
pht('Failed to update library: %s', $path));
}
}
if (!$any_errors) {
$log->writeSuccess(
pht('DONE'),
pht('Updated %s librarie(s).', phutil_count($paths)));
}
return 0;
}
/**
* @return int The exit code of running the rebuild-map.php script, which
* will be 0 to indicate success or non-zero for failure.
*/
private function liberatePath($path) {
if (!Filesystem::pathExists($path.'/__phutil_library_init__.php')) {
echo tsprintf(
@ -103,8 +116,7 @@ EOTEXT
'No library currently exists at the path "%s"...',
$path));
$this->liberateCreateDirectory($path);
$this->liberateCreateLibrary($path);
return;
return $this->liberateCreateLibrary($path);
}
$version = $this->getLibraryFormatVersion($path);
@ -119,8 +131,6 @@ EOTEXT
throw new ArcanistUsageException(
pht("Unknown library version '%s'!", $version));
}
echo tsprintf("%s\n", pht('Done.'));
}
private function getLibraryFormatVersion($path) {
@ -140,6 +150,10 @@ EOTEXT
return 1;
}
/**
* @return int The exit code of running the rebuild-map.php script, which
* will be 0 to indicate success or non-zero for failure.
*/
private function liberateVersion2($path) {
$bin = $this->getScriptPath('support/lib/rebuild-map.php');
@ -181,10 +195,14 @@ EOTEXT
execx('mkdir -p %R', $path);
}
/**
* @return int The exit code of running the rebuild-map.php script, which
* will be 0 to indicate success or non-zero for failure.
*/
private function liberateCreateLibrary($path) {
$init_path = $path.'/__phutil_library_init__.php';
if (Filesystem::pathExists($init_path)) {
return;
return 0;
}
echo pht("Creating new libphutil library in '%s'.", $path)."\n";
@ -213,7 +231,7 @@ EOTEXT
'__phutil_library_init__.php',
$path);
Filesystem::writeFile($init_path, $template);
$this->liberateVersion2($path);
return $this->liberateVersion2($path);
}

View file

@ -707,7 +707,7 @@ EOTEXT
'git apply --whitespace nowarn --index --reject -- %s',
$patchfile);
$passthru->setCWD($repository_api->getPath());
$err = $passthru->execute();
$err = $passthru->resolve();
if ($err) {
echo phutil_console_format(
@ -890,8 +890,8 @@ EOTEXT
'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}");
}
@ -909,6 +909,8 @@ EOTEXT
$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);

View file

@ -9,12 +9,12 @@ final class ArcanistUpgradeWorkflow
public function getWorkflowInformation() {
$help = pht(<<<EOTEXT
Upgrade Arcanist to the latest version.
Upgrade this program to the latest version.
EOTEXT
);
return $this->newWorkflowInformation()
->setSynopsis(pht('Upgrade Arcanist to the latest version.'))
->setSynopsis(pht('Upgrade this program to the latest version.'))
->addExample(pht('**upgrade**'))
->setHelp($help);
}
@ -51,10 +51,10 @@ EOTEXT
if (!$is_git) {
throw new PhutilArgumentUsageException(
pht(
'The "arc upgrade" workflow uses "git pull" to upgrade '.
'Arcanist, but the "arcanist/" directory (in "%s") is not a Git '.
'working copy. You must leave "arcanist/" as a Git '.
'working copy to use "arc upgrade".',
'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));
}
@ -125,7 +125,7 @@ EOTEXT
$log->writeSuccess(
pht('UPGRADED'),
pht('Your copy of Arcanist is now up to date.'));
pht('This software is now up to date.'));
return 0;
}

View file

@ -9,7 +9,7 @@ final class ArcanistUploadWorkflow
public function getWorkflowInformation() {
$help = pht(<<<EOTEXT
Upload one or more files from local disk to Phabricator.
Upload one or more files from local disk.
EOTEXT
);

View file

@ -28,15 +28,15 @@ The __symbol__ may be a branch or bookmark name, a revision name (like "D123"),
a task name (like "T123"), or a new symbol.
If you provide a symbol which currently does not identify any ongoing work,
Arcanist will create a new branch or bookmark with the name you provide.
a new branch or bookmark will be created with the name you provide.
If you provide the name of an existing branch or bookmark, Arcanist will switch
to that branch or bookmark.
If you provide the name of an existing branch or bookmark, the working copy
will be switched to that branch or bookmark.
If you provide the name of a revision or task, Arcanist will look for a related
branch or bookmark that exists in the working copy. If it finds one, it will
switch to it. If it does not find one, it will attempt to create a new branch
or bookmark.
If you provide the name of a revision or task, the workflow will look for a
related branch or bookmark that already exists in the working copy. If one is
found, it will switch to it. If it does not find one, it will attempt to create
a new branch or bookmark.
When "arc work" creates a branch or bookmark, it will use **--start** as the
branchpoint if it is provided. Otherwise, the current working copy state will

View file

@ -143,7 +143,7 @@ abstract class ArcanistWorkflow extends Phobject {
if ($information) {
$synopsis = $information->getSynopsis();
if (strlen($synopsis)) {
if ($synopsis !== null) {
$phutil_workflow->setSynopsis($synopsis);
}
@ -154,7 +154,7 @@ abstract class ArcanistWorkflow extends Phobject {
}
$help = $information->getHelp();
if (strlen($help)) {
if ($help !== null) {
// Unwrap linebreaks in the help text so we don't get weird formatting.
$help = preg_replace("/(?<=\S)\n(?=\S)/", ' ', $help);
@ -534,7 +534,7 @@ abstract class ArcanistWorkflow extends Phobject {
$conduit_uri = $this->conduitURI;
$message = phutil_console_format(
"\n%s\n\n %s\n\n%s\n%s",
pht('YOU NEED TO __INSTALL A CERTIFICATE__ TO LOGIN TO PHABRICATOR'),
pht('YOU NEED TO __INSTALL A CERTIFICATE__ TO LOG IN'),
pht('To do this, run: **%s**', 'arc install-certificate'),
pht("The server '%s' rejected your request:", $conduit_uri),
$ex->getMessage());
@ -1234,6 +1234,9 @@ abstract class ArcanistWorkflow extends Phobject {
$commit_message = $this->newInteractiveEditor($template)
->setName(pht('commit-message'))
->setTaskMessage(pht(
'Supply commit message for uncommitted changes, then save and '.
'exit.'))
->editInteractively();
if ($commit_message === $template) {
@ -1562,7 +1565,7 @@ abstract class ArcanistWorkflow extends Phobject {
* @return void
*/
final protected function writeStatusMessage($msg) {
fwrite(STDERR, $msg);
PhutilSystem::writeStderr($msg);
}
final public function writeInfo($title, $message) {
@ -1954,11 +1957,10 @@ abstract class ArcanistWorkflow extends Phobject {
} catch (ConduitClientException $ex) {
if ($ex->getErrorCode() == 'ERR-CONDUIT-CALL') {
$reasons[] = pht(
'This version of Arcanist is more recent than the version of '.
'Phabricator you are connecting to: the Phabricator install is '.
'out of date and does not have support for identifying '.
'repositories by callsign or URI. Update Phabricator to enable '.
'these features.');
'This software version on the server you are connecting to is out '.
'of date and does not have support for identifying repositories '.
'by callsign or URI. Update the server sofwware to enable these '.
'features.');
return array(null, $reasons);
}
throw $ex;
@ -2201,9 +2203,8 @@ abstract class ArcanistWorkflow extends Phobject {
throw new ArcanistUsageException(
pht(
"Unable to find a browser command to run. Set '%s' in your ".
"Arcanist config to specify a command to use.",
'browser'));
'Unable to find a browser command to run. Set "browser" in your '.
'configuration to specify a command to use.'));
}

View file

@ -21,18 +21,123 @@ from mercurial import (
hg,
i18n,
node,
registrar,
)
_ = i18n._
cmdtable = {}
command = registrar.command(cmdtable)
# Older veresions of Mercurial (~4.7) moved the command function and the
# remoteopts object to different modules. Using try/except here to attempt
# allowing this module to load properly, despite whether individual commands
# will work properly on older versions of Mercurial or not.
# https://phab.mercurial-scm.org/rHG46ba2cdda476ac53a8a8f50e4d9435d88267db60
# https://phab.mercurial-scm.org/rHG04baab18d60a5c833ab3190506147e01b3c6d12c
try:
from mercurial import registrar
command = registrar.command(cmdtable)
except:
command = cmdutil.command(cmdtable)
try:
remoteopts = cmdutil.remoteopts
except:
from mercurial import commands
remoteopts = commands.remoteopts
try:
parseurl = hg.parseurl
except:
from mercurial import utils
parseurl = utils.urlutil.parseurl
@command(
b'arc-amend',
[
(b'l',
b'logfile',
b'',
_(b'read commit message from file'),
_(b'FILE')),
(b'm',
b'message',
b'',
_(b'use text as commit message'),
_(b'TEXT')),
(b'u',
b'user',
b'',
_(b'record the specified user as committer'),
_(b'USER')),
(b'd',
b'date',
b'',
_(b'record the specified date as commit date'),
_(b'DATE')),
(b'A',
b'addremove',
False,
_(b'mark new/missing files as added/removed before committing')),
(b'n',
b'note',
b'',
_(b'store a note on amend'),
_(b'TEXT')),
],
_(b'[OPTION]'))
def amend(ui, repo, source=None, **opts):
"""amend
Uses Mercurial internal API to amend changes to a non-head commit.
(This is an Arcanist extension to Mercurial.)
Returns 0 if amending succeeds, 1 otherwise.
"""
# The option keys seem to come in as 'str' type but the cmdutil.amend() code
# expects them as binary. To account for both Python 2 and Python 3
# compatibility, insert the value under both 'str' and binary type.
newopts = {}
for key in opts:
val = opts.get(key)
newopts[key] = val
if isinstance(key, str):
newkey = key.encode('UTF-8')
newopts[newkey] = val
orig = repo[b'.']
extra = {}
pats = []
cmdutil.amend(ui, repo, orig, extra, pats, newopts)
"""
# This will allow running amend on older versions of Mercurial, ~3.5, however
# the behavior on those versions will squash child commits of the working
# directory into the amended commit which is undesired.
try:
cmdutil.amend(ui, repo, orig, extra, pats, newopts)
except:
def commitfunc(ui, repo, message, match, opts):
return repo.commit(
message,
opts.get('user') or orig.user(),
opts.get('date') or orig.date(),
match,
extra=extra)
cmdutil.amend(ui, repo, commitfunc, orig, extra, pats, newopts)
"""
return 0
@command(
b'arc-ls-markers',
[(b'', b'output', b'',
_(b'file to output refs to'), _(b'FILE')),
] + cmdutil.remoteopts,
[
(b'',
b'output',
b'',
_(b'file to output refs to'),
_(b'FILE')),
] + remoteopts,
_(b'[--output FILENAME] [SOURCE]'))
def lsmarkers(ui, repo, source=None, **opts):
"""list markers
@ -168,7 +273,7 @@ def remotemarkers(ui, repo, source, opts):
markers = []
source, branches = hg.parseurl(ui.expandpath(source))
source, branches = parseurl(ui.expandpath(source))
remote = hg.peer(repo, opts, source)
with remote.commandexecutor() as e:

Some files were not shown because too many files have changed in this diff Show more