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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + 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(''); + } + + 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 @@ +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( + '** %s **', + 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( + '** %s **', + 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 @@ +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 `.')) + ->addItem( + pht( + 'Specify a URI explicitly with `--conduit-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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + $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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + 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 @@ - 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; + } }