Page MenuHomePhabricator

D20987.diff
No OneTemporary

D20987.diff

diff --git a/scripts/arcanist.php b/scripts/arcanist.php
--- a/scripts/arcanist.php
+++ b/scripts/arcanist.php
@@ -35,6 +35,10 @@
'param' => 'token',
'help' => pht('Use a specific authentication token.'),
),
+ array(
+ 'name' => 'anonymous',
+ 'help' => pht('Run workflow as a public user, without authenticating.'),
+ ),
array(
'name' => 'conduit-version',
'param' => 'version',
@@ -64,6 +68,7 @@
$conduit_timeout = $base_args->getArg('conduit-timeout');
$skip_arcconfig = $base_args->getArg('skip-arcconfig');
$custom_arcrc = $base_args->getArg('arcrc-file');
+$is_anonymous = $base_args->getArg('anonymous');
$load = $base_args->getArg('load-phutil-library');
$help = $base_args->getArg('help');
$args = array_values($base_args->getUnconsumedArgumentVector());
@@ -323,6 +328,10 @@
$conduit_token = $force_token;
}
+ if ($is_anonymous) {
+ $conduit_token = null;
+ }
+
$description = implode(' ', $original_argv);
$credentials = array(
'user' => $user_name,
@@ -332,6 +341,23 @@
);
$workflow->setConduitCredentials($credentials);
+ $basic_user = $configuration_manager->getConfigFromAnySource(
+ 'http.basicauth.user');
+ $basic_pass = $configuration_manager->getConfigFromAnySource(
+ 'http.basicauth.pass');
+
+ $engine = id(new ArcanistConduitEngine())
+ ->setConduitURI($conduit_uri)
+ ->setConduitToken($conduit_token)
+ ->setBasicAuthUser($basic_user)
+ ->setBasicAuthPass($basic_pass);
+
+ if ($conduit_timeout) {
+ $engine->setConduitTimeout($conduit_timeout);
+ }
+
+ $workflow->setConduitEngine($engine);
+
if ($need_auth) {
if ((!$user_name || !$certificate) && (!$conduit_token)) {
$arc = 'arc';
@@ -408,7 +434,7 @@
fwrite(STDERR, phutil_console_format(
"**%s** %s\n",
pht('Usage Exception:'),
- $ex->getMessage()));
+ rtrim($ex->getMessage())));
}
if ($config) {
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -46,8 +46,17 @@
'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php',
'ArcanistBraceFormattingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php',
'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBraceFormattingXHPASTLinterRuleTestCase.php',
+ 'ArcanistBranchRef' => 'ref/ArcanistBranchRef.php',
'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php',
- 'ArcanistBrowseWorkflow' => 'workflow/ArcanistBrowseWorkflow.php',
+ 'ArcanistBrowseCommitHardpointLoader' => 'browse/loader/ArcanistBrowseCommitHardpointLoader.php',
+ 'ArcanistBrowseCommitURIHardpointLoader' => 'browse/loader/ArcanistBrowseCommitURIHardpointLoader.php',
+ 'ArcanistBrowseObjectNameURIHardpointLoader' => 'browse/loader/ArcanistBrowseObjectNameURIHardpointLoader.php',
+ 'ArcanistBrowsePathURIHardpointLoader' => 'browse/loader/ArcanistBrowsePathURIHardpointLoader.php',
+ 'ArcanistBrowseRef' => 'browse/ref/ArcanistBrowseRef.php',
+ 'ArcanistBrowseRevisionURIHardpointLoader' => 'browse/loader/ArcanistBrowseRevisionURIHardpointLoader.php',
+ 'ArcanistBrowseURIHardpointLoader' => 'browse/loader/ArcanistBrowseURIHardpointLoader.php',
+ 'ArcanistBrowseURIRef' => 'browse/ref/ArcanistBrowseURIRef.php',
+ 'ArcanistBrowseWorkflow' => 'browse/workflow/ArcanistBrowseWorkflow.php',
'ArcanistBuildPlanRef' => 'ref/ArcanistBuildPlanRef.php',
'ArcanistBuildRef' => 'ref/ArcanistBuildRef.php',
'ArcanistBundle' => 'parser/ArcanistBundle.php',
@@ -84,12 +93,16 @@
'ArcanistCommentSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentSpacingXHPASTLinterRule.php',
'ArcanistCommentStyleXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentStyleXHPASTLinterRule.php',
'ArcanistCommentStyleXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistCommentStyleXHPASTLinterRuleTestCase.php',
+ 'ArcanistCommitRef' => 'ref/ArcanistCommitRef.php',
+ 'ArcanistCommitUpstreamHardpointLoader' => 'loader/ArcanistCommitUpstreamHardpointLoader.php',
'ArcanistCommitWorkflow' => 'workflow/ArcanistCommitWorkflow.php',
'ArcanistCompilerLintRenderer' => 'lint/renderer/ArcanistCompilerLintRenderer.php',
'ArcanistComposerLinter' => 'lint/linter/ArcanistComposerLinter.php',
'ArcanistComprehensiveLintEngine' => 'lint/engine/ArcanistComprehensiveLintEngine.php',
'ArcanistConcatenationOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php',
'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConcatenationOperatorXHPASTLinterRuleTestCase.php',
+ 'ArcanistConduitCall' => 'conduit/ArcanistConduitCall.php',
+ 'ArcanistConduitEngine' => 'conduit/ArcanistConduitEngine.php',
'ArcanistConfiguration' => 'configuration/ArcanistConfiguration.php',
'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php',
'ArcanistConfigurationDrivenUnitTestEngine' => 'unit/engine/ArcanistConfigurationDrivenUnitTestEngine.php',
@@ -168,7 +181,10 @@
'ArcanistGeneratedLinterTestCase' => 'lint/linter/__tests__/ArcanistGeneratedLinterTestCase.php',
'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php',
'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php',
+ 'ArcanistGitCommitMessageHardpointLoader' => 'loader/ArcanistGitCommitMessageHardpointLoader.php',
+ 'ArcanistGitHardpointLoader' => 'loader/ArcanistGitHardpointLoader.php',
'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php',
+ 'ArcanistGitRevisionHardpointLoader' => 'loader/ArcanistGitRevisionHardpointLoader.php',
'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php',
'ArcanistGlobalVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistGlobalVariableXHPASTLinterRule.php',
'ArcanistGlobalVariableXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistGlobalVariableXHPASTLinterRuleTestCase.php',
@@ -178,6 +194,7 @@
'ArcanistGoTestResultParserTestCase' => 'unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php',
'ArcanistHLintLinter' => 'lint/linter/ArcanistHLintLinter.php',
'ArcanistHLintLinterTestCase' => 'lint/linter/__tests__/ArcanistHLintLinterTestCase.php',
+ 'ArcanistHardpointLoader' => 'loader/ArcanistHardpointLoader.php',
'ArcanistHelpWorkflow' => 'workflow/ArcanistHelpWorkflow.php',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule.php',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase.php',
@@ -254,10 +271,14 @@
'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php',
'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase.php',
'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php',
+ 'ArcanistMercurialBranchCommitHardpointLoader' => 'loader/ArcanistMercurialBranchCommitHardpointLoader.php',
+ 'ArcanistMercurialHardpointLoader' => 'loader/ArcanistMercurialHardpointLoader.php',
'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php',
'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php',
+ 'ArcanistMercurialWorkingCopyCommitHardpointLoader' => 'loader/ArcanistMercurialWorkingCopyCommitHardpointLoader.php',
'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php',
'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php',
+ 'ArcanistMessageRevisionHardpointLoader' => 'loader/ArcanistMessageRevisionHardpointLoader.php',
'ArcanistMissingLinterException' => 'lint/linter/exception/ArcanistMissingLinterException.php',
'ArcanistModifierOrderingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php',
'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistModifierOrderingXHPASTLinterRuleTestCase.php',
@@ -322,9 +343,12 @@
'ArcanistPyLintLinterTestCase' => 'lint/linter/__tests__/ArcanistPyLintLinterTestCase.php',
'ArcanistRaggedClassTreeEdgeXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistRaggedClassTreeEdgeXHPASTLinterRule.php',
'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase.php',
+ 'ArcanistRef' => 'ref/ArcanistRef.php',
+ 'ArcanistRefQuery' => 'ref/ArcanistRefQuery.php',
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php',
'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php',
+ 'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php',
'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php',
@@ -332,6 +356,8 @@
'ArcanistReusedIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php',
'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedIteratorXHPASTLinterRuleTestCase.php',
'ArcanistRevertWorkflow' => 'workflow/ArcanistRevertWorkflow.php',
+ 'ArcanistRevisionRef' => 'ref/ArcanistRevisionRef.php',
+ 'ArcanistRevisionRefSource' => 'ref/ArcanistRevisionRefSource.php',
'ArcanistRuboCopLinter' => 'lint/linter/ArcanistRuboCopLinter.php',
'ArcanistRuboCopLinterTestCase' => 'lint/linter/__tests__/ArcanistRuboCopLinterTestCase.php',
'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php',
@@ -344,6 +370,7 @@
'ArcanistSemicolonSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSemicolonSpacingXHPASTLinterRule.php',
'ArcanistSemicolonSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSemicolonSpacingXHPASTLinterRuleTestCase.php',
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
+ 'ArcanistSetting' => 'configuration/ArcanistSetting.php',
'ArcanistSettings' => 'configuration/ArcanistSettings.php',
'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php',
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
@@ -414,6 +441,7 @@
'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php',
'ArcanistWorkflow' => 'workflow/ArcanistWorkflow.php',
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.php',
+ 'ArcanistWorkingCopyStateRef' => 'ref/ArcanistWorkingCopyStateRef.php',
'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php',
'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php',
'ArcanistXHPASTLintSwitchHook' => 'lint/linter/xhpast/ArcanistXHPASTLintSwitchHook.php',
@@ -916,7 +944,16 @@
'ArcanistBookmarkWorkflow' => 'ArcanistFeatureWorkflow',
'ArcanistBraceFormattingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
+ 'ArcanistBranchRef' => 'ArcanistRef',
'ArcanistBranchWorkflow' => 'ArcanistFeatureWorkflow',
+ 'ArcanistBrowseCommitHardpointLoader' => 'ArcanistHardpointLoader',
+ 'ArcanistBrowseCommitURIHardpointLoader' => 'ArcanistBrowseURIHardpointLoader',
+ 'ArcanistBrowseObjectNameURIHardpointLoader' => 'ArcanistBrowseURIHardpointLoader',
+ 'ArcanistBrowsePathURIHardpointLoader' => 'ArcanistBrowseURIHardpointLoader',
+ 'ArcanistBrowseRef' => 'ArcanistRef',
+ 'ArcanistBrowseRevisionURIHardpointLoader' => 'ArcanistBrowseURIHardpointLoader',
+ 'ArcanistBrowseURIHardpointLoader' => 'ArcanistHardpointLoader',
+ 'ArcanistBrowseURIRef' => 'ArcanistRef',
'ArcanistBrowseWorkflow' => 'ArcanistWorkflow',
'ArcanistBuildPlanRef' => 'Phobject',
'ArcanistBuildRef' => 'Phobject',
@@ -954,12 +991,16 @@
'ArcanistCommentSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistCommentStyleXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistCommentStyleXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
+ 'ArcanistCommitRef' => 'ArcanistRef',
+ 'ArcanistCommitUpstreamHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistCommitWorkflow' => 'ArcanistWorkflow',
'ArcanistCompilerLintRenderer' => 'ArcanistLintRenderer',
'ArcanistComposerLinter' => 'ArcanistLinter',
'ArcanistComprehensiveLintEngine' => 'ArcanistLintEngine',
'ArcanistConcatenationOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
+ 'ArcanistConduitCall' => 'Phobject',
+ 'ArcanistConduitEngine' => 'Phobject',
'ArcanistConfiguration' => 'Phobject',
'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine',
'ArcanistConfigurationDrivenUnitTestEngine' => 'ArcanistUnitTestEngine',
@@ -1038,7 +1079,10 @@
'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistGetConfigWorkflow' => 'ArcanistWorkflow',
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
+ 'ArcanistGitCommitMessageHardpointLoader' => 'ArcanistGitHardpointLoader',
+ 'ArcanistGitHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
+ 'ArcanistGitRevisionHardpointLoader' => 'ArcanistGitHardpointLoader',
'ArcanistGitUpstreamPath' => 'Phobject',
'ArcanistGlobalVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistGlobalVariableXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@@ -1048,6 +1092,7 @@
'ArcanistGoTestResultParserTestCase' => 'PhutilTestCase',
'ArcanistHLintLinter' => 'ArcanistExternalLinter',
'ArcanistHLintLinterTestCase' => 'ArcanistExternalLinterTestCase',
+ 'ArcanistHardpointLoader' => 'Phobject',
'ArcanistHelpWorkflow' => 'ArcanistWorkflow',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@@ -1124,10 +1169,14 @@
'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
+ 'ArcanistMercurialBranchCommitHardpointLoader' => 'ArcanistMercurialHardpointLoader',
+ 'ArcanistMercurialHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistMercurialParser' => 'Phobject',
'ArcanistMercurialParserTestCase' => 'PhutilTestCase',
+ 'ArcanistMercurialWorkingCopyCommitHardpointLoader' => 'ArcanistMercurialHardpointLoader',
'ArcanistMergeConflictLinter' => 'ArcanistLinter',
'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase',
+ 'ArcanistMessageRevisionHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistMissingLinterException' => 'Exception',
'ArcanistModifierOrderingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@@ -1192,9 +1241,12 @@
'ArcanistPyLintLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistRaggedClassTreeEdgeXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
+ 'ArcanistRef' => 'Phobject',
+ 'ArcanistRefQuery' => 'Phobject',
'ArcanistRepositoryAPI' => 'Phobject',
'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase',
'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase',
+ 'ArcanistRepositoryRef' => 'ArcanistRef',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
@@ -1202,6 +1254,8 @@
'ArcanistReusedIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistRevertWorkflow' => 'ArcanistWorkflow',
+ 'ArcanistRevisionRef' => 'ArcanistRef',
+ 'ArcanistRevisionRefSource' => 'Phobject',
'ArcanistRuboCopLinter' => 'ArcanistExternalLinter',
'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistRubyLinter' => 'ArcanistExternalLinter',
@@ -1214,6 +1268,7 @@
'ArcanistSemicolonSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistSemicolonSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistSetConfigWorkflow' => 'ArcanistWorkflow',
+ 'ArcanistSetting' => 'Phobject',
'ArcanistSettings' => 'Phobject',
'ArcanistShellCompleteWorkflow' => 'ArcanistWorkflow',
'ArcanistSingleLintEngine' => 'ArcanistLintEngine',
@@ -1284,6 +1339,7 @@
'ArcanistWhichWorkflow' => 'ArcanistWorkflow',
'ArcanistWorkflow' => 'Phobject',
'ArcanistWorkingCopyIdentity' => 'Phobject',
+ 'ArcanistWorkingCopyStateRef' => 'ArcanistRef',
'ArcanistXHPASTLintNamingHook' => 'Phobject',
'ArcanistXHPASTLintNamingHookTestCase' => 'PhutilTestCase',
'ArcanistXHPASTLintSwitchHook' => 'Phobject',
diff --git a/src/browse/loader/ArcanistBrowseCommitHardpointLoader.php b/src/browse/loader/ArcanistBrowseCommitHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/browse/loader/ArcanistBrowseCommitHardpointLoader.php
@@ -0,0 +1,80 @@
+<?php
+
+final class ArcanistBrowseCommitHardpointLoader
+ extends ArcanistHardpointLoader {
+
+ const LOADERKEY = 'browse.ref.commit';
+
+ public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
+ return true;
+ }
+
+ public function canLoadRef(ArcanistRef $ref) {
+ return ($ref instanceof ArcanistBrowseRef);
+ }
+
+ public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
+ return ($hardpoint == 'commitRefs');
+ }
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $query = $this->getQuery();
+
+ $api = $query->getRepositoryAPI();
+ if (!$api) {
+ return array();
+ }
+
+ $repository_ref = $query->getRepositoryRef();
+ if (!$repository_ref) {
+ return array();
+ }
+ $repository_phid = $repository_ref->getPHID();
+
+ $commit_map = array();
+ foreach ($refs as $key => $ref) {
+ $token = $ref->getToken();
+
+ if ($token === '.') {
+ // Git resolves "." like HEAD, but we want to treat it as "browse the
+ // current directory" instead in all cases.
+ continue;
+ }
+
+ // Always resolve the empty token; top-level loaders filter out
+ // irrelevant tokens before this stage.
+ if ($token === null) {
+ $token = $api->getHeadCommit();
+ }
+
+ // TODO: We should pull a full commit ref out of the API as soon as it
+ // is able to provide them. In particular, we currently miss Git tree
+ // hashes which reduces the accuracy of lookups.
+
+ try {
+ $commit = $api->getCanonicalRevisionName($token);
+ if ($commit) {
+ $commit_map[$commit][] = $key;
+ }
+ } catch (Exception $ex) {
+ // Ignore anything we can't resolve.
+ }
+ }
+
+ if (!$commit_map) {
+ return array();
+ }
+
+ $results = array();
+ foreach ($commit_map as $commit_identifier => $ref_keys) {
+ foreach ($ref_keys as $key) {
+ $commit_ref = id(new ArcanistCommitRef())
+ ->setCommitHash($commit_identifier);
+ $results[$key][] = $commit_ref;
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/browse/loader/ArcanistBrowseCommitURIHardpointLoader.php b/src/browse/loader/ArcanistBrowseCommitURIHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/browse/loader/ArcanistBrowseCommitURIHardpointLoader.php
@@ -0,0 +1,92 @@
+<?php
+
+final class ArcanistBrowseCommitURIHardpointLoader
+ extends ArcanistBrowseURIHardpointLoader {
+
+ const LOADERKEY = 'browse.uri.commit';
+ const BROWSETYPE = 'commit';
+
+ public function willLoadBrowseURIRefs(array $refs) {
+ $refs = $this->getRefsWithSupportedTypes($refs);
+
+ if (!$refs) {
+ return;
+ }
+
+ $query = $this->getQuery();
+
+ $working_ref = $query->getWorkingCopyRef();
+ if (!$working_ref) {
+ // If we aren't in a working copy, don't warn about this.
+ return;
+ }
+
+ $repository_ref = $this->getQuery()->getRepositoryRef();
+ if (!$repository_ref) {
+ echo pht(
+ 'NO REPOSITORY: Unable to determine which repository this working '.
+ 'copy belongs to, so arguments can not be resolved as commits. Use '.
+ '"%s" to understand how repositories are resolved.',
+ 'arc which');
+ echo "\n";
+ return;
+ }
+ }
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $query = $this->getQuery();
+
+ $api = $query->getRepositoryAPI();
+ if (!$api) {
+ return array();
+ }
+
+ $repository_ref = $query->getRepositoryRef();
+ if (!$repository_ref) {
+ return array();
+ }
+
+ $refs = $this->getRefsWithSupportedTypes($refs);
+ if (!$refs) {
+ return array();
+ }
+
+ $this->newQuery($refs)
+ ->needHardpoints(
+ array(
+ 'commitRefs',
+ ))
+ ->execute();
+
+ $commit_refs = array();
+ foreach ($refs as $key => $ref) {
+ foreach ($ref->getCommitRefs() as $commit_ref) {
+ $commit_refs[] = $commit_ref;
+ }
+ }
+
+ $this->newQuery($commit_refs)
+ ->needHardpoints(
+ array(
+ 'upstream',
+ ))
+ ->execute();
+
+ $results = array();
+ foreach ($refs as $key => $ref) {
+ $commit_refs = $ref->getCommitRefs();
+ foreach ($commit_refs as $commit_ref) {
+ $uri = $commit_ref->getURI();
+ if ($uri !== null) {
+ $results[$key][] = id(new ArcanistBrowseURIRef())
+ ->setURI($uri)
+ ->setType(self::BROWSETYPE);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+
+}
diff --git a/src/browse/loader/ArcanistBrowseObjectNameURIHardpointLoader.php b/src/browse/loader/ArcanistBrowseObjectNameURIHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/browse/loader/ArcanistBrowseObjectNameURIHardpointLoader.php
@@ -0,0 +1,54 @@
+<?php
+
+final class ArcanistBrowseObjectNameURIHardpointLoader
+ extends ArcanistBrowseURIHardpointLoader {
+
+ const LOADERKEY = 'browse.uri.name';
+ const BROWSETYPE = 'object';
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $refs = $this->getRefsWithSupportedTypes($refs);
+
+ $name_map = array();
+ foreach ($refs as $key => $ref) {
+ $token = $ref->getToken();
+ if (!strlen($token)) {
+ continue;
+ }
+
+ $name_map[$key] = $token;
+ }
+
+ if (!$name_map) {
+ return array();
+ }
+
+ $objects = $this->resolveCall(
+ 'phid.lookup',
+ array(
+ 'names' => $name_map,
+ ));
+
+ $result = array();
+
+ $reverse_map = array_flip($name_map);
+ foreach ($objects as $name => $object) {
+ $key = idx($reverse_map, $name);
+ if ($key === null) {
+ continue;
+ }
+
+ $uri = idx($object, 'uri');
+ if (!strlen($uri)) {
+ continue;
+ }
+
+ $result[$key][] = id(new ArcanistBrowseURIRef())
+ ->setURI($object['uri'])
+ ->setType('object');
+ }
+
+ return $result;
+ }
+
+}
diff --git a/src/browse/loader/ArcanistBrowsePathURIHardpointLoader.php b/src/browse/loader/ArcanistBrowsePathURIHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/browse/loader/ArcanistBrowsePathURIHardpointLoader.php
@@ -0,0 +1,132 @@
+<?php
+
+final class ArcanistBrowsePathURIHardpointLoader
+ extends ArcanistBrowseURIHardpointLoader {
+
+ const LOADERKEY = 'browse.uri.path';
+ const BROWSETYPE = 'path';
+
+ public function willLoadBrowseURIRefs(array $refs) {
+ $refs = $this->getRefsWithSupportedTypes($refs);
+ if (!$refs) {
+ return;
+ }
+
+ $query = $this->getQuery();
+
+ $working_ref = $query->getWorkingCopyRef();
+ if (!$working_ref) {
+ echo pht(
+ 'NO WORKING COPY: The current directory is not a repository '.
+ 'working copy, so arguments can not be resolved as paths. Run '.
+ 'this command inside a working copy to resolve paths.');
+ echo "\n";
+ return;
+ }
+
+ $repository_ref = $query->getRepositoryRef();
+ if (!$repository_ref) {
+ echo pht(
+ 'NO REPOSITORY: Unable to determine which repository this working '.
+ 'copy belongs to, so arguments can not be resolved as paths. Use '.
+ '"%s" to understand how repositories are resolved.',
+ 'arc which');
+ echo "\n";
+ return;
+ }
+ }
+
+ public function didFailToLoadBrowseURIRefs(array $refs) {
+ $refs = $this->getRefsWithSupportedTypes($refs);
+ if (!$refs) {
+ return;
+ }
+
+ $query = $this->getQuery();
+
+ $working_ref = $query->getWorkingCopyRef();
+ if (!$working_ref) {
+ return;
+ }
+
+ $repository_ref = $query->getRepositoryRef();
+ if (!$repository_ref) {
+ return;
+ }
+
+ echo pht(
+ 'Use "--types path" to force arguments to be interpreted as paths.');
+ echo "\n";
+ }
+
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $query = $this->getQuery();
+
+ $working_ref = $query->getWorkingCopyRef();
+ if (!$working_ref) {
+ return array();
+ }
+
+ $repository_ref = $query->getRepositoryRef();
+ if (!$repository_ref) {
+ return array();
+ }
+
+ $refs = $this->getRefsWithSupportedTypes($refs);
+ $project_root = $working_ref->getRootDirectory();
+
+ $results = array();
+ foreach ($refs as $key => $ref) {
+ $is_path = $ref->hasType(self::BROWSETYPE);
+
+ $path = $ref->getToken();
+ if ($path === null) {
+ // If we're explicitly resolving no arguments as a path, treat it
+ // as the current working directory.
+ if ($is_path) {
+ $path = '.';
+ } else {
+ continue;
+ }
+ }
+
+ $lines = null;
+ $parts = explode(':', $path);
+ if (count($parts) > 1) {
+ $lines = array_pop($parts);
+ }
+ $path = implode(':', $parts);
+
+ $full_path = Filesystem::resolvePath($path);
+
+ if (!Filesystem::pathExists($full_path)) {
+ if (!$is_path) {
+ continue;
+ }
+ }
+
+ if ($full_path == $project_root) {
+ $path = '';
+ } else {
+ $path = Filesystem::readablePath($full_path, $project_root);
+ }
+
+ $params = array(
+ 'path' => $path,
+ 'lines' => $lines,
+ 'branch' => $ref->getBranch(),
+ );
+
+ $uri = $repository_ref->newBrowseURI($params);
+
+ $results[$key][] = id(new ArcanistBrowseURIRef())
+ ->setURI($uri)
+ ->setType(self::BROWSETYPE);
+ }
+
+ return $results;
+ }
+
+
+}
diff --git a/src/browse/loader/ArcanistBrowseRevisionURIHardpointLoader.php b/src/browse/loader/ArcanistBrowseRevisionURIHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/browse/loader/ArcanistBrowseRevisionURIHardpointLoader.php
@@ -0,0 +1,78 @@
+<?php
+
+final class ArcanistBrowseRevisionURIHardpointLoader
+ extends ArcanistBrowseURIHardpointLoader {
+
+ const LOADERKEY = 'browse.uri.revision';
+ const BROWSETYPE = 'revision';
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $query = $this->getQuery();
+
+ $working_ref = $query->getWorkingCopyRef();
+ if (!$working_ref) {
+ return array();
+ }
+
+ $repository_ref = $query->getRepositoryRef();
+ if (!$repository_ref) {
+ return array();
+ }
+
+ $refs = $this->getRefsWithSupportedTypes($refs);
+ if (!$refs) {
+ return array();
+ }
+
+ $this->newQuery($refs)
+ ->needHardpoints(
+ array(
+ 'commitRefs',
+ ))
+ ->execute();
+
+ $states = array();
+ $map = array();
+ foreach ($refs as $key => $ref) {
+ foreach ($ref->getCommitRefs() as $commit_ref) {
+ $hash = $commit_ref->getCommitHash();
+ $states[$hash] = id(clone $working_ref)
+ ->setCommitRef($commit_ref);
+ $map[$hash][] = $key;
+ }
+ }
+
+ if (!$states) {
+ return array();
+ }
+
+ $this->newQuery($states)
+ ->needHardpoints(
+ array(
+ 'revisionRefs',
+ ))
+ ->execute();
+
+ $results = array();
+ foreach ($states as $hash => $state) {
+ foreach ($state->getRevisionRefs() as $revision) {
+ if ($revision->isClosed()) {
+ // Don't resolve closed revisions.
+ continue;
+ }
+
+ $uri = $revision->getURI();
+
+ foreach ($map[$hash] as $key) {
+ $results[$key][] = id(new ArcanistBrowseURIRef())
+ ->setURI($uri)
+ ->setType(self::BROWSETYPE);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+
+}
diff --git a/src/browse/loader/ArcanistBrowseURIHardpointLoader.php b/src/browse/loader/ArcanistBrowseURIHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/browse/loader/ArcanistBrowseURIHardpointLoader.php
@@ -0,0 +1,55 @@
+<?php
+
+abstract class ArcanistBrowseURIHardpointLoader
+ extends ArcanistHardpointLoader {
+
+ public function getSupportedBrowseType() {
+ return $this->getPhobjectClassConstant('BROWSETYPE', 32);
+ }
+
+ public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
+ return true;
+ }
+
+ public function canLoadRef(ArcanistRef $ref) {
+ return ($ref instanceof ArcanistBrowseRef);
+ }
+
+ public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
+ return ($hardpoint == 'uris');
+ }
+
+ public function willLoadBrowseURIRefs(array $refs) {
+ return;
+ }
+
+ public function didFailToLoadBrowseURIRefs(array $refs) {
+ return;
+ }
+
+ public function getRefsWithSupportedTypes(array $refs) {
+ $type = $this->getSupportedBrowseType();
+
+ foreach ($refs as $key => $ref) {
+ if ($ref->isUntyped()) {
+ continue;
+ }
+
+ if ($ref->hasType($type)) {
+ continue;
+ }
+
+ unset($refs[$key]);
+ }
+
+ return $refs;
+ }
+
+ public static function getAllBrowseLoaders() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getLoaderKey')
+ ->execute();
+ }
+
+}
diff --git a/src/browse/ref/ArcanistBrowseRef.php b/src/browse/ref/ArcanistBrowseRef.php
new file mode 100644
--- /dev/null
+++ b/src/browse/ref/ArcanistBrowseRef.php
@@ -0,0 +1,72 @@
+<?php
+
+final class ArcanistBrowseRef
+ extends ArcanistRef {
+
+ private $token;
+ private $types;
+ private $branch;
+
+ public function getRefIdentifier() {
+ return pht('Browse Query "%s"', $this->getToken());
+ }
+
+ public function defineHardpoints() {
+ return array(
+ 'commitRefs' => array(
+ 'type' => 'ArcanistCommitRef',
+ 'vector' => true,
+ ),
+ 'uris' => array(
+ 'type' => 'ArcanistBrowseURIRef',
+ 'vector' => true,
+ ),
+ );
+ }
+
+ public function setToken($token) {
+ $this->token = $token;
+ return $this;
+ }
+
+ public function getToken() {
+ return $this->token;
+ }
+
+ public function setTypes(array $types) {
+ $this->types = $types;
+ return $this;
+ }
+
+ public function getTypes() {
+ return $this->types;
+ }
+
+ public function hasType($type) {
+ $map = $this->getTypes();
+ $map = array_fuse($map);
+ return isset($map[$type]);
+ }
+
+ public function isUntyped() {
+ return !$this->types;
+ }
+
+ public function setBranch($branch) {
+ $this->branch = $branch;
+ return $this;
+ }
+
+ public function getBranch() {
+ return $this->branch;
+ }
+
+ public function getURIs() {
+ return $this->getHardpoint('uris');
+ }
+
+ public function getCommitRefs() {
+ return $this->getHardpoint('commitRefs');
+ }
+
+}
diff --git a/src/browse/ref/ArcanistBrowseURIRef.php b/src/browse/ref/ArcanistBrowseURIRef.php
new file mode 100644
--- /dev/null
+++ b/src/browse/ref/ArcanistBrowseURIRef.php
@@ -0,0 +1,35 @@
+<?php
+
+final class ArcanistBrowseURIRef
+ extends ArcanistRef {
+
+ private $uri;
+ private $type;
+
+ public function getRefIdentifier() {
+ return pht('Browse URI "%s"', $this->getURI());
+ }
+
+ public function defineHardpoints() {
+ return array();
+ }
+
+ public function setURI($uri) {
+ $this->uri = $uri;
+ return $this;
+ }
+
+ public function getURI() {
+ return $this->uri;
+ }
+
+ public function setType($type) {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function getType() {
+ return $this->type;
+ }
+
+}
diff --git a/src/browse/workflow/ArcanistBrowseWorkflow.php b/src/browse/workflow/ArcanistBrowseWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/browse/workflow/ArcanistBrowseWorkflow.php
@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * Browse files or objects in the Phabricator web interface.
+ */
+final class ArcanistBrowseWorkflow extends ArcanistWorkflow {
+
+ public function getWorkflowName() {
+ return 'browse';
+ }
+
+ public function getCommandSynopses() {
+ return phutil_console_format(<<<EOTEXT
+ **browse** [__options__] __path__ ...
+ **browse** [__options__] __object__ ...
+EOTEXT
+ );
+ }
+
+ public function getCommandHelp() {
+ return phutil_console_format(<<<EOTEXT
+ Supports: git, hg, svn
+ Open a file or object (like a task or revision) in your web browser.
+
+ $ arc browse README # Open a file in Diffusion.
+ $ arc browse T123 # View a task.
+ $ arc browse HEAD # View a symbolic commit.
+
+ Set the 'browser' value using 'arc set-config' to select a browser. If
+ no browser is set, the command will try to guess which browser to use.
+EOTEXT
+ );
+ }
+
+ public function getArguments() {
+ return array(
+ 'branch' => array(
+ 'param' => 'branch_name',
+ 'help' => pht(
+ 'Default branch name to view on server. Defaults to "%s".',
+ 'master'),
+ ),
+ 'types' => array(
+ 'param' => 'types',
+ 'aliases' => array('type'),
+ 'help' => pht(
+ 'Parse arguments with particular types.'),
+ ),
+ 'force' => array(
+ 'help' => pht(
+ '(DEPRECATED) Obsolete, use "--types path" instead.'),
+ ),
+ '*' => 'targets',
+ );
+ }
+
+ public function desiresWorkingCopy() {
+ return true;
+ }
+
+ public function desiresRepositoryAPI() {
+ return true;
+ }
+
+ public function run() {
+ $conduit = $this->getConduitEngine();
+
+ $console = PhutilConsole::getConsole();
+
+ $targets = $this->getArgument('targets');
+ $targets = array_fuse($targets);
+
+ if (!$targets) {
+ $refs = array(
+ new ArcanistBrowseRef(),
+ );
+ } else {
+ $refs = array();
+ foreach ($targets as $target) {
+ $refs[] = id(new ArcanistBrowseRef())
+ ->setToken($target);
+ }
+ }
+
+ $is_force = $this->getArgument('force');
+ if ($is_force) {
+ // TODO: Remove this completely.
+ $this->writeWarn(
+ pht('DEPRECATED'),
+ pht(
+ 'Argument "--force" for "arc browse" is deprecated. Use '.
+ '"--type %s" instead.',
+ ArcanistBrowsePathURIHardpointLoader::BROWSETYPE));
+ }
+
+ $types = $this->getArgument('types');
+ if ($types !== null) {
+ $types = preg_split('/[\s,]+/', $types);
+ } else {
+ if ($is_force) {
+ $types = array(ArcanistBrowsePathURIHardpointLoader::BROWSETYPE);
+ } else {
+ $types = array();
+ }
+ }
+
+ foreach ($refs as $ref) {
+ $ref->setTypes($types);
+ }
+
+ $branch = $this->getArgument('branch');
+ if ($branch) {
+ foreach ($refs as $ref) {
+ $ref->setBranch($branch);
+ }
+ }
+
+ $loaders = ArcanistBrowseURIHardpointLoader::getAllBrowseLoaders();
+ foreach ($loaders as $key => $loader) {
+ $loaders[$key] = clone $loader;
+ }
+
+ $query = $this->newRefQuery($refs)
+ ->needHardpoints(
+ array(
+ 'uris',
+ ))
+ ->setLoaders($loaders);
+
+ foreach ($loaders as $loader) {
+ $loader->willLoadBrowseURIRefs($refs);
+ }
+
+ $query->execute();
+
+ $zero_hits = array();
+ $open_uris = array();
+ $many_hits = array();
+ foreach ($refs as $ref) {
+ $uris = $ref->getURIs();
+ if (!$uris) {
+ $zero_hits[] = $ref;
+ } else if (count($uris) == 1) {
+ $open_uris[] = $ref;
+ } else {
+ $many_hits[] = $ref;
+ }
+ }
+
+ $pick_map = array();
+ $pick_selection = null;
+ $pick_id = 0;
+ if ($many_hits) {
+ foreach ($many_hits as $ref) {
+ $token = $ref->getToken();
+ if (strlen($token)) {
+ $message = pht('Argument "%s" is ambiguous.', $token);
+ } else {
+ $message = pht('Default behavior is ambiguous.');
+ }
+
+ $this->writeWarn(pht('AMBIGUOUS'), $message);
+ }
+
+ $is_single_ref = (count($refs) == 1);
+
+ $table = id(new PhutilConsoleTable());
+
+ if ($is_single_ref) {
+ $table->addColumn('pick', array('title' => pht('Pick')));
+ } else {
+ $table->addColumn('argument', array('title' => pht('Argument')));
+ }
+
+ $table
+ ->addColumn('type', array('title' => pht('Type')))
+ ->addColumn('uri', array('title' => pht('URI')));
+
+ foreach ($many_hits as $ref) {
+ $token_display = $ref->getToken();
+ if (!strlen($token)) {
+ $token_display = pht('<default>');
+ }
+
+ foreach ($ref->getURIs() as $uri) {
+ ++$pick_id;
+ $pick_map[$pick_id] = $uri;
+
+ $row = array(
+ 'pick' => $pick_id,
+ 'argument' => $token_display,
+ 'type' => $uri->getType(),
+ 'uri' => $uri->getURI(),
+ );
+
+ $table->addRow($row);
+ }
+ }
+
+ $table->draw();
+
+ if ($is_single_ref) {
+ $pick_selection = phutil_console_select(
+ pht('Which URI do you want to open?'),
+ 1,
+ $pick_id);
+ $open_uris[] = $ref;
+ } else {
+ $this->writeInfo(
+ pht('CHOOSE'),
+ pht('Use "--types" to select between alternatives.'));
+ }
+ }
+
+ // If anything failed to resolve, this is also an error.
+ if ($zero_hits) {
+ foreach ($zero_hits as $ref) {
+ $token = $ref->getToken();
+ if ($token === null) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Unable to resolve default browse target.'));
+ } else {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Unable to resolve argument "%s".',
+ $ref->getToken()));
+ }
+ }
+
+ foreach ($loaders as $loader) {
+ $loader->didFailToLoadBrowseURIRefs($refs);
+ }
+ }
+
+ $uris = array();
+ foreach ($open_uris as $ref) {
+ $ref_uris = $ref->getURIs();
+
+ if (count($ref_uris) > 1) {
+ foreach ($ref_uris as $uri_key => $uri) {
+ if ($pick_map[$pick_selection] !== $uri) {
+ unset($ref_uris[$uri_key]);
+ }
+ }
+ }
+
+ $ref_uri = head($ref_uris);
+ $uris[] = $ref_uri->getURI();
+ }
+
+ $this->openURIsInBrowser($uris);
+
+ return 0;
+ }
+
+}
diff --git a/src/conduit/ArcanistConduitCall.php b/src/conduit/ArcanistConduitCall.php
new file mode 100644
--- /dev/null
+++ b/src/conduit/ArcanistConduitCall.php
@@ -0,0 +1,155 @@
+<?php
+
+final class ArcanistConduitCall
+ extends Phobject {
+
+ private $key;
+ private $engine;
+ private $method;
+ private $parameters;
+ private $future;
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function setEngine(ArcanistConduitEngine $engine) {
+ $this->engine = $engine;
+ return $this;
+ }
+
+ public function getEngine() {
+ return $this->engine;
+ }
+
+ public function setMethod($method) {
+ $this->method = $method;
+ return $this;
+ }
+
+ public function getMethod() {
+ return $this->method;
+ }
+
+ public function setParameters(array $parameters) {
+ $this->parameters = $parameters;
+ return $this;
+ }
+
+ public function getParameters() {
+ return $this->parameters;
+ }
+
+ private function newFuture() {
+ if ($this->future) {
+ throw new Exception(
+ pht(
+ 'Call has previously generated a future. Create a '.
+ 'new call object for each API method invocation.'));
+ }
+
+ $method = $this->getMethod();
+ $parameters = $this->getParameters();
+ $future = $this->getEngine()->newFuture($this);
+ $this->future = $future;
+
+ return $this->future;
+ }
+
+ public function resolve() {
+ if (!$this->future) {
+ $this->newFuture();
+ }
+
+ $this->getEngine()->resolveFuture($this->getKey());
+
+ return $this->resolveFuture();
+ }
+
+ private function resolveFuture() {
+ $future = $this->future;
+
+ try {
+ $result = $future->resolve();
+ } catch (ConduitClientException $ex) {
+ switch ($ex->getErrorCode()) {
+ case 'ERR-INVALID-SESSION':
+ if (!$this->getEngine()->getConduitToken()) {
+ $this->raiseLoginRequired();
+ }
+ break;
+ case 'ERR-INVALID-AUTH':
+ $this->raiseInvalidAuth();
+ break;
+ }
+
+ throw $ex;
+ }
+
+ return $result;
+ }
+
+ private function raiseLoginRequired() {
+ $conduit_uri = $this->getEngine()->getConduitURI();
+ $conduit_uri = new PhutilURI($conduit_uri);
+ $conduit_uri->setPath('/');
+
+ $conduit_domain = $conduit_uri->getDomain();
+
+ $block = id(new PhutilConsoleBlock())
+ ->addParagraph(
+ tsprintf(
+ '**<bg:red> %s </bg>**',
+ pht('LOGIN REQUIRED')))
+ ->addParagraph(
+ pht(
+ 'You are trying to connect to a server ("%s") that you do not '.
+ 'have any stored credentials for, but the command you are '.
+ 'running requires authentication.',
+ $conduit_domain))
+ ->addParagraph(
+ pht(
+ 'To login and save credentials for this server, run this '.
+ 'command:'))
+ ->addParagraph(
+ tsprintf(
+ " $ arc install-certificate %s\n",
+ $conduit_uri));
+
+ throw new ArcanistUsageException($block->drawConsoleString());
+ }
+
+ private function raiseInvalidAuth() {
+ $conduit_uri = $this->getEngine()->getConduitURI();
+ $conduit_uri = new PhutilURI($conduit_uri);
+ $conduit_uri->setPath('/');
+
+ $conduit_domain = $conduit_uri->getDomain();
+
+ $block = id(new PhutilConsoleBlock())
+ ->addParagraph(
+ tsprintf(
+ '**<bg:red> %s </bg>**',
+ pht('INVALID CREDENTIALS')))
+ ->addParagraph(
+ pht(
+ 'Your stored credentials for this server ("%s") are not valid.',
+ $conduit_domain))
+ ->addParagraph(
+ pht(
+ 'To login and save valid credentials for this server, run this '.
+ 'command:'))
+ ->addParagraph(
+ tsprintf(
+ " $ arc install-certificate %s\n",
+ $conduit_uri));
+
+ throw new ArcanistUsageException($block->drawConsoleString());
+ }
+
+}
diff --git a/src/conduit/ArcanistConduitEngine.php b/src/conduit/ArcanistConduitEngine.php
new file mode 100644
--- /dev/null
+++ b/src/conduit/ArcanistConduitEngine.php
@@ -0,0 +1,166 @@
+<?php
+
+final class ArcanistConduitEngine
+ extends Phobject {
+
+ private $conduitURI;
+ private $conduitToken;
+
+ private $conduitTimeout;
+ private $basicAuthUser;
+ private $basicAuthPass;
+
+ private $client;
+ private $callKey = 0;
+ private $activeFutures = array();
+ private $resolvedFutures = array();
+
+ public function isCallable() {
+ return ($this->conduitURI !== null);
+ }
+
+ public function setConduitURI($conduit_uri) {
+ $this->conduitURI = $conduit_uri;
+ return $this;
+ }
+
+ public function getConduitURI() {
+ return $this->conduitURI;
+ }
+
+ public function setConduitToken($conduit_token) {
+ $this->conduitToken = $conduit_token;
+ return $this;
+ }
+
+ public function getConduitToken() {
+ return $this->conduitToken;
+ }
+
+ public function setConduitTimeout($conduit_timeout) {
+ $this->conduitTimeout = $conduit_timeout;
+ return $this;
+ }
+
+ public function getConduitTimeout() {
+ return $this->conduitTimeout;
+ }
+
+ public function setBasicAuthUser($basic_auth_user) {
+ $this->basicAuthUser = $basic_auth_user;
+ return $this;
+ }
+
+ public function getBasicAuthUser() {
+ return $this->basicAuthUser;
+ }
+
+ public function setBasicAuthPass($basic_auth_pass) {
+ $this->basicAuthPass = $basic_auth_pass;
+ return $this;
+ }
+
+ public function getBasicAuthPass() {
+ return $this->basicAuthPass;
+ }
+
+ public function newCall($method, array $parameters) {
+ if ($this->conduitURI == null) {
+ $this->raiseURIException();
+ }
+
+ $next_key = ++$this->callKey;
+
+ return id(new ArcanistConduitCall())
+ ->setKey($next_key)
+ ->setEngine($this)
+ ->setMethod($method)
+ ->setParameters($parameters);
+ }
+
+ public function resolveCall($method, array $parameters) {
+ return $this->newCall($method, $parameters)->resolve();
+ }
+
+ public function newFuture(ArcanistConduitCall $call) {
+ $method = $call->getMethod();
+ $parameters = $call->getParameters();
+
+ $future = $this->getClient()->callMethod($method, $parameters);
+ $this->activeFutures[$call->getKey()] = $future;
+ return $future;
+ }
+
+ private function getClient() {
+ if (!$this->client) {
+ $conduit_uri = $this->getConduitURI();
+
+ $client = new ConduitClient($conduit_uri);
+
+ $timeout = $this->getConduitTimeout();
+ if ($timeout) {
+ $client->setTimeout($timeout);
+ }
+
+ $basic_user = $this->getBasicAuthUser();
+ $basic_pass = $this->getBasicAuthPass();
+ if ($basic_user !== null || $basic_pass !== null) {
+ $client->setBasicAuthCredentials($basic_user, $basic_pass);
+ }
+
+ $token = $this->getConduitToken();
+ if ($token) {
+ $client->setConduitToken($this->getConduitToken());
+ }
+ }
+
+ return $client;
+ }
+
+ public function resolveFuture($key) {
+ if (isset($this->resolvedFutures[$key])) {
+ return;
+ }
+
+ if (!isset($this->activeFutures[$key])) {
+ throw new Exception(
+ pht(
+ 'No future with key "%s" is present in pool.',
+ $key));
+ }
+
+ $iterator = new FutureIterator($this->activeFutures);
+ foreach ($iterator as $future_key => $future) {
+ $this->resolvedFutures[$future_key] = $future;
+ unset($this->activeFutures[$future_key]);
+ if ($future_key == $key) {
+ break;
+ }
+ }
+
+ return;
+ }
+
+ private function raiseURIException() {
+ $list = id(new PhutilConsoleList())
+ ->addItem(
+ pht(
+ 'Run in a working copy with "phabricator.uri" set in ".arcconfig".'))
+ ->addItem(
+ pht(
+ 'Set a default URI with `arc set-config default <uri>`.'))
+ ->addItem(
+ pht(
+ 'Specify a URI explicitly with `--conduit-uri=<uri>`.'));
+
+ $block = id(new PhutilConsoleBlock())
+ ->addParagraph(
+ pht(
+ 'This command needs to communicate with Phabricator, but no '.
+ 'Phabricator URI is configured.'))
+ ->addList($list);
+
+ throw new ArcanistUsageException($block->drawConsoleString());
+ }
+
+}
diff --git a/src/configuration/ArcanistSetting.php b/src/configuration/ArcanistSetting.php
new file mode 100644
--- /dev/null
+++ b/src/configuration/ArcanistSetting.php
@@ -0,0 +1,48 @@
+<?php
+
+abstract class ArcanistSetting
+ extends Phobject {
+
+ final public function getSettingKey() {
+ return $this->getPhobjectClassConstant('SETTINGKEY', 32);
+ }
+
+ public function getAliases() {
+ return array();
+ }
+
+ abstract public function getHelp();
+ abstract public function getType();
+
+ public function getExample() {
+ return null;
+ }
+
+ final public function getLegacyDictionary() {
+ $result = array(
+ 'type' => $this->getType(),
+ 'help' => $this->getHelp(),
+ );
+
+ $example = $this->getExample();
+ if ($example !== null) {
+ $result['example'] = $example;
+ }
+
+ $aliases = $this->getAliases();
+ if ($aliases) {
+ $result['legacy'] = head($aliases);
+ }
+
+ return $result;
+ }
+
+ final public static function getAllSettings() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getSettingKey')
+ ->setSortMethod('getSettingKey')
+ ->execute();
+ }
+
+}
diff --git a/src/configuration/ArcanistSettings.php b/src/configuration/ArcanistSettings.php
--- a/src/configuration/ArcanistSettings.php
+++ b/src/configuration/ArcanistSettings.php
@@ -3,7 +3,7 @@
final class ArcanistSettings extends Phobject {
private function getOptions() {
- return array(
+ $legacy_builtins = array(
'default' => array(
'type' => 'string',
'help' => pht(
@@ -80,19 +80,6 @@
'arc land'),
'example' => '"develop"',
),
- 'arc.lint.cache' => array(
- 'type' => 'bool',
- 'help' => pht(
- 'Enable the lint cache by default. When enabled, `%s` attempts to '.
- 'use cached results if possible. Currently, the cache is not always '.
- 'invalidated correctly and may cause `%s` to report incorrect '.
- 'results, particularly while developing linters. This is probably '.
- 'worth enabling only if your linters are very slow.',
- 'arc lint',
- 'arc lint'),
- 'default' => false,
- 'example' => 'false',
- ),
'history.immutable' => array(
'type' => 'bool',
'legacy' => 'immutable_history',
@@ -168,6 +155,16 @@
'Configured command aliases. Use "arc alias" to define aliases.'),
),
);
+
+ $settings = ArcanistSetting::getAllSettings();
+ foreach ($settings as $key => $setting) {
+ $settings[$key] = $setting->getLegacyDictionary();
+ }
+
+ $results = $settings + $legacy_builtins;
+ ksort($results);
+
+ return $results;
}
private function getOption($key) {
diff --git a/src/loader/ArcanistCommitUpstreamHardpointLoader.php b/src/loader/ArcanistCommitUpstreamHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistCommitUpstreamHardpointLoader.php
@@ -0,0 +1,59 @@
+<?php
+
+final class ArcanistCommitUpstreamHardpointLoader
+ extends ArcanistHardpointLoader {
+
+ const LOADERKEY = 'commit.conduit';
+
+ public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
+ return true;
+ }
+
+ public function canLoadRef(ArcanistRef $ref) {
+ return ($ref instanceof ArcanistCommitRef);
+ }
+
+ public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
+ return ($hardpoint == 'upstream');
+ }
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $query = $this->getQuery();
+
+ $repository_ref = $query->getRepositoryRef();
+ if (!$repository_ref) {
+ return array_fill_keys(array_keys($refs), null);
+ }
+ $repository_phid = $repository_ref->getPHID();
+
+ $commit_map = array();
+ foreach ($refs as $key => $ref) {
+ $hash = $ref->getCommitHash();
+ $commit_map[$hash][] = $key;
+ }
+
+ $commit_info = $this->resolveCall(
+ 'diffusion.querycommits',
+ array(
+ 'repositoryPHID' => $repository_phid,
+ 'names' => array_keys($commit_map),
+ ));
+
+ $results = array();
+ foreach ($commit_map as $hash => $keys) {
+ $commit_phid = idx($commit_info['identifierMap'], $hash);
+ if ($commit_phid) {
+ $commit_data = idx($commit_info['data'], $commit_phid);
+ } else {
+ $commit_data = null;
+ }
+
+ foreach ($keys as $key) {
+ $results[$key] = $commit_data;
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/loader/ArcanistGitCommitMessageHardpointLoader.php b/src/loader/ArcanistGitCommitMessageHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistGitCommitMessageHardpointLoader.php
@@ -0,0 +1,41 @@
+<?php
+
+final class ArcanistGitCommitMessageHardpointLoader
+ extends ArcanistGitHardpointLoader {
+
+ const LOADERKEY = 'git.commit.message';
+
+ public function canLoadRef(ArcanistRef $ref) {
+ return ($ref instanceof ArcanistCommitRef);
+ }
+
+ public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
+ return ($hardpoint == 'message');
+ }
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $api = $this->getQuery()->getRepositoryAPI();
+
+
+ $futures = array();
+ foreach ($refs as $ref_key => $ref) {
+ $hash = $ref->getCommitHash();
+
+ $futures[$ref_key] = $api->execFutureLocal(
+ 'log -n1 --format=%C %s --',
+ '%s%n%n%b',
+ $hash);
+ }
+
+ $iterator = $this->newFutureIterator($futures);
+
+ $results = array();
+ foreach ($iterator as $ref_key => $future) {
+ list($stdout) = $future->resolvex();
+ $results[$ref_key] = $stdout;
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/loader/ArcanistGitHardpointLoader.php b/src/loader/ArcanistGitHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistGitHardpointLoader.php
@@ -0,0 +1,10 @@
+<?php
+
+abstract class ArcanistGitHardpointLoader
+ extends ArcanistHardpointLoader {
+
+ public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
+ return ($api instanceof ArcanistGitAPI);
+ }
+
+}
diff --git a/src/loader/ArcanistGitRevisionHardpointLoader.php b/src/loader/ArcanistGitRevisionHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistGitRevisionHardpointLoader.php
@@ -0,0 +1,82 @@
+<?php
+
+final class ArcanistGitRevisionHardpointLoader
+ extends ArcanistGitHardpointLoader {
+
+ const LOADERKEY = 'git.revision';
+
+ public function canLoadRef(ArcanistRef $ref) {
+ return ($ref instanceof ArcanistWorkingCopyStateRef);
+ }
+
+ public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
+ return ($hardpoint == 'revisionRefs');
+ }
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $this->newQuery($refs)
+ ->needHardpoints(
+ array(
+ 'commitRef',
+ ))
+ ->execute();
+
+ $hashes = array();
+ $map = array();
+ foreach ($refs as $ref_key => $ref) {
+ $commit = $ref->getCommitRef();
+
+ $commit_hashes = array();
+
+ $commit_hashes[] = array(
+ 'gtcm',
+ $commit->getCommitHash(),
+ );
+
+ if ($commit->getTreeHash()) {
+ $commit_hashes[] = array(
+ 'gttr',
+ $commit->getTreeHash(),
+ );
+ }
+
+ foreach ($commit_hashes as $hash) {
+ $hashes[] = $hash;
+ $hash_key = $this->getHashKey($hash);
+ $map[$hash_key][$ref_key] = $ref;
+ }
+ }
+
+ $results = array();
+ if ($hashes) {
+ $revisions = $this->resolveCall(
+ 'differential.query',
+ array(
+ 'commitHashes' => $hashes,
+ ));
+
+ foreach ($revisions as $dict) {
+ $revision_hashes = idx($dict, 'hashes');
+ if (!$revision_hashes) {
+ continue;
+ }
+
+ $revision_ref = ArcanistRevisionRef::newFromConduit($dict);
+ foreach ($revision_hashes as $revision_hash) {
+ $hash_key = $this->getHashKey($revision_hash);
+ $state_refs = idx($map, $hash_key, array());
+ foreach ($state_refs as $ref_key => $state_ref) {
+ $results[$ref_key][] = $revision_ref;
+ }
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ private function getHashKey(array $hash) {
+ return $hash[0].':'.$hash[1];
+ }
+
+}
diff --git a/src/loader/ArcanistHardpointLoader.php b/src/loader/ArcanistHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistHardpointLoader.php
@@ -0,0 +1,76 @@
+<?php
+
+abstract class ArcanistHardpointLoader
+ extends Phobject {
+
+ private $query;
+ private $conduitEngine;
+
+ abstract public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api);
+ abstract public function canLoadRef(ArcanistRef $ref);
+ abstract public function canLoadHardpoint(ArcanistRef $ref, $hardpoint);
+ abstract public function loadHardpoints(array $refs, $hardpoint);
+
+ final public function setQuery(ArcanistRefQuery $query) {
+ $this->query = $query;
+ return $this;
+ }
+
+ final public function getQuery() {
+ return $this->query;
+ }
+
+ final public function getConduitEngine() {
+ return $this->getQuery()->getConduitEngine();
+ }
+
+ final protected function newQuery(array $refs) {
+ $result = id(new ArcanistRefQuery())
+ ->setConduitEngine($this->getQuery()->getConduitEngine())
+ ->setRefs($refs);
+
+ $query = $this->getQuery();
+
+ $repository_api = $query->getRepositoryAPI();
+ if ($repository_api) {
+ $result->setRepositoryAPI($repository_api);
+ }
+
+ $repository_ref = $query->getRepositoryRef();
+ if ($repository_ref) {
+ $result->setRepositoryRef($repository_ref);
+ }
+
+ $working_ref = $query->getWorkingCopyRef();
+ if ($working_ref) {
+ $result->setWorkingCopyRef($working_ref);
+ }
+
+ return $result;
+ }
+
+ final public function getLoaderKey() {
+ return $this->getPhobjectClassConstant('LOADERKEY', 64);
+ }
+
+ final public static function getAllLoaders() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getLoaderKey')
+ ->execute();
+ }
+
+ final public function resolveCall($method, array $parameters) {
+ return $this->newCall($method, $parameters)->resolve();
+ }
+
+ final public function newCall($method, array $parameters) {
+ return $this->getConduitEngine()->newCall($method, $parameters);
+ }
+
+ final protected function newFutureIterator(array $futures) {
+ return id(new FutureIterator($futures))
+ ->limit(16);
+ }
+
+}
diff --git a/src/loader/ArcanistMercurialBranchCommitHardpointLoader.php b/src/loader/ArcanistMercurialBranchCommitHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistMercurialBranchCommitHardpointLoader.php
@@ -0,0 +1,49 @@
+<?php
+
+final class ArcanistMercurialBranchCommitHardpointLoader
+ extends ArcanistMercurialHardpointLoader {
+
+ const LOADERKEY = 'hg.branch.commit';
+
+ public function canLoadRef(ArcanistRef $ref) {
+ return ($ref instanceof ArcanistBranchRef);
+ }
+
+ public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
+ return ($hardpoint == 'commitRef');
+ }
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $api = $this->getQuery()->getRepositoryAPI();
+
+ $futures = array();
+ foreach ($refs as $ref_key => $branch) {
+ $branch_name = $branch->getBranchName();
+
+ $futures[$ref_key] = $api->execFutureLocal(
+ 'log -l 1 --template %s -r %s',
+ "{node}\1{date|hgdate}\1{p1node}\1{desc|firstline}\1{desc}",
+ hgsprintf('%s', $branch_name));
+ }
+
+ $results = array();
+
+ $iterator = $this->newFutureIterator($futures);
+ foreach ($iterator as $ref_key => $future) {
+ list($info) = $future->resolvex();
+
+ $fields = explode("\1", trim($info), 5);
+ list($hash, $epoch, $parent, $desc, $text) = $fields;
+
+ $commit_ref = $api->newCommitRef()
+ ->setCommitHash($hash)
+ ->setCommitEpoch((int)$epoch)
+ ->attachMessage($text);
+
+ $results[$ref_key] = $commit_ref;
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/loader/ArcanistMercurialHardpointLoader.php b/src/loader/ArcanistMercurialHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistMercurialHardpointLoader.php
@@ -0,0 +1,10 @@
+<?php
+
+abstract class ArcanistMercurialHardpointLoader
+ extends ArcanistHardpointLoader {
+
+ public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
+ return ($api instanceof ArcanistMercurialAPI);
+ }
+
+}
diff --git a/src/loader/ArcanistMercurialWorkingCopyCommitHardpointLoader.php b/src/loader/ArcanistMercurialWorkingCopyCommitHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistMercurialWorkingCopyCommitHardpointLoader.php
@@ -0,0 +1,36 @@
+<?php
+
+final class ArcanistMercurialWorkingCopyCommitHardpointLoader
+ extends ArcanistMercurialHardpointLoader {
+
+ const LOADERKEY = 'hg.state.commit';
+
+ public function canLoadRef(ArcanistRef $ref) {
+ return ($ref instanceof ArcanistWorkingCopyStateRef);
+ }
+
+ public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
+ return ($hardpoint == 'commitRef');
+ }
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $branch_refs = array();
+ foreach ($refs as $ref_key => $ref) {
+ if ($ref->hasAttachedHardpoint('branchRef')) {
+ $branch_refs[$ref_key] = $ref->getBranchRef();
+ }
+ }
+
+ if ($branch_refs) {
+ $this->newQuery($branch_refs)
+ ->needHardpoints(
+ array(
+ 'commitRef',
+ ))
+ ->execute();
+ }
+
+ return mpull($branch_refs, 'getCommitRef');
+ }
+
+}
diff --git a/src/loader/ArcanistMessageRevisionHardpointLoader.php b/src/loader/ArcanistMessageRevisionHardpointLoader.php
new file mode 100644
--- /dev/null
+++ b/src/loader/ArcanistMessageRevisionHardpointLoader.php
@@ -0,0 +1,82 @@
+<?php
+
+final class ArcanistMessageRevisionHardpointLoader
+ extends ArcanistHardpointLoader {
+
+ const LOADERKEY = 'message.revision';
+
+ public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
+ return true;
+ }
+
+ public function canLoadRef(ArcanistRef $ref) {
+ return ($ref instanceof ArcanistWorkingCopyStateRef);
+ }
+
+ public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
+ return ($hardpoint == 'revisionRefs');
+ }
+
+ public function loadHardpoints(array $refs, $hardpoint) {
+ $this->newQuery($refs)
+ ->needHardpoints(
+ array(
+ 'commitRef',
+ ))
+ ->execute();
+
+ $commit_refs = array();
+ foreach ($refs as $ref) {
+ $commit_refs[] = $ref->getCommitRef();
+ }
+
+ $this->newQuery($commit_refs)
+ ->needHardpoints(
+ array(
+ 'message',
+ ))
+ ->execute();
+
+ $map = array();
+ foreach ($refs as $ref_key => $ref) {
+ $commit_ref = $ref->getCommitRef();
+ $corpus = $commit_ref->getMessage();
+
+ $id = null;
+ try {
+ $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($corpus);
+ $id = $message->getRevisionID();
+ } catch (ArcanistUsageException $ex) {
+ continue;
+ }
+
+ if (!$id) {
+ continue;
+ }
+
+ $map[$id][$ref_key] = $ref;
+ }
+
+ $results = array();
+ if ($map) {
+ $revisions = $this->resolveCall(
+ 'differential.query',
+ array(
+ 'ids' => array_keys($map),
+ ));
+
+ foreach ($revisions as $dict) {
+ $revision_ref = ArcanistRevisionRef::newFromConduit($dict);
+ $id = $dict['id'];
+
+ $state_refs = idx($map, $id, array());
+ foreach ($state_refs as $ref_key => $state_ref) {
+ $results[$ref_key][] = $revision_ref;
+ }
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/ref/ArcanistBranchRef.php b/src/ref/ArcanistBranchRef.php
new file mode 100644
--- /dev/null
+++ b/src/ref/ArcanistBranchRef.php
@@ -0,0 +1,57 @@
+<?php
+
+final class ArcanistBranchRef
+ extends ArcanistRef {
+
+ private $branchName;
+ private $refName;
+ private $isCurrentBranch;
+
+ public function getRefIdentifier() {
+ return pht('Branch %s', $this->getBranchName());
+ }
+
+ public function defineHardpoints() {
+ return array(
+ 'commitRef' => array(
+ 'type' => 'ArcanistCommitRef',
+ ),
+ );
+ }
+
+ public function setBranchName($branch_name) {
+ $this->branchName = $branch_name;
+ return $this;
+ }
+
+ public function getBranchName() {
+ return $this->branchName;
+ }
+
+ public function setRefName($ref_name) {
+ $this->refName = $ref_name;
+ return $this;
+ }
+
+ public function getRefName() {
+ return $this->refName;
+ }
+
+ public function setIsCurrentBranch($is_current_branch) {
+ $this->isCurrentBranch = $is_current_branch;
+ return $this;
+ }
+
+ public function getIsCurrentBranch() {
+ return $this->isCurrentBranch;
+ }
+
+ public function attachCommitRef(ArcanistCommitRef $ref) {
+ return $this->attachHardpoint('commitRef', $ref);
+ }
+
+ public function getCommitRef() {
+ return $this->getHardpoint('commitRef');
+ }
+
+}
diff --git a/src/ref/ArcanistCommitRef.php b/src/ref/ArcanistCommitRef.php
new file mode 100644
--- /dev/null
+++ b/src/ref/ArcanistCommitRef.php
@@ -0,0 +1,94 @@
+<?php
+
+final class ArcanistCommitRef
+ extends ArcanistRef {
+
+ private $commitHash;
+ private $treeHash;
+ private $commitEpoch;
+ private $authorEpoch;
+ private $upstream;
+
+ public function getRefIdentifier() {
+ return pht('Commit %s', $this->getCommitHash());
+ }
+
+ public function defineHardpoints() {
+ return array(
+ 'message' => array(
+ 'type' => 'string',
+ ),
+ 'upstream' => array(
+ 'type' => 'wild',
+ ),
+ );
+ }
+
+ public function setCommitHash($commit_hash) {
+ $this->commitHash = $commit_hash;
+ return $this;
+ }
+
+ public function getCommitHash() {
+ return $this->commitHash;
+ }
+
+ public function setTreeHash($tree_hash) {
+ $this->treeHash = $tree_hash;
+ return $this;
+ }
+
+ public function getTreeHash() {
+ return $this->treeHash;
+ }
+
+ public function setCommitEpoch($commit_epoch) {
+ $this->commitEpoch = $commit_epoch;
+ return $this;
+ }
+
+ public function getCommitEpoch() {
+ return $this->commitEpoch;
+ }
+
+ public function setAuthorEpoch($author_epoch) {
+ $this->authorEpoch = $author_epoch;
+ return $this;
+ }
+
+ public function getAuthorEpoch() {
+ return $this->authorEpoch;
+ }
+
+ public function getSummary() {
+ $message = $this->getMessage();
+
+ $message = trim($message);
+ $lines = phutil_split_lines($message, false);
+
+ return head($lines);
+ }
+
+ public function attachMessage($message) {
+ return $this->attachHardpoint('message', $message);
+ }
+
+ public function getMessage() {
+ return $this->getHardpoint('message');
+ }
+
+ public function getURI() {
+ return $this->getUpstreamProperty('uri');
+ }
+
+ private function getUpstreamProperty($key, $default = null) {
+ $upstream = $this->getHardpoint('upstream');
+
+ if (!$upstream) {
+ return $default;
+ }
+
+ return idx($upstream, $key, $default);
+ }
+
+}
diff --git a/src/ref/ArcanistRef.php b/src/ref/ArcanistRef.php
new file mode 100644
--- /dev/null
+++ b/src/ref/ArcanistRef.php
@@ -0,0 +1,106 @@
+<?php
+
+abstract class ArcanistRef
+ extends Phobject {
+
+ private $hardpoints = array();
+
+ abstract public function getRefIdentifier();
+ abstract public function defineHardpoints();
+
+ final public function hasHardpoint($hardpoint) {
+ $map = $this->getHardpointMap();
+ return isset($map[$hardpoint]);
+ }
+
+ final public function hasAttachedHardpoint($hardpoint) {
+ if (array_key_exists($hardpoint, $this->hardpoints)) {
+ return true;
+ }
+
+ return $this->canReadHardpoint($hardpoint);
+ }
+
+ final public function attachHardpoint($hardpoint, $value) {
+ if (!$this->hasHardpoint($hardpoint)) {
+ throw new Exception(pht('No hardpoint "%s".', $hardpoint));
+ }
+
+ $this->hardpoints[$hardpoint] = $value;
+
+ return $this;
+ }
+
+ final public function appendHardpoint($hardpoint, array $value) {
+ if (!$this->isVectorHardpoint($hardpoint)) {
+ throw new Exception(
+ pht(
+ 'Hardpoint "%s" is not a vector hardpoint.',
+ $hardpoint));
+ }
+
+ if (!isset($this->hardpoints[$hardpoint])) {
+ $this->hardpoints[$hardpoint] = array();
+ }
+
+ $this->hardpoints[$hardpoint] = $this->mergeHardpoint(
+ $hardpoint,
+ $this->hardpoints[$hardpoint],
+ $value);
+
+ return $this;
+ }
+
+ protected function mergeHardpoint($hardpoint, array $src, array $new) {
+ foreach ($new as $value) {
+ $src[] = $value;
+ }
+ return $src;
+ }
+
+ final public function isVectorHardpoint($hardpoint) {
+ if (!$this->hasHardpoint($hardpoint)) {
+ return false;
+ }
+
+ $map = $this->getHardpointMap();
+ $spec = idx($map, $hardpoint, array());
+
+ return (idx($spec, 'vector') === true);
+ }
+
+ final public function getHardpoint($hardpoint) {
+ if (!$this->hasAttachedHardpoint($hardpoint)) {
+ if (!$this->hasHardpoint($hardpoint)) {
+ throw new Exception(
+ pht(
+ 'Ref does not have hardpoint "%s"!',
+ $hardpoint));
+ } else {
+ throw new Exception(
+ pht(
+ 'Hardpoint "%s" is not attached!',
+ $hardpoint));
+ }
+ }
+
+ if (array_key_exists($hardpoint, $this->hardpoints)) {
+ return $this->hardpoints[$hardpoint];
+ }
+
+ return $this->readHardpoint($hardpoint);
+ }
+
+ private function getHardpointMap() {
+ return $this->defineHardpoints();
+ }
+
+ protected function canReadHardpoint($hardpoint) {
+ return false;
+ }
+
+ protected function readHardpoint($hardpoint) {
+ throw new Exception(pht('Can not read hardpoint "%s".', $hardpoint));
+ }
+
+}
diff --git a/src/ref/ArcanistRefQuery.php b/src/ref/ArcanistRefQuery.php
new file mode 100644
--- /dev/null
+++ b/src/ref/ArcanistRefQuery.php
@@ -0,0 +1,188 @@
+<?php
+
+final class ArcanistRefQuery extends Phobject {
+
+ private $repositoryAPI;
+ private $conduitEngine;
+ private $repositoryRef;
+ private $workingCopyRef;
+
+ private $refs;
+ private $hardpoints;
+ private $loaders;
+
+ public function setRefs(array $refs) {
+ assert_instances_of($refs, 'ArcanistRef');
+ $this->refs = $refs;
+ return $this;
+ }
+
+ public function getRefs() {
+ return $this->refs;
+ }
+
+ public function setRepositoryAPI(ArcanistRepositoryAPI $repository_api) {
+ $this->repositoryAPI = $repository_api;
+ return $this;
+ }
+
+ public function getRepositoryAPI() {
+ return $this->repositoryAPI;
+ }
+
+ public function setRepositoryRef(ArcanistRepositoryRef $repository_ref) {
+ $this->repositoryRef = $repository_ref;
+ return $this;
+ }
+ public function getRepositoryRef() {
+ return $this->repositoryRef;
+ }
+
+ public function setConduitEngine(ArcanistConduitEngine $conduit_engine) {
+ $this->conduitEngine = $conduit_engine;
+ return $this;
+ }
+
+ public function getConduitEngine() {
+ return $this->conduitEngine;
+ }
+
+ public function setWorkingCopyRef(ArcanistWorkingCopyStateRef $working_ref) {
+ $this->workingCopyRef = $working_ref;
+ return $this;
+ }
+
+ public function getWorkingCopyRef() {
+ return $this->workingCopyRef;
+ }
+
+ public function needHardpoints(array $hardpoints) {
+ $this->hardpoints = $hardpoints;
+ return $this;
+ }
+
+ public function setLoaders(array $loaders) {
+ assert_instances_of($loaders, 'ArcanistHardpointLoader');
+
+ foreach ($loaders as $key => $loader) {
+ $loader->setQuery($this);
+ }
+ $this->loaders = $loaders;
+
+ return $this;
+ }
+
+ public function execute() {
+ $refs = $this->getRefs();
+
+ if ($this->refs === null) {
+ throw new PhutilInvalidStateException('setRefs');
+ }
+
+ if ($this->hardpoints === null) {
+ throw new PhutilInvalidStateException('needHardpoints');
+ }
+
+ if ($this->loaders == null) {
+ $all_loaders = ArcanistHardpointLoader::getAllLoaders();
+ foreach ($all_loaders as $key => $loader) {
+ $all_loaders[$key] = clone $loader;
+ }
+ $this->setLoaders($all_loaders);
+ }
+
+ $all_loaders = $this->loaders;
+
+ $api = $this->getRepositoryAPI();
+ $loaders = array();
+ foreach ($all_loaders as $loader_key => $loader) {
+ if ($api) {
+ if (!$loader->canLoadRepositoryAPI($api)) {
+ continue;
+ }
+ }
+
+ $loaders[$loader_key] = id(clone $loader)
+ ->setQuery($this);
+ }
+
+ foreach ($this->hardpoints as $hardpoint) {
+ $load = array();
+ $need = array();
+ $has_hardpoint = false;
+ foreach ($refs as $ref_key => $ref) {
+ if (!$ref->hasHardpoint($hardpoint)) {
+ continue;
+ }
+
+ $has_hardpoint = true;
+
+ if ($ref->hasAttachedHardpoint($hardpoint)) {
+ continue;
+ }
+
+ foreach ($loaders as $loader_key => $loader) {
+ if (!$loader->canLoadRef($ref)) {
+ continue;
+ }
+
+ if (!$loader->canLoadHardpoint($ref, $hardpoint)) {
+ continue;
+ }
+
+ $load[$loader_key][$ref_key] = $ref;
+ }
+
+ $need[$ref_key] = $ref_key;
+ }
+
+ if ($refs && !$has_hardpoint) {
+ throw new Exception(
+ pht(
+ 'No ref in query has hardpoint "%s".',
+ $hardpoint));
+ }
+
+ $vectors = array();
+ foreach ($need as $ref_key) {
+ $ref = $refs[$ref_key];
+ if ($ref->isVectorHardpoint($hardpoint)) {
+ $vectors[$ref_key] = $ref_key;
+ $ref->attachHardpoint($hardpoint, array());
+ }
+ }
+
+ foreach ($load as $loader_key => $loader_refs) {
+ $loader_refs = array_select_keys($loader_refs, $need);
+
+ $loader = $loaders[$loader_key];
+ $data = $loader->loadHardpoints($loader_refs, $hardpoint);
+
+ foreach ($data as $ref_key => $value) {
+ $ref = $refs[$ref_key];
+ if (isset($vectors[$ref_key])) {
+ $ref->appendHardpoint($hardpoint, $value);
+ } else {
+ unset($need[$ref_key]);
+ $ref->attachHardpoint($hardpoint, $value);
+ }
+ }
+ }
+
+ foreach ($vectors as $ref_key) {
+ unset($need[$ref_key]);
+ }
+
+ if ($need) {
+ throw new Exception(
+ pht(
+ 'Nothing could attach data to hardpoint "%s" for ref "%s".',
+ $hardpoint,
+ $refs[head($need)]->getRefIdentifier()));
+ }
+ }
+
+ return $refs;
+ }
+
+}
diff --git a/src/ref/ArcanistRepositoryRef.php b/src/ref/ArcanistRepositoryRef.php
new file mode 100644
--- /dev/null
+++ b/src/ref/ArcanistRepositoryRef.php
@@ -0,0 +1,79 @@
+<?php
+
+final class ArcanistRepositoryRef
+ extends ArcanistRef {
+
+ private $phid;
+ private $browseURI;
+
+ public function getRefIdentifier() {
+ return pht('Remote Repository');
+ }
+
+ public function defineHardpoints() {
+ return array();
+ }
+
+ public function setPHID($phid) {
+ $this->phid = $phid;
+ return $this;
+ }
+
+ public function getPHID() {
+ return $this->phid;
+ }
+
+ public function setBrowseURI($browse_uri) {
+ $this->browseURI = $browse_uri;
+ return $this;
+ }
+
+ public function newBrowseURI(array $params) {
+ PhutilTypeSpec::checkMap(
+ $params,
+ array(
+ 'path' => 'optional string|null',
+ 'branch' => 'optional string|null',
+ 'lines' => 'optional string|null',
+ ));
+
+ foreach ($params as $key => $value) {
+ if (!strlen($value)) {
+ unset($params[$key]);
+ }
+ }
+
+ $defaults = array(
+ 'path' => '/',
+ 'branch' => $this->getDefaultBranch(),
+ 'lines' => null,
+ );
+
+ $params = $params + $defaults;
+
+ $uri_base = $this->browseURI;
+ $uri_base = rtrim($uri_base, '/');
+
+ $uri_branch = phutil_escape_uri_path_component($params['branch']);
+
+ $uri_path = ltrim($params['path'], '/');
+ $uri_path = phutil_escape_uri($uri_path);
+
+ $uri_lines = null;
+ if ($params['lines']) {
+ $uri_lines = '$'.phutil_escape_uri($params['lines']);
+ }
+
+ // TODO: This construction, which includes a branch, is probably wrong for
+ // Subversion.
+
+ return "{$uri_base}/browse/{$uri_branch}/{$uri_path}{$uri_lines}";
+ }
+
+ public function getDefaultBranch() {
+ // TODO: This should read from the remote, and is not correct for
+ // Mercurial anyway, as "default" would be a better default branch.
+ return 'master';
+ }
+
+}
diff --git a/src/ref/ArcanistRevisionRef.php b/src/ref/ArcanistRevisionRef.php
new file mode 100644
--- /dev/null
+++ b/src/ref/ArcanistRevisionRef.php
@@ -0,0 +1,72 @@
+<?php
+
+final class ArcanistRevisionRef
+ extends ArcanistRef {
+
+ private $parameters;
+ private $sources = array();
+
+ public function getRefIdentifier() {
+ return pht('Revision %s', $this->getMonogram());
+ }
+
+ public function defineHardpoints() {
+ return array();
+ }
+
+ public static function newFromConduit(array $dict) {
+ $ref = new self();
+ $ref->parameters = $dict;
+ return $ref;
+ }
+
+ public function getMonogram() {
+ return 'D'.$this->getID();
+ }
+
+ public function getStatusDisplayName() {
+ return idx($this->parameters, 'statusName');
+ }
+
+ public function isClosed() {
+ // TODO: This should use sensible constants, not English language
+ // display text.
+ switch ($this->getStatusDisplayName()) {
+ case 'Abandoned':
+ case 'Closed':
+ return true;
+ }
+
+ return false;
+ }
+
+ public function getURI() {
+ return idx($this->parameters, 'uri');
+ }
+
+ public function getFullName() {
+ return pht('%s: %s', $this->getMonogram(), $this->getName());
+ }
+
+ public function getID() {
+ return idx($this->parameters, 'id');
+ }
+
+ public function getName() {
+ return idx($this->parameters, 'title');
+ }
+
+ public function getAuthorPHID() {
+ return idx($this->parameters, 'authorPHID');
+ }
+
+ public function addSource(ArcanistRevisionRefSource $source) {
+ $this->sources[] = $source;
+ return $this;
+ }
+
+ public function getSources() {
+ return $this->sources;
+ }
+
+}
diff --git a/src/ref/ArcanistRevisionRefSource.php b/src/ref/ArcanistRevisionRefSource.php
new file mode 100644
--- /dev/null
+++ b/src/ref/ArcanistRevisionRefSource.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class ArcanistRevisionRefSource
+ extends Phobject {}
diff --git a/src/ref/ArcanistWorkingCopyStateRef.php b/src/ref/ArcanistWorkingCopyStateRef.php
new file mode 100644
--- /dev/null
+++ b/src/ref/ArcanistWorkingCopyStateRef.php
@@ -0,0 +1,122 @@
+<?php
+
+final class ArcanistWorkingCopyStateRef
+ extends ArcanistRef {
+
+ private $rootDirectory;
+
+ public function getRefIdentifier() {
+ // TODO: This could check attached hardpoints and render something more
+ // insightful.
+ return pht('Working Copy State');
+ }
+
+ public function defineHardpoints() {
+ return array(
+ 'commitRef' => array(
+ 'type' => 'ArcanistCommitRef',
+ ),
+ 'branchRef' => array(
+ 'type' => 'ArcanistBranchRef',
+ ),
+ 'revisionRefs' => array(
+ 'type' => 'ArcanistRevisionRef',
+ 'vector' => true,
+ ),
+ );
+ }
+
+ public function setRootDirectory($root_directory) {
+ $this->rootDirectory = $root_directory;
+ return $this;
+ }
+
+ public function getRootDirectory() {
+ return $this->rootDirectory;
+ }
+
+ public function attachBranchRef(ArcanistBranchRef $branch_ref) {
+ return $this->attachHardpoint('branchRef', $branch_ref);
+ }
+
+ public function getBranchRef() {
+ return $this->getHardpoint('branchRef');
+ }
+
+ public function setCommitRef(ArcanistCommitRef $commit_ref) {
+ return $this->attachHardpoint('commitRef', $commit_ref);
+ }
+
+ public function getCommitRef() {
+ return $this->getHardpoint('commitRef');
+ }
+
+ public function getRevisionRefs() {
+ return $this->getHardpoint('revisionRefs');
+ }
+
+ public function getRevisionRef() {
+ if ($this->hasAmbiguousRevisionRefs()) {
+ throw new Exception(
+ pht('State has multiple ambiguous revisions refs.'));
+ }
+
+ $refs = $this->getRevisionRefs();
+ if ($refs) {
+ return head($refs);
+ }
+
+ return null;
+ }
+
+ public function hasAmbiguousRevisionRefs() {
+ return (count($this->getRevisionRefs()) > 1);
+ }
+
+ protected function canReadHardpoint($hardpoint) {
+ switch ($hardpoint) {
+ case 'commitRef':
+ // If we have a branch ref, we can try to read the commit ref from the
+ // branch ref.
+ if ($this->hasAttachedHardpoint('branchRef')) {
+ if ($this->getBranchRef()->hasAttachedHardpoint('commitRef')) {
+ return true;
+ }
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ protected function readHardpoint($hardpoint) {
+ switch ($hardpoint) {
+ case 'commitRef':
+ return $this->getBranchRef()->getCommitRef();
+ }
+
+ return parent::readHardpoint($hardpoint);
+ }
+
+ protected function mergeHardpoint($hardpoint, array $src, array $new) {
+ if ($hardpoint == 'revisionRefs') {
+ $src = mpull($src, null, 'getID');
+ $new = mpull($new, null, 'getID');
+
+ foreach ($new as $id => $ref) {
+ if (isset($src[$id])) {
+ foreach ($ref->getSources() as $source) {
+ $src[$id]->addSource($source);
+ }
+ } else {
+ $src[$id] = $ref;
+ }
+ }
+
+ return array_values($src);
+ }
+
+ return parent::mergeHardpoint($hardpoint, $src, $new);
+ }
+
+}
diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php
--- a/src/repository/api/ArcanistGitAPI.php
+++ b/src/repository/api/ArcanistGitAPI.php
@@ -1090,6 +1090,7 @@
$result[] = array(
'current' => ($branch === $current),
'name' => $branch,
+ 'ref' => $ref,
'hash' => $hash,
'tree' => $tree,
'epoch' => (int)$epoch,
@@ -1102,17 +1103,46 @@
return $result;
}
- public function getWorkingCopyRevision() {
- list($stdout) = $this->execxLocal('rev-parse HEAD');
- return rtrim($stdout, "\n");
+ public function getAllBranchRefs() {
+ $branches = $this->getAllBranches();
+
+ $refs = array();
+ foreach ($branches as $branch) {
+ $commit_ref = $this->newCommitRef()
+ ->setCommitHash($branch['hash'])
+ ->setTreeHash($branch['tree'])
+ ->setCommitEpoch($branch['epoch'])
+ ->attachMessage($branch['text']);
+
+ $refs[] = $this->newBranchRef()
+ ->setBranchName($branch['name'])
+ ->setRefName($branch['ref'])
+ ->setIsCurrentBranch($branch['current'])
+ ->attachCommitRef($commit_ref);
+ }
+
+ return $refs;
}
- public function getUnderlyingWorkingCopyRevision() {
- list($err, $stdout) = $this->execManualLocal('svn find-rev HEAD');
- if (!$err && $stdout) {
- return rtrim($stdout, "\n");
+ public function getBaseCommitRef() {
+ $base_commit = $this->getBaseCommit();
+
+ if ($base_commit === self::GIT_MAGIC_ROOT_COMMIT) {
+ return null;
}
- return $this->getWorkingCopyRevision();
+
+ $base_message = $this->getCommitMessage($base_commit);
+
+ // TODO: We should also pull the tree hash.
+
+ return $this->newCommitRef()
+ ->setCommitHash($base_commit)
+ ->attachMessage($base_message);
+ }
+
+ public function getWorkingCopyRevision() {
+ list($stdout) = $this->execxLocal('rev-parse HEAD');
+ return rtrim($stdout, "\n");
}
public function isHistoryDefaultImmutable() {
diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php
--- a/src/repository/api/ArcanistMercurialAPI.php
+++ b/src/repository/api/ArcanistMercurialAPI.php
@@ -564,6 +564,33 @@
return $return;
}
+ public function getAllBranchRefs() {
+ $branches = $this->getAllBranches();
+
+ $refs = array();
+ foreach ($branches as $branch) {
+ $refs[] = $this->newBranchRef()
+ ->setBranchName($branch['name'])
+ ->setIsCurrentBranch($branch['current']);
+ }
+
+ return $refs;
+ }
+
+ public function getBaseCommitRef() {
+ $base_commit = $this->getBaseCommit();
+
+ if ($base_commit === 'null') {
+ return null;
+ }
+
+ $base_message = $this->getCommitMessage($base_commit);
+
+ return $this->newCommitRef()
+ ->setCommitHash($base_commit)
+ ->attachMessage($base_message);
+ }
+
public function hasLocalCommit($commit) {
try {
$this->getCanonicalRevisionName($commit);
diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php
--- a/src/repository/api/ArcanistRepositoryAPI.php
+++ b/src/repository/api/ArcanistRepositoryAPI.php
@@ -343,11 +343,6 @@
array $query);
abstract public function getRemoteURI();
-
- public function getUnderlyingWorkingCopyRevision() {
- return $this->getWorkingCopyRevision();
- }
-
public function getChangedFiles($since_commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
@@ -375,6 +370,14 @@
return array();
}
+ public function getAllBranchRefs() {
+ throw new ArcanistCapabilityNotSupportedException($this);
+ }
+
+ public function getBaseCommitRef() {
+ throw new ArcanistCapabilityNotSupportedException($this);
+ }
+
public function hasLocalCommit($commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
@@ -668,4 +671,12 @@
return null;
}
+ final public function newCommitRef() {
+ return new ArcanistCommitRef();
+ }
+
+ final public function newBranchRef() {
+ return new ArcanistBranchRef();
+ }
+
}
diff --git a/src/workflow/ArcanistBrowseWorkflow.php b/src/workflow/ArcanistBrowseWorkflow.php
deleted file mode 100644
--- a/src/workflow/ArcanistBrowseWorkflow.php
+++ /dev/null
@@ -1,233 +0,0 @@
-<?php
-
-/**
- * Browse files or objects in the Phabricator web interface.
- */
-final class ArcanistBrowseWorkflow extends ArcanistWorkflow {
-
- public function getWorkflowName() {
- return 'browse';
- }
-
- public function getCommandSynopses() {
- return phutil_console_format(<<<EOTEXT
- **browse** [__options__] __path__ ...
- **browse** [__options__] __object__ ...
-EOTEXT
- );
- }
-
- public function getCommandHelp() {
- return phutil_console_format(<<<EOTEXT
- Supports: git, hg, svn
- Open a file or object (like a task or revision) in your web browser.
-
- $ arc browse README # Open a file in Diffusion.
- $ arc browse T123 # View a task.
- $ arc browse HEAD # View a symbolic commit.
-
- Set the 'browser' value using 'arc set-config' to select a browser. If
- no browser is set, the command will try to guess which browser to use.
-EOTEXT
- );
- }
-
- public function getArguments() {
- return array(
- 'branch' => array(
- 'param' => 'branch_name',
- 'help' => pht(
- 'Default branch name to view on server. Defaults to "%s".',
- 'master'),
- ),
- 'force' => array(
- 'help' => pht(
- 'Open arguments as paths, even if they do not exist in the '.
- 'working copy.'),
- ),
- '*' => 'paths',
- );
- }
-
- public function desiresWorkingCopy() {
- return true;
- }
-
- public function requiresConduit() {
- return true;
- }
-
- public function requiresAuthentication() {
- return true;
- }
-
- public function desiresRepositoryAPI() {
- return true;
- }
-
- public function run() {
- $console = PhutilConsole::getConsole();
-
- $is_force = $this->getArgument('force');
-
- $things = $this->getArgument('paths');
- if (!$things) {
- throw new ArcanistUsageException(
- pht(
- 'Specify one or more paths or objects to browse. Use the command '.
- '"%s" if you want to browse this directory.',
- 'arc browse .'));
- }
- $things = array_fuse($things);
-
- $objects = $this->getConduit()->callMethodSynchronous(
- 'phid.lookup',
- array(
- 'names' => array_keys($things),
- ));
-
- $uris = array();
- foreach ($objects as $name => $object) {
- $uris[] = $object['uri'];
-
- $console->writeOut(
- pht(
- 'Opening **%s** as an object.',
- $name)."\n");
-
- unset($things[$name]);
- }
-
- if ($this->hasRepositoryAPI()) {
- $repository_api = $this->getRepositoryAPI();
- $project_root = $this->getWorkingCopy()->getProjectRoot();
-
- // First, try to resolve arguments as symbolic commits.
-
- $commits = array();
- foreach ($things as $key => $thing) {
- if ($thing == '.') {
- // Git resolves '.' like HEAD, but it should be interpreted to mean
- // "the current directory". Just skip resolution and fall through.
- continue;
- }
-
- try {
- $commit = $repository_api->getCanonicalRevisionName($thing);
- if ($commit) {
- $commits[$commit] = $key;
- }
- } catch (Exception $ex) {
- // Ignore.
- }
- }
-
- if ($commits) {
- $commit_info = $this->getConduit()->callMethodSynchronous(
- 'diffusion.querycommits',
- array(
- 'repositoryPHID' => $this->getRepositoryPHID(),
- 'names' => array_keys($commits),
- ));
-
- foreach ($commit_info['identifierMap'] as $ckey => $cphid) {
- $thing = $commits[$ckey];
- unset($things[$thing]);
-
- $uris[] = $commit_info['data'][$cphid]['uri'];
-
- $console->writeOut(
- pht(
- 'Opening **%s** as a commit.',
- $thing)."\n");
- }
- }
-
- // If we fail, try to resolve them as paths.
-
- foreach ($things as $key => $path) {
- $lines = null;
- $parts = explode(':', $path);
- if (count($parts) > 1) {
- $lines = array_pop($parts);
- }
- $path = implode(':', $parts);
-
- $full_path = Filesystem::resolvePath($path);
-
- if (!$is_force && !Filesystem::pathExists($full_path)) {
- continue;
- }
-
- $console->writeOut(
- pht(
- 'Opening **%s** as a repository path.',
- $key)."\n");
-
- unset($things[$key]);
-
- if ($full_path == $project_root) {
- $path = '';
- } else {
- $path = Filesystem::readablePath($full_path, $project_root);
- }
-
- $base_uri = $this->getBaseURI();
- $uri = $base_uri.$path;
-
- if ($lines) {
- $uri = $uri.'$'.$lines;
- }
-
- $uris[] = $uri;
- }
- } else {
- if ($things) {
- $console->writeOut(
- "%s\n",
- pht(
- "The current working directory is not a repository working ".
- "copy, so remaining arguments can not be resolved as paths or ".
- "commits. To browse paths or symbolic commits in Diffusion, run ".
- "'%s' from inside a working copy.",
- 'arc browse'));
- }
- }
-
- foreach ($things as $thing) {
- $console->writeOut(
- "%s\n",
- pht(
- 'Unable to find an object named **%s**, no such commit exists in '.
- 'the remote, and no such path exists in the working copy. Use '.
- '__%s__ to treat this as a path anyway.',
- $thing,
- '--force'));
- }
-
- if ($uris) {
- $this->openURIsInBrowser($uris);
- }
-
- return 0;
- }
-
- private function getBaseURI() {
- $repo_uri = $this->getRepositoryURI();
- if ($repo_uri === null) {
- throw new ArcanistUsageException(
- pht(
- 'arc is unable to determine which repository in Diffusion '.
- 'this working copy belongs to. Use "%s" to understand how '.
- '%s looks for a repository.',
- 'arc which',
- 'arc'));
- }
-
- $branch = $this->getArgument('branch', 'master');
- $branch = phutil_escape_uri_path_component($branch);
-
- return $repo_uri.'browse/'.$branch.'/';
- }
-
-}
diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php
--- a/src/workflow/ArcanistDiffWorkflow.php
+++ b/src/workflow/ArcanistDiffWorkflow.php
@@ -2017,6 +2017,15 @@
$faux_message[] = pht('CC: %s', $this->getArgument('cc'));
}
+ // NOTE: For now, this isn't a real field, so it just ends up as the first
+ // part of the summary.
+ $depends_ref = $this->getDependsOnRevisionRef();
+ if ($depends_ref) {
+ $faux_message[] = pht(
+ 'Depends on %s. ',
+ $depends_ref->getMonogram());
+ }
+
// See T12069. After T10312, the first line of a message is always parsed
// as a title. Add a placeholder so "Reviewers" and "CC" are never the
// first line.
@@ -3030,4 +3039,46 @@
}
}
+ private function getDependsOnRevisionRef() {
+ $api = $this->getRepositoryAPI();
+ $base_ref = $api->getBaseCommitRef();
+
+ $state_ref = $this->newWorkingCopyStateRef()
+ ->setCommitRef($base_ref);
+
+ $this->newRefQuery(array($state_ref))
+ ->needHardpoints(
+ array(
+ 'revisionRefs',
+ ))
+ ->execute();
+
+ $revision_refs = $state_ref->getRevisionRefs();
+ $viewer_phid = $this->getUserPHID();
+
+ foreach ($revision_refs as $key => $revision_ref) {
+ // Don't automatically depend on closed revisions.
+ if ($revision_ref->isClosed()) {
+ unset($revision_refs[$key]);
+ continue;
+ }
+
+ // Don't automatically depend on revisions authored by other users.
+ if ($revision_ref->getAuthorPHID() != $viewer_phid) {
+ unset($revision_refs[$key]);
+ continue;
+ }
+ }
+
+ if (!$revision_refs) {
+ return null;
+ }
+
+ if (count($revision_refs) > 1) {
+ return null;
+ }
+
+ return head($revision_refs);
+ }
+
}
diff --git a/src/workflow/ArcanistFeatureWorkflow.php b/src/workflow/ArcanistFeatureWorkflow.php
--- a/src/workflow/ArcanistFeatureWorkflow.php
+++ b/src/workflow/ArcanistFeatureWorkflow.php
@@ -38,18 +38,10 @@
);
}
- public function requiresConduit() {
- return true;
- }
-
public function requiresRepositoryAPI() {
return true;
}
- public function requiresAuthentication() {
- return !$this->getArgument('branch');
- }
-
public function getArguments() {
return array(
'view-all' => array(
@@ -86,15 +78,29 @@
return $this->checkoutBranch($names);
}
- $branches = $repository_api->getAllBranches();
+ // TODO: Everything in this whole workflow that says "branch" means
+ // "bookmark" in Mercurial.
+
+ $branches = $repository_api->getAllBranchRefs();
if (!$branches) {
throw new ArcanistUsageException(
pht('No branches in this working copy.'));
}
- $branches = $this->loadCommitInfo($branches);
- $revisions = $this->loadRevisions($branches);
- $this->printBranches($branches, $revisions);
+ $states = array();
+ foreach ($branches as $branch) {
+ $states[] = $this->newWorkingCopyStateRef()
+ ->attachBranchRef($branch);
+ }
+
+ $this->newRefQuery($states)
+ ->needHardpoints(
+ array(
+ 'revisionRefs',
+ ))
+ ->execute();
+
+ $this->printBranches($states);
return 0;
}
@@ -125,21 +131,19 @@
if ($err) {
$match = null;
if (preg_match('/^D(\d+)$/', $name, $match)) {
- try {
- $diff = $this->getConduit()->callMethodSynchronous(
- 'differential.querydiffs',
- array(
- 'revisionIDs' => array($match[1]),
- ));
- $diff = head($diff);
-
- if ($diff['branch'] != '') {
- $name = $diff['branch'];
- list($err, $stdout, $stderr) = $api->execManualLocal(
- $command,
- $name);
- }
- } catch (ConduitClientException $ex) {}
+ $diff = $this->getConduitEngine()->resolveCall(
+ 'differential.querydiffs',
+ array(
+ 'revisionIDs' => array($match[1]),
+ ));
+ $diff = head($diff);
+
+ if ($diff['branch'] != '') {
+ $name = $diff['branch'];
+ list($err, $stdout, $stderr) = $api->execManualLocal(
+ $command,
+ $name);
+ }
}
}
@@ -171,99 +175,7 @@
return $err;
}
- private function loadCommitInfo(array $branches) {
- $repository_api = $this->getRepositoryAPI();
-
- $branches = ipull($branches, null, 'name');
-
- if ($repository_api instanceof ArcanistMercurialAPI) {
- $futures = array();
- foreach ($branches as $branch) {
- $futures[$branch['name']] = $repository_api->execFutureLocal(
- 'log -l 1 --template %s -r %s',
- "{node}\1{date|hgdate}\1{p1node}\1{desc|firstline}\1{desc}",
- hgsprintf('%s', $branch['name']));
- }
-
- $futures = id(new FutureIterator($futures))
- ->limit(16);
- foreach ($futures as $name => $future) {
- list($info) = $future->resolvex();
-
- $fields = explode("\1", trim($info), 5);
- list($hash, $epoch, $tree, $desc, $text) = $fields;
-
- $branches[$name] += array(
- 'hash' => $hash,
- 'desc' => $desc,
- 'tree' => $tree,
- 'epoch' => (int)$epoch,
- 'text' => $text,
- );
- }
- }
-
- foreach ($branches as $name => $branch) {
- $text = $branch['text'];
-
- try {
- $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
- $id = $message->getRevisionID();
-
- $branch['revisionID'] = $id;
- } catch (ArcanistUsageException $ex) {
- // In case of invalid commit message which fails the parsing,
- // do nothing.
- $branch['revisionID'] = null;
- }
-
- $branches[$name] = $branch;
- }
-
- return $branches;
- }
-
- private function loadRevisions(array $branches) {
- $ids = array();
- $hashes = array();
-
- foreach ($branches as $branch) {
- if ($branch['revisionID']) {
- $ids[] = $branch['revisionID'];
- }
- $hashes[] = array('gtcm', $branch['hash']);
- $hashes[] = array('gttr', $branch['tree']);
- }
-
- $calls = array();
-
- if ($ids) {
- $calls[] = $this->getConduit()->callMethod(
- 'differential.query',
- array(
- 'ids' => $ids,
- ));
- }
-
- if ($hashes) {
- $calls[] = $this->getConduit()->callMethod(
- 'differential.query',
- array(
- 'commitHashes' => $hashes,
- ));
- }
-
- $results = array();
- foreach (new FutureIterator($calls) as $call) {
- $results[] = $call->resolve();
- }
-
- return array_mergev($results);
- }
-
- private function printBranches(array $branches, array $revisions) {
- $revisions = ipull($revisions, null, 'id');
-
+ private function printBranches(array $states) {
static $color_map = array(
'Closed' => 'cyan',
'Needs Review' => 'magenta',
@@ -282,48 +194,45 @@
);
$out = array();
- foreach ($branches as $branch) {
- $revision = idx($revisions, idx($branch, 'revisionID'));
-
- // If we haven't identified a revision by ID, try to identify it by hash.
- if (!$revision) {
- foreach ($revisions as $rev) {
- $hashes = idx($rev, 'hashes', array());
- foreach ($hashes as $hash) {
- if (($hash[0] == 'gtcm' && $hash[1] == $branch['hash']) ||
- ($hash[0] == 'gttr' && $hash[1] == $branch['tree'])) {
- $revision = $rev;
- break;
- }
- }
- }
- }
+ foreach ($states as $state) {
+ $branch = $state->getBranchRef();
- if ($revision) {
- $desc = 'D'.$revision['id'].': '.$revision['title'];
- $status = $revision['statusName'];
+ $revision = null;
+ if ($state->hasAmbiguousRevisionRefs()) {
+ $status = pht('Ambiguous Revision');
} else {
- $desc = $branch['desc'];
- $status = pht('No Revision');
+ $revision = $state->getRevisionRef();
+ if ($revision) {
+ $status = $revision->getStatusDisplayName();
+ } else {
+ $status = pht('No Revision');
+ }
}
- if (!$this->getArgument('view-all') && !$branch['current']) {
+ if (!$this->getArgument('view-all') && !$branch->getIsCurrentBranch()) {
if ($status == 'Closed' || $status == 'Abandoned') {
continue;
}
}
- $epoch = $branch['epoch'];
+ $commit = $branch->getCommitRef();
+ $epoch = $commit->getCommitEpoch();
$color = idx($color_map, $status, 'default');
$ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch);
+ if ($revision) {
+ $desc = $revision->getFullName();
+ } else {
+ $desc = $commit->getSummary();
+ }
+
$out[] = array(
- 'name' => $branch['name'],
- 'current' => $branch['current'],
+ 'name' => $branch->getBranchName(),
+ 'current' => $branch->getIsCurrentBranch(),
'status' => $status,
'desc' => $desc,
- 'revision' => $revision ? $revision['id'] : null,
+ 'revision' => $revision ? $revision->getID() : null,
'color' => $color,
'esort' => $epoch,
'epoch' => $epoch,
diff --git a/src/workflow/ArcanistUpgradeWorkflow.php b/src/workflow/ArcanistUpgradeWorkflow.php
--- a/src/workflow/ArcanistUpgradeWorkflow.php
+++ b/src/workflow/ArcanistUpgradeWorkflow.php
@@ -29,6 +29,13 @@
'arcanist' => dirname(phutil_get_library_root('arcanist')),
);
+ $supported_branches = array(
+ 'master',
+ 'stable',
+ 'experimental',
+ );
+ $supported_branches = array_fuse($supported_branches);
+
foreach ($roots as $lib => $root) {
echo phutil_console_format(
"%s\n",
@@ -75,18 +82,16 @@
}
$branch_name = $repository->getBranchName();
- if ($branch_name != 'master' && $branch_name != 'stable') {
+ if (!isset($supported_branches[$branch_name])) {
throw new ArcanistUsageException(
pht(
- "%s must be on either branch '%s' or '%s' to be automatically ".
- "upgraded. ".
- "This copy of %s (in '%s') is on branch '%s'.",
- $lib,
- 'master',
- 'stable',
+ 'Library "%s" (in "%s") is on branch "%s", but this branch is '.
+ 'not supported for automatic upgrades. Supported branches are: '.
+ '%s.',
$lib,
$root,
- $branch_name));
+ $branch_name,
+ implode(', ', array_keys($supported_branches))));
}
chdir($root);
diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php
--- a/src/workflow/ArcanistWorkflow.php
+++ b/src/workflow/ArcanistWorkflow.php
@@ -62,6 +62,7 @@
private $projectInfo;
private $repositoryInfo;
private $repositoryReasons;
+ private $repositoryRef;
private $arcanistConfiguration;
private $parentWorkflow;
@@ -69,6 +70,7 @@
private $repositoryVersion;
private $changeCache = array();
+ private $conduitEngine;
public function __construct() {}
@@ -582,6 +584,7 @@
$arc_config = $this->getArcanistConfiguration();
$workflow = $arc_config->buildWorkflow($command);
$workflow->setParentWorkflow($this);
+ $workflow->setConduitEngine($this->getConduitEngine());
$workflow->setCommand($command);
$workflow->setConfigurationManager($this->getConfigurationManager());
@@ -1773,9 +1776,9 @@
}
try {
- $results = $this->getConduit()->callMethodSynchronous(
- 'repository.query',
- $query);
+ $method = 'repository.query';
+ $results = $this->getConduitEngine()->newCall($method, $query)
+ ->resolve();
} catch (ConduitClientException $ex) {
if ($ex->getErrorCode() == 'ERR-CONDUIT-CALL') {
$reasons[] = pht(
@@ -2068,5 +2071,68 @@
return $map;
}
+ final public function setConduitEngine(
+ ArcanistConduitEngine $conduit_engine) {
+ $this->conduitEngine = $conduit_engine;
+ return $this;
+ }
+
+ final public function getConduitEngine() {
+ return $this->conduitEngine;
+ }
+
+ final protected function newWorkingCopyStateRef() {
+ $ref = new ArcanistWorkingCopyStateRef();
+
+ $working_copy = $this->getWorkingCopy();
+ $ref->setRootDirectory($working_copy->getProjectRoot());
+
+ return $ref;
+ }
+
+ final protected function newRefQuery(array $refs) {
+ assert_instances_of($refs, 'ArcanistRef');
+
+ $query = id(new ArcanistRefQuery())
+ ->setConduitEngine($this->getConduitEngine())
+ ->setRefs($refs);
+
+ if ($this->hasRepositoryAPI()) {
+ $query->setRepositoryAPI($this->getRepositoryAPI());
+ }
+
+ $repository_ref = $this->getRepositoryRef();
+ if ($repository_ref) {
+ $query->setRepositoryRef($repository_ref);
+ }
+
+ $working_copy = $this->getConfigurationManager()->getWorkingCopyIdentity();
+ if ($working_copy) {
+ $working_ref = $this->newWorkingCopyStateRef();
+ $query->setWorkingCopyRef($working_ref);
+ }
+
+ return $query;
+ }
+
+ final public function getRepositoryRef() {
+ if (!$this->getConfigurationManager()->getWorkingCopyIdentity()) {
+ return null;
+ }
+
+ if (!$this->repositoryAPI) {
+ return null;
+ }
+
+ if (!$this->repositoryRef) {
+ $ref = id(new ArcanistRepositoryRef())
+ ->setPHID($this->getRepositoryPHID())
+ ->setBrowseURI($this->getRepositoryURI());
+
+ $this->repositoryRef = $ref;
+ }
+
+ return $this->repositoryRef;
+ }
}

File Metadata

Mime Type
text/plain
Expires
Fri, Mar 14, 8:35 AM (2 w, 4 h ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/q4/rh/5kyaokepu6jt4boi
Default Alt Text
D20987.diff (105 KB)

Event Timeline