diff --git a/bin/arc b/bin/arc --- a/bin/arc +++ b/bin/arc @@ -1,21 +1,10 @@ -#!/usr/bin/env bash +#!/usr/bin/env php + 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase.php', 'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractPrivateMethodXHPASTLinterRule.php', 'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase.php', + 'ArcanistAlias' => 'toolset/ArcanistAlias.php', + 'ArcanistAliasEffect' => 'toolset/ArcanistAliasEffect.php', + 'ArcanistAliasEngine' => 'toolset/ArcanistAliasEngine.php', 'ArcanistAliasFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php', 'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAliasFunctionXHPASTLinterRuleTestCase.php', 'ArcanistAliasWorkflow' => 'workflow/ArcanistAliasWorkflow.php', + 'ArcanistAliasesConfigOption' => 'config/option/ArcanistAliasesConfigOption.php', 'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php', 'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php', + 'ArcanistArcConfigurationEngineExtension' => 'config/arc/ArcanistArcConfigurationEngineExtension.php', + 'ArcanistArcToolset' => 'toolset/ArcanistArcToolset.php', 'ArcanistArrayCombineXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArrayCombineXHPASTLinterRule.php', 'ArcanistArrayCombineXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistArrayCombineXHPASTLinterRuleTestCase.php', 'ArcanistArrayIndexSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArrayIndexSpacingXHPASTLinterRule.php', @@ -103,10 +109,17 @@ 'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConcatenationOperatorXHPASTLinterRuleTestCase.php', 'ArcanistConduitCall' => 'conduit/ArcanistConduitCall.php', 'ArcanistConduitEngine' => 'conduit/ArcanistConduitEngine.php', + 'ArcanistConduitException' => 'conduit/ArcanistConduitException.php', + 'ArcanistConfigOption' => 'config/option/ArcanistConfigOption.php', 'ArcanistConfiguration' => 'configuration/ArcanistConfiguration.php', 'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php', 'ArcanistConfigurationDrivenUnitTestEngine' => 'unit/engine/ArcanistConfigurationDrivenUnitTestEngine.php', + 'ArcanistConfigurationEngine' => 'config/ArcanistConfigurationEngine.php', + 'ArcanistConfigurationEngineExtension' => 'config/ArcanistConfigurationEngineExtension.php', 'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php', + 'ArcanistConfigurationSource' => 'config/source/ArcanistConfigurationSource.php', + 'ArcanistConfigurationSourceList' => 'config/ArcanistConfigurationSourceList.php', + 'ArcanistConfigurationSourceValue' => 'config/ArcanistConfigurationSourceValue.php', 'ArcanistConsoleLintRenderer' => 'lint/renderer/ArcanistConsoleLintRenderer.php', 'ArcanistConsoleLintRendererTestCase' => 'lint/renderer/__tests__/ArcanistConsoleLintRendererTestCase.php', 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php', @@ -126,8 +139,10 @@ 'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase.php', 'ArcanistDefaultParametersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php', 'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDefaultParametersXHPASTLinterRuleTestCase.php', + 'ArcanistDefaultsConfigurationSource' => 'config/source/ArcanistDefaultsConfigurationSource.php', 'ArcanistDeprecationXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDeprecationXHPASTLinterRule.php', 'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeprecationXHPASTLinterRuleTestCase.php', + 'ArcanistDictionaryConfigurationSource' => 'config/source/ArcanistDictionaryConfigurationSource.php', 'ArcanistDiffByteSizeException' => 'exception/ArcanistDiffByteSizeException.php', 'ArcanistDiffChange' => 'parser/diff/ArcanistDiffChange.php', 'ArcanistDiffChangeType' => 'parser/diff/ArcanistDiffChangeType.php', @@ -165,10 +180,12 @@ 'ArcanistExtractUseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php', 'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistExtractUseXHPASTLinterRuleTestCase.php', 'ArcanistFeatureWorkflow' => 'workflow/ArcanistFeatureWorkflow.php', + 'ArcanistFileConfigurationSource' => 'config/source/ArcanistFileConfigurationSource.php', 'ArcanistFileDataRef' => 'upload/ArcanistFileDataRef.php', 'ArcanistFileUploader' => 'upload/ArcanistFileUploader.php', 'ArcanistFilenameLinter' => 'lint/linter/ArcanistFilenameLinter.php', 'ArcanistFilenameLinterTestCase' => 'lint/linter/__tests__/ArcanistFilenameLinterTestCase.php', + 'ArcanistFilesystemConfigurationSource' => 'config/source/ArcanistFilesystemConfigurationSource.php', 'ArcanistFlagWorkflow' => 'workflow/ArcanistFlagWorkflow.php', 'ArcanistFlake8Linter' => 'lint/linter/ArcanistFlake8Linter.php', 'ArcanistFlake8LinterTestCase' => 'lint/linter/__tests__/ArcanistFlake8LinterTestCase.php', @@ -186,6 +203,7 @@ 'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php', 'ArcanistGitRevisionHardpointLoader' => 'loader/ArcanistGitRevisionHardpointLoader.php', 'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php', + 'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php', 'ArcanistGlobalVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistGlobalVariableXHPASTLinterRule.php', 'ArcanistGlobalVariableXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistGlobalVariableXHPASTLinterRuleTestCase.php', 'ArcanistGoLintLinter' => 'lint/linter/ArcanistGoLintLinter.php', @@ -248,7 +266,6 @@ 'ArcanistLesscLinter' => 'lint/linter/ArcanistLesscLinter.php', 'ArcanistLesscLinterTestCase' => 'lint/linter/__tests__/ArcanistLesscLinterTestCase.php', 'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php', - 'ArcanistLibraryTestCase' => '__tests__/ArcanistLibraryTestCase.php', 'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php', 'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php', 'ArcanistLintMessageTestCase' => 'lint/__tests__/ArcanistLintMessageTestCase.php', @@ -264,7 +281,11 @@ 'ArcanistLintersWorkflow' => 'workflow/ArcanistLintersWorkflow.php', 'ArcanistListAssignmentXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistListAssignmentXHPASTLinterRule.php', 'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistListAssignmentXHPASTLinterRuleTestCase.php', + 'ArcanistListConfigOption' => 'config/option/ArcanistListConfigOption.php', 'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php', + 'ArcanistLocalConfigurationSource' => 'config/source/ArcanistLocalConfigurationSource.php', + 'ArcanistLogEngine' => 'log/ArcanistLogEngine.php', + 'ArcanistLogMessage' => 'log/ArcanistLogMessage.php', 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php', 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLogicalOperatorsXHPASTLinterRuleTestCase.php', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php', @@ -274,6 +295,7 @@ 'ArcanistMercurialHardpointLoader' => 'loader/ArcanistMercurialHardpointLoader.php', 'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php', 'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php', + 'ArcanistMercurialWorkingCopy' => 'workingcopy/ArcanistMercurialWorkingCopy.php', 'ArcanistMercurialWorkingCopyCommitHardpointLoader' => 'loader/ArcanistMercurialWorkingCopyCommitHardpointLoader.php', 'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php', 'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php', @@ -295,6 +317,7 @@ 'ArcanistNoLintLinterTestCase' => 'lint/linter/__tests__/ArcanistNoLintLinterTestCase.php', 'ArcanistNoParentScopeXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNoParentScopeXHPASTLinterRule.php', 'ArcanistNoParentScopeXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistNoParentScopeXHPASTLinterRuleTestCase.php', + 'ArcanistNoURIConduitException' => 'conduit/ArcanistNoURIConduitException.php', 'ArcanistNoneLintRenderer' => 'lint/renderer/ArcanistNoneLintRenderer.php', 'ArcanistObjectOperatorSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistObjectOperatorSpacingXHPASTLinterRule.php', 'ArcanistObjectOperatorSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistObjectOperatorSpacingXHPASTLinterRuleTestCase.php', @@ -320,6 +343,7 @@ 'ArcanistParseStrUseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistParseStrUseXHPASTLinterRuleTestCase.php', 'ArcanistPasteWorkflow' => 'workflow/ArcanistPasteWorkflow.php', 'ArcanistPatchWorkflow' => 'workflow/ArcanistPatchWorkflow.php', + 'ArcanistPhageToolset' => 'toolset/ArcanistPhageToolset.php', 'ArcanistPhpLinter' => 'lint/linter/ArcanistPhpLinter.php', 'ArcanistPhpLinterTestCase' => 'lint/linter/__tests__/ArcanistPhpLinterTestCase.php', 'ArcanistPhpcsLinter' => 'lint/linter/ArcanistPhpcsLinter.php', @@ -327,11 +351,14 @@ 'ArcanistPhpunitTestResultParser' => 'unit/parser/ArcanistPhpunitTestResultParser.php', 'ArcanistPhrequentWorkflow' => 'workflow/ArcanistPhrequentWorkflow.php', 'ArcanistPhutilLibraryLinter' => 'lint/linter/ArcanistPhutilLibraryLinter.php', + 'ArcanistPhutilWorkflow' => 'toolset/ArcanistPhutilWorkflow.php', 'ArcanistPhutilXHPASTLinterStandard' => 'lint/linter/standards/phutil/ArcanistPhutilXHPASTLinterStandard.php', 'ArcanistPlusOperatorOnStringsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPlusOperatorOnStringsXHPASTLinterRule.php', 'ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase.php', 'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPregQuoteMisuseXHPASTLinterRule.php', 'ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase.php', + 'ArcanistProjectConfigurationSource' => 'config/source/ArcanistProjectConfigurationSource.php', + 'ArcanistPrompt' => 'toolset/ArcanistPrompt.php', 'ArcanistPublicPropertyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPublicPropertyXHPASTLinterRule.php', 'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPublicPropertyXHPASTLinterRuleTestCase.php', 'ArcanistPuppetLintLinter' => 'lint/linter/ArcanistPuppetLintLinter.php', @@ -361,6 +388,9 @@ 'ArcanistRuboCopLinterTestCase' => 'lint/linter/__tests__/ArcanistRuboCopLinterTestCase.php', 'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php', 'ArcanistRubyLinterTestCase' => 'lint/linter/__tests__/ArcanistRubyLinterTestCase.php', + 'ArcanistRuntime' => 'runtime/ArcanistRuntime.php', + 'ArcanistRuntimeConfigurationSource' => 'config/source/ArcanistRuntimeConfigurationSource.php', + 'ArcanistScalarConfigOption' => 'config/option/ArcanistScalarConfigOption.php', 'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSelfClassReferenceXHPASTLinterRule.php', 'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSelfClassReferenceXHPASTLinterRuleTestCase.php', @@ -381,9 +411,12 @@ 'ArcanistStaticThisXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistStaticThisXHPASTLinterRule.php', 'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistStaticThisXHPASTLinterRuleTestCase.php', 'ArcanistStopWorkflow' => 'workflow/ArcanistStopWorkflow.php', + 'ArcanistStringConfigOption' => 'config/option/ArcanistStringConfigOption.php', 'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php', + 'ArcanistSubversionWorkingCopy' => 'workingcopy/ArcanistSubversionWorkingCopy.php', 'ArcanistSummaryLintRenderer' => 'lint/renderer/ArcanistSummaryLintRenderer.php', 'ArcanistSyntaxErrorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSyntaxErrorXHPASTLinterRule.php', + 'ArcanistSystemConfigurationSource' => 'config/source/ArcanistSystemConfigurationSource.php', 'ArcanistTasksWorkflow' => 'workflow/ArcanistTasksWorkflow.php', 'ArcanistTautologicalExpressionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistTautologicalExpressionXHPASTLinterRule.php', 'ArcanistTautologicalExpressionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistTautologicalExpressionXHPASTLinterRuleTestCase.php', @@ -399,6 +432,7 @@ 'ArcanistTodoCommentXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistTodoCommentXHPASTLinterRule.php', 'ArcanistTodoCommentXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistTodoCommentXHPASTLinterRuleTestCase.php', 'ArcanistTodoWorkflow' => 'workflow/ArcanistTodoWorkflow.php', + 'ArcanistToolset' => 'toolset/ArcanistToolset.php', 'ArcanistUSEnglishTranslation' => 'internationalization/ArcanistUSEnglishTranslation.php', 'ArcanistUnableToParseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnableToParseXHPASTLinterRule.php', 'ArcanistUnaryPostfixExpressionSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnaryPostfixExpressionSpacingXHPASTLinterRule.php', @@ -431,6 +465,7 @@ 'ArcanistUselessOverridingMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUselessOverridingMethodXHPASTLinterRule.php', 'ArcanistUselessOverridingMethodXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUselessOverridingMethodXHPASTLinterRuleTestCase.php', 'ArcanistUserAbortException' => 'exception/usage/ArcanistUserAbortException.php', + 'ArcanistUserConfigurationSource' => 'config/source/ArcanistUserConfigurationSource.php', 'ArcanistVariableReferenceSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistVariableReferenceSpacingXHPASTLinterRule.php', 'ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase.php', 'ArcanistVariableVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistVariableVariableXHPASTLinterRule.php', @@ -438,8 +473,14 @@ 'ArcanistVersionWorkflow' => 'workflow/ArcanistVersionWorkflow.php', 'ArcanistWeldWorkflow' => 'workflow/ArcanistWeldWorkflow.php', 'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php', + 'ArcanistWildConfigOption' => 'config/option/ArcanistWildConfigOption.php', 'ArcanistWorkflow' => 'workflow/ArcanistWorkflow.php', + 'ArcanistWorkflowArgument' => 'toolset/ArcanistWorkflowArgument.php', + 'ArcanistWorkflowInformation' => 'toolset/ArcanistWorkflowInformation.php', + 'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php', + 'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php', 'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.php', + 'ArcanistWorkingCopyPath' => 'workingcopy/ArcanistWorkingCopyPath.php', 'ArcanistWorkingCopyStateRef' => 'ref/ArcanistWorkingCopyStateRef.php', 'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php', 'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php', @@ -916,11 +957,17 @@ 'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistAlias' => 'Phobject', + 'ArcanistAliasEffect' => 'Phobject', + 'ArcanistAliasEngine' => 'Phobject', 'ArcanistAliasFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAliasWorkflow' => 'ArcanistWorkflow', + 'ArcanistAliasesConfigOption' => 'ArcanistListConfigOption', 'ArcanistAmendWorkflow' => 'ArcanistWorkflow', 'ArcanistAnoidWorkflow' => 'ArcanistWorkflow', + 'ArcanistArcConfigurationEngineExtension' => 'ArcanistConfigurationEngineExtension', + 'ArcanistArcToolset' => 'ArcanistToolset', 'ArcanistArrayCombineXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistArrayCombineXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistArrayIndexSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1000,10 +1047,17 @@ 'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistConduitCall' => 'Phobject', 'ArcanistConduitEngine' => 'Phobject', + 'ArcanistConduitException' => 'Exception', + 'ArcanistConfigOption' => 'Phobject', 'ArcanistConfiguration' => 'Phobject', 'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine', 'ArcanistConfigurationDrivenUnitTestEngine' => 'ArcanistUnitTestEngine', + 'ArcanistConfigurationEngine' => 'Phobject', + 'ArcanistConfigurationEngineExtension' => 'Phobject', 'ArcanistConfigurationManager' => 'Phobject', + 'ArcanistConfigurationSource' => 'Phobject', + 'ArcanistConfigurationSourceList' => 'Phobject', + 'ArcanistConfigurationSourceValue' => 'Phobject', 'ArcanistConsoleLintRenderer' => 'ArcanistLintRenderer', 'ArcanistConsoleLintRendererTestCase' => 'PhutilTestCase', 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1023,8 +1077,10 @@ 'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistDefaultsConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistDeprecationXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistDictionaryConfigurationSource' => 'ArcanistConfigurationSource', 'ArcanistDiffByteSizeException' => 'Exception', 'ArcanistDiffChange' => 'Phobject', 'ArcanistDiffChangeType' => 'Phobject', @@ -1062,10 +1118,12 @@ 'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistFeatureWorkflow' => 'ArcanistWorkflow', + 'ArcanistFileConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistFileDataRef' => 'Phobject', 'ArcanistFileUploader' => 'Phobject', 'ArcanistFilenameLinter' => 'ArcanistLinter', 'ArcanistFilenameLinterTestCase' => 'ArcanistLinterTestCase', + 'ArcanistFilesystemConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistFlagWorkflow' => 'ArcanistWorkflow', 'ArcanistFlake8Linter' => 'ArcanistExternalLinter', 'ArcanistFlake8LinterTestCase' => 'ArcanistExternalLinterTestCase', @@ -1083,6 +1141,7 @@ 'ArcanistGitLandEngine' => 'ArcanistLandEngine', 'ArcanistGitRevisionHardpointLoader' => 'ArcanistGitHardpointLoader', 'ArcanistGitUpstreamPath' => 'Phobject', + 'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistGlobalVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistGlobalVariableXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistGoLintLinter' => 'ArcanistExternalLinter', @@ -1145,7 +1204,6 @@ 'ArcanistLesscLinter' => 'ArcanistExternalLinter', 'ArcanistLesscLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistLiberateWorkflow' => 'ArcanistWorkflow', - 'ArcanistLibraryTestCase' => 'PhutilLibraryTestCase', 'ArcanistLintEngine' => 'Phobject', 'ArcanistLintMessage' => 'Phobject', 'ArcanistLintMessageTestCase' => 'PhutilTestCase', @@ -1161,7 +1219,11 @@ 'ArcanistLintersWorkflow' => 'ArcanistWorkflow', 'ArcanistListAssignmentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistListConfigOption' => 'ArcanistConfigOption', 'ArcanistListWorkflow' => 'ArcanistWorkflow', + 'ArcanistLocalConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource', + 'ArcanistLogEngine' => 'Phobject', + 'ArcanistLogMessage' => 'Phobject', 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1171,6 +1233,7 @@ 'ArcanistMercurialHardpointLoader' => 'ArcanistHardpointLoader', 'ArcanistMercurialParser' => 'Phobject', 'ArcanistMercurialParserTestCase' => 'PhutilTestCase', + 'ArcanistMercurialWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistMercurialWorkingCopyCommitHardpointLoader' => 'ArcanistMercurialHardpointLoader', 'ArcanistMergeConflictLinter' => 'ArcanistLinter', 'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase', @@ -1192,6 +1255,7 @@ 'ArcanistNoLintLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistNoParentScopeXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNoParentScopeXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistNoURIConduitException' => 'ArcanistConduitException', 'ArcanistNoneLintRenderer' => 'ArcanistLintRenderer', 'ArcanistObjectOperatorSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistObjectOperatorSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', @@ -1217,6 +1281,7 @@ 'ArcanistParseStrUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPasteWorkflow' => 'ArcanistWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistWorkflow', + 'ArcanistPhageToolset' => 'ArcanistToolset', 'ArcanistPhpLinter' => 'ArcanistExternalLinter', 'ArcanistPhpLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPhpcsLinter' => 'ArcanistExternalLinter', @@ -1224,11 +1289,14 @@ 'ArcanistPhpunitTestResultParser' => 'ArcanistTestResultParser', 'ArcanistPhrequentWorkflow' => 'ArcanistWorkflow', 'ArcanistPhutilLibraryLinter' => 'ArcanistLinter', + 'ArcanistPhutilWorkflow' => 'PhutilArgumentWorkflow', 'ArcanistPhutilXHPASTLinterStandard' => 'ArcanistLinterStandard', 'ArcanistPlusOperatorOnStringsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistProjectConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource', + 'ArcanistPrompt' => 'Phobject', 'ArcanistPublicPropertyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPuppetLintLinter' => 'ArcanistExternalLinter', @@ -1258,6 +1326,8 @@ 'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRubyLinter' => 'ArcanistExternalLinter', 'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase', + 'ArcanistRuntimeConfigurationSource' => 'ArcanistDictionaryConfigurationSource', + 'ArcanistScalarConfigOption' => 'ArcanistConfigOption', 'ArcanistScriptAndRegexLinter' => 'ArcanistLinter', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', @@ -1278,9 +1348,12 @@ 'ArcanistStaticThisXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistStopWorkflow' => 'ArcanistPhrequentWorkflow', + 'ArcanistStringConfigOption' => 'ArcanistScalarConfigOption', 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', + 'ArcanistSubversionWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistSummaryLintRenderer' => 'ArcanistLintRenderer', 'ArcanistSyntaxErrorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistSystemConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistTasksWorkflow' => 'ArcanistWorkflow', 'ArcanistTautologicalExpressionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistTautologicalExpressionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', @@ -1296,6 +1369,7 @@ 'ArcanistTodoCommentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistTodoCommentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistTodoWorkflow' => 'ArcanistWorkflow', + 'ArcanistToolset' => 'Phobject', 'ArcanistUSEnglishTranslation' => 'PhutilTranslation', 'ArcanistUnableToParseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnaryPostfixExpressionSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1328,6 +1402,7 @@ 'ArcanistUselessOverridingMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUselessOverridingMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUserAbortException' => 'ArcanistUsageException', + 'ArcanistUserConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistVariableReferenceSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistVariableVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1335,8 +1410,14 @@ 'ArcanistVersionWorkflow' => 'ArcanistWorkflow', 'ArcanistWeldWorkflow' => 'ArcanistWorkflow', 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', + 'ArcanistWildConfigOption' => 'ArcanistConfigOption', 'ArcanistWorkflow' => 'Phobject', + 'ArcanistWorkflowArgument' => 'Phobject', + 'ArcanistWorkflowInformation' => 'Phobject', + 'ArcanistWorkingCopy' => 'Phobject', + 'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistWorkingCopyIdentity' => 'Phobject', + 'ArcanistWorkingCopyPath' => 'Phobject', 'ArcanistWorkingCopyStateRef' => 'ArcanistRef', 'ArcanistXHPASTLintNamingHook' => 'Phobject', 'ArcanistXHPASTLintNamingHookTestCase' => 'PhutilTestCase', diff --git a/src/__tests__/ArcanistLibraryTestCase.php b/src/__tests__/ArcanistLibraryTestCase.php deleted file mode 100644 --- a/src/__tests__/ArcanistLibraryTestCase.php +++ /dev/null @@ -1,3 +0,0 @@ -workingCopy = $working_copy; + return $this; + } + + public function getWorkingCopy() { + return $this->workingCopy; + } + + public function setArguments(PhutilArgumentParser $arguments) { + $this->arguments = $arguments; + return $this; + } + + public function getArguments() { + if (!$this->arguments) { + throw new PhutilInvalidStateException('setArguments'); + } + return $this->arguments; + } + + public function newConfigurationSourceList() { + $list = new ArcanistConfigurationSourceList(); + + $list->addSource(new ArcanistDefaultsConfigurationSource()); + + $arguments = $this->getArguments(); + + // If the invoker has provided one or more configuration files with + // "--config-file" arguments, read those files instead of the system + // and user configuration files. Otherwise, read the system and user + // configuration files. + + $config_files = $arguments->getArg('config-file'); + if ($config_files) { + foreach ($config_files as $config_file) { + $list->addSource(new ArcanistFileConfigurationSource($config_file)); + } + } else { + $system_path = $this->getSystemConfigurationFilePath(); + $list->addSource(new ArcanistSystemConfigurationSource($system_path)); + + $user_path = $this->getUserConfigurationFilePath(); + $list->addSource(new ArcanistUserConfigurationSource($user_path)); + } + + + // If we're running in a working copy, load the ".arcconfig" and any + // local configuration. + $working_copy = $this->getWorkingCopy(); + if ($working_copy) { + $project_path = $working_copy->getProjectConfigurationFilePath(); + if ($project_path !== null) { + $list->addSource(new ArcanistProjectConfigurationSource($project_path)); + } + + $local_path = $working_copy->getLocalConfigurationFilePath(); + if ($local_path !== null) { + $list->addSource(new ArcanistLocalConfigurationSource($local_path)); + } + } + + // If the invoker has provided "--config" arguments, parse those now. + $runtime_args = $arguments->getArg('config'); + if ($runtime_args) { + $list->addSource(new ArcanistRuntimeConfigurationSource($runtime_args)); + } + + return $list; + } + + private function getSystemConfigurationFilePath() { + if (phutil_is_windows()) { + return Filesystem::resolvePath( + 'Phabricator/Arcanist/config', + getenv('ProgramData')); + } else { + return '/etc/arcconfig'; + } + } + + private function getUserConfigurationFilePath() { + if (phutil_is_windows()) { + return getenv('APPDATA').'/.arcrc'; + } else { + return getenv('HOME').'/.arcrc'; + } + } + + public function newDefaults() { + $map = $this->newConfigOptionsMap(); + return mpull($map, 'getDefaultValue'); + } + + public function newConfigOptionsMap() { + $extensions = $this->newEngineExtensions(); + + $map = array(); + $alias_map = array(); + foreach ($extensions as $extension) { + $options = $extension->newConfigurationOptions(); + + foreach ($options as $option) { + $key = $option->getKey(); + + $this->validateConfigOptionKey($key, $extension); + + if (isset($map[$key])) { + throw new Exception( + pht( + 'Configuration option ("%s") defined by extension "%s" '. + 'conflicts with an existing option. Each option must have '. + 'a unique key.', + $key, + get_class($extension))); + } + + if (isset($alias_map[$key])) { + throw new Exception( + pht( + 'Configuration option ("%s") defined by extension "%s" '. + 'conflicts with an alias for another option ("%s"). The '. + 'key and aliases of each option must be unique.', + $key, + get_class($extension), + $alias_map[$key]->getKey())); + } + + $map[$key] = $option; + + foreach ($option->getAliases() as $alias) { + $this->validateConfigOptionKey($alias, $extension, $key); + + if (isset($map[$alias])) { + throw new Exception( + pht( + 'Configuration option ("%s") defined by extension "%s" '. + 'has an alias ("%s") which conflicts with an existing '. + 'option. The key and aliases of each option must be '. + 'unique.', + $key, + get_class($extension), + $alias)); + } + + if (isset($alias_map[$alias])) { + throw new Exception( + pht( + 'Configuration option ("%s") defined by extension "%s" '. + 'has an alias ("%s") which conflicts with the alias of '. + 'another configuration option ("%s"). The key and aliases '. + 'of each option must be unique.', + $key, + get_class($extension), + $alias, + $alias_map[$alias]->getKey())); + } + + $alias_map[$alias] = $option; + } + } + } + + return $map; + } + + private function validateConfigOptionKey( + $key, + ArcanistConfigurationEngineExtension $extension, + $is_alias_of = null) { + + $reserved = array( + // The presence of this key is used to detect old "~/.arcrc" files, so + // configuration options may not use it. + 'config', + ); + $reserved = array_fuse($reserved); + + if (isset($reserved[$key])) { + throw new Exception( + pht( + 'Extension ("%s") defines invalid configuration with key "%s". '. + 'This key is reserved.', + get_class($extension), + $key)); + } + + $is_ok = preg_match('(^[a-z][a-z0-9._-]{2,}\z)', $key); + if (!$is_ok) { + if ($is_alias_of === null) { + throw new Exception( + pht( + 'Extension ("%s") defines invalid configuration with key "%s". '. + 'Configuration keys: may only contain lowercase letters, '. + 'numbers, hyphens, underscores, and periods; must start with a '. + 'letter; and must be at least three characters long.', + get_class($extension), + $key)); + } else { + throw new Exception( + pht( + 'Extension ("%s") defines invalid alias ("%s") for configuration '. + 'key ("%s"). Configuration keys and aliases: may only contain '. + 'lowercase letters, numbers, hyphens, underscores, and periods; '. + 'must start with a letter; and must be at least three characters '. + 'long.', + get_class($extension), + $key, + $is_alias_of)); + } + } + } + + private function newEngineExtensions() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass('ArcanistConfigurationEngineExtension') + ->setUniqueMethod('getExtensionKey') + ->setContinueOnFailure(true) + ->execute(); + } + +} diff --git a/src/config/ArcanistConfigurationEngineExtension.php b/src/config/ArcanistConfigurationEngineExtension.php new file mode 100644 --- /dev/null +++ b/src/config/ArcanistConfigurationEngineExtension.php @@ -0,0 +1,10 @@ +getPhobjectClassConstant('EXTENSIONKEY'); + } + +} diff --git a/src/config/ArcanistConfigurationSourceList.php b/src/config/ArcanistConfigurationSourceList.php new file mode 100644 --- /dev/null +++ b/src/config/ArcanistConfigurationSourceList.php @@ -0,0 +1,202 @@ +sources[] = $source; + return $this; + } + + public function getSources() { + return $this->sources; + } + + private function getSourcesWithScopes($scopes) { + if ($scopes !== null) { + $scopes = array_fuse($scopes); + } + + $results = array(); + foreach ($this->getSources() as $source) { + if ($scopes !== null) { + $scope = $source->getConfigurationSourceScope(); + if ($scope === null) { + continue; + } + if (!isset($scopes[$scope])) { + continue; + } + } + + $results[] = $source; + } + + return $results; + } + + public function getWritableSourceFromScope($scope) { + $sources = $this->getSourcesWithScopes(array($scope)); + + $writable = array(); + foreach ($sources as $source) { + if (!$source->isWritableConfigurationSource()) { + continue; + } + + $writable[] = $source; + } + + if (!$writable) { + throw new Exception( + pht( + 'Unable to write configuration: there is no writable configuration '. + 'source in the "%s" scope.', + $scope)); + } + + if (count($writable) > 1) { + throw new Exception( + pht( + 'Unable to write configuration: more than one writable source '. + 'exists in the "%s" scope.', + $scope)); + } + + return head($writable); + } + + public function getConfig($key) { + $option = $this->getConfigOption($key); + $values = $this->getStorageValueList($key); + return $option->getValueFromStorageValueList($values); + } + + public function getConfigFromScopes($key, array $scopes) { + $option = $this->getConfigOption($key); + $values = $this->getStorageValueListFromScopes($key, $scopes); + return $option->getValueFromStorageValueList($values); + } + + public function getStorageValueList($key) { + return $this->getStorageValueListFromScopes($key, null); + } + + private function getStorageValueListFromScopes($key, $scopes) { + $values = array(); + + foreach ($this->getSourcesWithScopes($scopes) as $source) { + if ($source->hasValueForKey($key)) { + $value = $source->getValueForKey($key); + $values[] = new ArcanistConfigurationSourceValue( + $source, + $source->getValueForKey($key)); + } + } + + return $values; + } + + public function getConfigOption($key) { + $options = $this->getConfigOptions(); + + if (!isset($options[$key])) { + throw new Exception( + pht( + 'Configuration option ("%s") is unrecognized. You can only read '. + 'recognized configuration options.', + $key)); + } + + return $options[$key]; + } + + public function setConfigOptions(array $config_options) { + assert_instances_of($config_options, 'ArcanistConfigOption'); + + $config_options = mpull($config_options, null, 'getKey'); + $this->configOptions = $config_options; + + return $this; + } + + public function getConfigOptions() { + if ($this->configOptions === null) { + throw new PhutilInvalidStateException('setConfigOptions'); + } + + return $this->configOptions; + } + + public function validateConfiguration(ArcanistRuntime $runtime) { + $options = $this->getConfigOptions(); + + $aliases = array(); + foreach ($options as $key => $option) { + foreach ($option->getAliases() as $alias) { + $aliases[$alias] = $key; + } + } + + // TOOLSETS: Handle the case where config specifies both a value and an + // alias for that value. The alias should be ignored and we should emit + // a warning. This also needs to be implemented when actually reading + // configuration. + + $value_lists = array(); + foreach ($this->getSources() as $source) { + $keys = $source->getAllKeys(); + foreach ($keys as $key) { + $resolved_key = idx($aliases, $key, $key); + $option = idx($options, $resolved_key); + + // If there's no option object for this config, this value is + // unrecognized. Sources are free to handle this however they want: + // for config files we emit a warning; for "--config" we fatal. + + if (!$option) { + $source->didReadUnknownOption($runtime, $key); + continue; + } + + $raw_value = $source->getValueForKey($key); + + // Make sure we can convert whatever value the configuration source is + // providing into a legitimate runtime value. + try { + $value = $raw_value; + if ($source->isStringSource()) { + $value = $option->getStorageValueFromStringValue($value); + } + $option->getValueFromStorageValue($value); + + $value_lists[$resolved_key][] = new ArcanistConfigurationSourceValue( + $source, + $raw_value); + } catch (Exception $ex) { + throw new PhutilProxyException( + pht( + 'Configuration value ("%s") defined in source "%s" is not '. + 'valid.', + $key, + $source->getSourceDisplayName()), + $ex); + } + } + } + + // Make sure each value list can be merged. + foreach ($value_lists as $key => $value_list) { + try { + $options[$key]->getValueFromStorageValueList($value_list); + } catch (Exception $ex) { + throw $ex; + } + } + + } + +} diff --git a/src/config/ArcanistConfigurationSourceValue.php b/src/config/ArcanistConfigurationSourceValue.php new file mode 100644 --- /dev/null +++ b/src/config/ArcanistConfigurationSourceValue.php @@ -0,0 +1,22 @@ +source = $source; + $this->value = $value; + } + + public function getConfigurationSource() { + return $this->source; + } + + public function getValue() { + return $this->value; + } + +} diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php new file mode 100644 --- /dev/null +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -0,0 +1,159 @@ + array( + 'type' => 'list', + 'legacy' => 'phutil_libraries', + 'help' => pht( + 'A list of paths to phutil libraries that should be loaded at '. + 'startup. This can be used to make classes available, like lint '. + 'or unit test engines.'), + 'default' => array(), + 'example' => '["/var/arc/customlib/src"]', + ), + + 'arc.feature.start.default' => array( + 'type' => 'string', + 'help' => pht( + 'The name of the default branch to create the new feature branch '. + 'off of.'), + 'example' => '"develop"', + ), + 'arc.land.onto.default' => array( + 'type' => 'string', + 'help' => pht( + 'The name of the default branch to land changes onto when '. + '`%s` is run.', + 'arc land'), + 'example' => '"develop"', + ), + + 'arc.autostash' => array( + 'type' => 'bool', + 'help' => pht( + 'Whether %s should permit the automatic stashing of changes in the '. + 'working directory when requiring a clean working copy. This option '. + 'should only be used when users understand how to restore their '. + 'working directory from the local stash if an Arcanist operation '. + 'causes an unrecoverable error.', + 'arc'), + 'default' => false, + 'example' => 'false', + ), + + 'history.immutable' => array( + 'type' => 'bool', + 'legacy' => 'immutable_history', + 'help' => pht( + 'If true, %s will never change repository history (e.g., through '. + 'amending or rebasing). Defaults to true in Mercurial and false in '. + 'Git. This setting has no effect in Subversion.', + 'arc'), + 'example' => 'false', + ), + 'editor' => array( + 'type' => 'string', + 'help' => pht( + 'Command to use to invoke an interactive editor, like `%s` or `%s`. '. + 'This setting overrides the %s environmental variable.', + 'nano', + 'vim', + 'EDITOR'), + 'example' => '"nano"', + ), + 'https.cabundle' => array( + 'type' => 'string', + 'help' => pht( + "Path to a custom CA bundle file to be used for arcanist's cURL ". + "calls. This is used primarily when your conduit endpoint is ". + "behind HTTPS signed by your organization's internal CA."), + 'example' => 'support/yourca.pem', + ), + 'https.blindly-trust-domains' => array( + 'type' => 'list', + 'help' => pht( + 'List of domains to blindly trust SSL certificates for. '. + 'Disables peer verification.'), + 'default' => array(), + 'example' => '["secure.mycompany.com"]', + ), + 'browser' => array( + 'type' => 'string', + 'help' => pht('Command to use to invoke a web browser.'), + 'example' => '"gnome-www-browser"', + ), +*/ + + + + return array( + id(new ArcanistStringConfigOption()) + ->setKey('base') + ->setSummary(pht('Ruleset for selecting commit ranges.')) + ->setHelp( + pht( + 'Base commit ruleset to invoke when determining the start of a '. + 'commit range. See "Arcanist User Guide: Commit Ranges" for '. + 'details.')) + ->setExamples( + array( + 'arc:amended, arc:prompt', + )), + id(new ArcanistStringConfigOption()) + ->setKey('repository') + ->setAliases( + array( + 'repository.callsign', + )) + ->setSummary(pht('Repository for the current working copy.')) + ->setHelp( + pht( + 'Associate the working copy with a specific Phabricator '. + 'repository. Normally, `arc` can figure this association out on '. + 'its own, but if your setup is unusual you can use this option '. + 'to tell it what the desired value is.')) + ->setExamples( + array( + 'libexample', + 'XYZ', + 'R123', + '123', + )), + id(new ArcanistStringConfigOption()) + ->setKey('phabricator.uri') + ->setAliases( + array( + 'conduit_uri', + 'default', + )) + ->setSummary(pht('Phabricator install to connect to.')) + ->setHelp( + pht( + 'Associates this working copy with a specific installation of '. + 'Phabricator.')) + ->setExamples( + array( + 'https://phabricator.mycompany.com/', + )), + id(new ArcanistAliasesConfigOption()) + ->setKey(self::KEY_ALIASES) + ->setDefaultValue(array()) + ->setSummary(pht('List of command aliases.')) + ->setHelp( + pht( + 'Configured command aliases. Use the "alias" workflow to define '. + 'aliases.')), + ); + } + +} diff --git a/src/config/option/ArcanistAliasesConfigOption.php b/src/config/option/ArcanistAliasesConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistAliasesConfigOption.php @@ -0,0 +1,36 @@ +'; + } + + public function getValueFromStorageValue($value) { + if (!is_array($value)) { + throw new Exception(pht('Expected a list or dictionary!')); + } + + $aliases = array(); + foreach ($value as $key => $spec) { + $aliases[] = ArcanistAlias::newFromConfig($key, $spec); + } + + return $aliases; + } + + protected function didReadStorageValueList(array $list) { + assert_instances_of($list, 'ArcanistConfigurationSourceValue'); + return mpull($list, 'getValue'); + } + + public function getDisplayValueFromValue($value) { + return pht('Use the "alias" workflow to review aliases.'); + } + + public function getStorageValueFromValue($value) { + return mpull($value, 'getStorageDictionary'); + } + +} diff --git a/src/config/option/ArcanistConfigOption.php b/src/config/option/ArcanistConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistConfigOption.php @@ -0,0 +1,100 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setAliases($aliases) { + $this->aliases = $aliases; + return $this; + } + + public function getAliases() { + return $this->aliases; + } + + public function setSummary($summary) { + $this->summary = $summary; + return $this; + } + + public function getSummary() { + return $this->summary; + } + + public function setHelp($help) { + $this->help = $help; + return $this; + } + + public function getHelp() { + return $this->help; + } + + public function setExamples(array $examples) { + $this->examples = $examples; + return $this; + } + + public function getExamples() { + return $this->examples; + } + + public function setDefaultValue($default_value) { + $this->defaultValue = $default_value; + return $this; + } + + public function getDefaultValue() { + return $this->defaultValue; + } + + abstract public function getType(); + + abstract public function getValueFromStorageValueList(array $list); + abstract public function getValueFromStorageValue($value); + abstract public function getDisplayValueFromValue($value); + abstract public function getStorageValueFromValue($value); + + public function getStorageValueFromStringValue($value) { + throw new Exception( + pht( + 'This configuration option ("%s") does not support runtime definition '. + 'with "--config".', + $this->getKey())); + } + + protected function getStorageValueFromSourceValue( + ArcanistConfigurationSourceValue $source_value) { + + $value = $source_value->getValue(); + $source = $source_value->getConfigurationSource(); + + if ($source->isStringSource()) { + $value = $this->getStorageValueFromStringValue($value); + } + + return $value; + } + + public function writeValue(ArcanistConfigurationSource $source, $value) { + $value = $this->getStorageValueFromValue($value); + $source->setStorageValueForKey($this->getKey(), $value); + } + +} diff --git a/src/config/option/ArcanistListConfigOption.php b/src/config/option/ArcanistListConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistListConfigOption.php @@ -0,0 +1,32 @@ +getConfigurationSource(); + $storage_value = $this->getStorageValueFromSourceValue($source_value); + + $items = $this->getValueFromStorageValue($storage_value); + foreach ($items as $item) { + $result_list[] = new ArcanistConfigurationSourceValue( + $source, + $item); + } + } + + $result_list = $this->didReadStorageValueList($result_list); + + return $result_list; + } + + protected function didReadStorageValueList(array $list) { + assert_instances_of($list, 'ArcanistConfigurationSourceValue'); + return mpull($list, 'getValue'); + } + +} diff --git a/src/config/option/ArcanistScalarConfigOption.php b/src/config/option/ArcanistScalarConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistScalarConfigOption.php @@ -0,0 +1,19 @@ +getStorageValueFromSourceValue($source_value); + + return $this->getValueFromStorageValue($storage_value); + } + + public function getValueFromStorageValue($value) { + return $value; + } + +} diff --git a/src/config/option/ArcanistStringConfigOption.php b/src/config/option/ArcanistStringConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistStringConfigOption.php @@ -0,0 +1,22 @@ +getStorageValueFromSourceValue($source_value); + + return $this->getValueFromStorageValue($storage_value); + } + + public function getValueFromStorageValue($value) { + return $value; + } + + public function getStorageValueFromValue($value) { + return $value; + } + +} diff --git a/src/config/source/ArcanistConfigurationSource.php b/src/config/source/ArcanistConfigurationSource.php new file mode 100644 --- /dev/null +++ b/src/config/source/ArcanistConfigurationSource.php @@ -0,0 +1,39 @@ +getLogEngine()->writeWarning( + pht('UNKNOWN CONFIGURATION'), + pht( + 'Ignoring unrecognized configuration option ("%s") from source: %s.', + $key, + $this->getSourceDisplayName())); + } + +} diff --git a/src/config/source/ArcanistDefaultsConfigurationSource.php b/src/config/source/ArcanistDefaultsConfigurationSource.php new file mode 100644 --- /dev/null +++ b/src/config/source/ArcanistDefaultsConfigurationSource.php @@ -0,0 +1,17 @@ +newDefaults(); + + parent::__construct($values); + } + +} diff --git a/src/config/source/ArcanistDictionaryConfigurationSource.php b/src/config/source/ArcanistDictionaryConfigurationSource.php new file mode 100644 --- /dev/null +++ b/src/config/source/ArcanistDictionaryConfigurationSource.php @@ -0,0 +1,44 @@ +values = $dictionary; + } + + public function getAllKeys() { + return array_keys($this->values); + } + + public function hasValueForKey($key) { + return array_key_exists($key, $this->values); + } + + public function getValueForKey($key) { + if (!$this->hasValueForKey($key)) { + throw new Exception( + pht( + 'Configuration source ("%s") has no value for key ("%s").', + get_class($this), + $key)); + } + + return $this->values[$key]; + } + + public function setStorageValueForKey($key, $value) { + $this->values[$key] = $value; + + $this->writeToStorage($this->values); + + return $this; + } + + protected function writeToStorage($values) { + throw new PhutilMethodNotImplementedException(); + } + +} diff --git a/src/config/source/ArcanistFileConfigurationSource.php b/src/config/source/ArcanistFileConfigurationSource.php new file mode 100644 --- /dev/null +++ b/src/config/source/ArcanistFileConfigurationSource.php @@ -0,0 +1,10 @@ +path = $path; + + $values = array(); + if (Filesystem::pathExists($path)) { + $contents = Filesystem::readFile($path); + if (strlen(trim($contents))) { + $values = phutil_json_decode($contents); + } + } + + $values = $this->didReadFilesystemValues($values); + + parent::__construct($values); + } + + public function getPath() { + return $this->path; + } + + public function getSourceDisplayName() { + return pht('%s (%s)', $this->getFileKindDisplayName(), $this->getPath()); + } + + abstract public function getFileKindDisplayName(); + + protected function didReadFilesystemValues(array $values) { + return $values; + } + + protected function writeToStorage($values) { + $content = id(new PhutilJSON()) + ->encodeFormatted($values); + + Filesystem::writeFile($this->path, $content); + } + +} diff --git a/src/config/source/ArcanistLocalConfigurationSource.php b/src/config/source/ArcanistLocalConfigurationSource.php new file mode 100644 --- /dev/null +++ b/src/config/source/ArcanistLocalConfigurationSource.php @@ -0,0 +1,10 @@ +showTraceMessages = $show_trace_messages; + return $this; + } + + public function getShowTraceMessages() { + return $this->showTraceMessages; + } + + public function newMessage() { + return new ArcanistLogMessage(); + } + + private function writeBytes($bytes) { + fprintf(STDERR, '%s', $bytes); + return $this; + } + + public function writeNewline() { + return $this->writeBytes("\n"); + } + + public function writeMessage(ArcanistLogMessage $message) { + $color = $message->getColor(); + + $this->writeBytes( + tsprintf( + "** %s ** %s\n", + $message->getLabel(), + $message->getMessage())); + + return $message; + } + + public function writeWarning($label, $message) { + return $this->writeMessage( + $this->newMessage() + ->setColor('yellow') + ->setLabel($label) + ->setMessage($message)); + } + + public function writeError($label, $message) { + return $this->writeMessage( + $this->newMessage() + ->setColor('red') + ->setLabel($label) + ->setMessage($message)); + } + + public function writeSuccess($label, $message) { + return $this->writeMessage( + $this->newMessage() + ->setColor('green') + ->setLabel($label) + ->setMessage($message)); + } + + public function writeStatus($label, $message) { + return $this->writeMessage( + $this->newMessage() + ->setColor('blue') + ->setLabel($label) + ->setMessage($message)); + } + + public function writeTrace($label, $message) { + $trace = $this->newMessage() + ->setColor('magenta') + ->setLabel($label) + ->setMessage($message); + + if ($this->getShowTraceMessages()) { + $this->writeMessage($trace); + } + + return $trace; + } + + public function writeHint($label, $message) { + return $this->writeMessage( + $this->newMessage() + ->setColor('cyan') + ->setLabel($label) + ->setMessage($message)); + } + +} diff --git a/src/log/ArcanistLogMessage.php b/src/log/ArcanistLogMessage.php new file mode 100644 --- /dev/null +++ b/src/log/ArcanistLogMessage.php @@ -0,0 +1,47 @@ +label = $label; + return $this; + } + + public function getLabel() { + return $this->label; + } + + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function getColor() { + return $this->color; + } + + public function setChannel($channel) { + $this->channel = $channel; + return $this; + } + + public function getChannel() { + return $this->channel; + } + + public function setMessage($message) { + $this->message = $message; + return $this; + } + + public function getMessage() { + return $this->message; + } + +} diff --git a/src/runtime/ArcanistRuntime.php b/src/runtime/ArcanistRuntime.php new file mode 100644 --- /dev/null +++ b/src/runtime/ArcanistRuntime.php @@ -0,0 +1,653 @@ +checkEnvironment(); + } catch (Exception $ex) { + echo "CONFIGURATION ERROR\n\n"; + echo $ex->getMessage(); + echo "\n\n"; + return 1; + } + + PhutilTranslator::getInstance() + ->setLocale(PhutilLocale::loadLocale('en_US')) + ->setTranslations(PhutilTranslation::getTranslationMapForLocale('en_US')); + + $log = new ArcanistLogEngine(); + $this->logEngine = $log; + + try { + return $this->executeCore($argv); + } catch (ArcanistConduitException $ex) { + $log->writeError(pht('CONDUIT'), $ex->getMessage()); + } catch (PhutilArgumentUsageException $ex) { + $log->writeError(pht('USAGE EXCEPTION'), $ex->getMessage()); + } catch (ArcanistUserAbortException $ex) { + $log->writeError(pht('---'), $ex->getMessage()); + } + + return 1; + } + + private function executeCore(array $argv) { + $log = $this->getLogEngine(); + + $config_args = array( + array( + 'name' => 'library', + 'param' => 'path', + 'help' => pht('Load a library.'), + 'repeat' => true, + ), + array( + 'name' => 'config', + 'param' => 'key=value', + 'repeat' => true, + 'help' => pht('Specify a runtime configuration value.'), + ), + array( + 'name' => 'config-file', + 'param' => 'path', + 'repeat' => true, + 'help' => pht( + 'Load one or more configuration files. If this flag is provided, '. + 'the system and user configuration files are ignored.'), + ), + ); + + $args = id(new PhutilArgumentParser($argv)) + ->parseStandardArguments(); + + $is_trace = $args->getArg('trace'); + $log->setShowTraceMessages($is_trace); + + $log->writeTrace(pht('ARGV'), csprintf('%Ls', $argv)); + + // We're installing the signal handler after parsing "--trace" so that it + // can emit debugging messages. This means there's a very small window at + // startup where signals have no special handling, but we couldn't really + // route them or do anything interesting with them anyway. + $this->installSignalHandler(); + + $args->parsePartial($config_args, true); + + $config_engine = $this->loadConfiguration($args); + $config = $config_engine->newConfigurationSourceList(); + + $this->loadLibraries($args, $config); + + // Now that we've loaded libraries, we can validate configuration. + // Do this before continuing since configuration can impact other + // behaviors immediately and we want to catch any issues right away. + $config->setConfigOptions($config_engine->newConfigOptionsMap()); + $config->validateConfiguration($this); + + $toolset = $this->newToolset($argv); + + $args->parsePartial($toolset->getToolsetArguments()); + + $workflows = $this->newWorkflows($toolset); + $this->workflows = $workflows; + + $phutil_workflows = array(); + foreach ($workflows as $key => $workflow) { + $phutil_workflows[$key] = $workflow->newPhutilWorkflow(); + + $workflow + ->setRuntime($this) + ->setConfigurationEngine($config_engine) + ->setConfigurationSourceList($config); + } + + + $unconsumed_argv = $args->getUnconsumedArgumentVector(); + + if (!$unconsumed_argv) { + // TOOLSETS: This means the user just ran "arc" or some other top-level + // toolset without any workflow argument. We should give them a summary + // of the toolset, a list of workflows, and a pointer to "arc help" for + // more details. + + // A possible exception is "arc --help", which should perhaps pass + // through and act like "arc help". + throw new PhutilArgumentUsageException(pht('Choose a workflow!')); + } + + $alias_effects = id(new ArcanistAliasEngine()) + ->setRuntime($this) + ->setToolset($toolset) + ->setWorkflows($workflows) + ->setConfigurationSourceList($config) + ->resolveAliases($unconsumed_argv); + + $result_argv = $this->applyAliasEffects($alias_effects, $unconsumed_argv); + + $args->setUnconsumedArgumentVector($result_argv); + + // TOOLSETS: Some day, stop falling through to the old "arc" runtime. + + try { + return $args->parseWorkflowsFull($phutil_workflows); + } catch (PhutilArgumentUsageException $usage_exception) { + $log->writeHint( + pht('(::)'), + pht( + 'Workflow is unrecognized by modern "arc", falling through '. + 'to classic mode.')); + } + + $arcanist_root = phutil_get_library_root('arcanist'); + $arcanist_root = dirname($arcanist_root); + $bin = $arcanist_root.'/scripts/arcanist.php'; + + $err = phutil_passthru( + 'php -f %R -- %Ls', + $bin, + array_slice($argv, 1)); + + return $err; + } + + + /** + * Perform some sanity checks against the possible diversity of PHP builds in + * the wild, like very old versions and builds that were compiled with flags + * that exclude core functionality. + */ + private function checkEnvironment() { + // NOTE: We don't have phutil_is_windows() yet here. + $is_windows = (DIRECTORY_SEPARATOR != '/'); + + // We use stream_socket_pair() which is not available on Windows earlier. + $min_version = ($is_windows ? '5.3.0' : '5.2.3'); + $cur_version = phpversion(); + if (version_compare($cur_version, $min_version, '<')) { + $message = sprintf( + 'You are running a version of PHP ("%s"), which is older than the '. + 'minimum supported version ("%s"). Update PHP to continue.', + $cur_version, + $min_version); + + throw new Exception($message); + } + + if ($is_windows) { + $need_functions = array( + 'curl_init' => array('builtin-dll', 'php_curl.dll'), + ); + } else { + $need_functions = array( + 'curl_init' => array( + 'text', + "You need to install the cURL PHP extension, maybe with ". + "'apt-get install php5-curl' or 'yum install php53-curl' or ". + "something similar.", + ), + 'json_decode' => array('flag', '--without-json'), + ); + } + + $problems = array(); + + $config = null; + $show_config = false; + foreach ($need_functions as $fname => $resolution) { + if (function_exists($fname)) { + continue; + } + + static $info; + if ($info === null) { + ob_start(); + phpinfo(INFO_GENERAL); + $info = ob_get_clean(); + $matches = null; + if (preg_match('/^Configure Command =>\s*(.*?)$/m', $info, $matches)) { + $config = $matches[1]; + } + } + + list($what, $which) = $resolution; + + if ($what == 'flag' && strpos($config, $which) !== false) { + $show_config = true; + $problems[] = sprintf( + 'The build of PHP you are running was compiled with the configure '. + 'flag "%s", which means it does not support the function "%s()". '. + 'This function is required for Arcanist to run. Install a standard '. + 'build of PHP or rebuild it without this flag. You may also be '. + 'able to build or install the relevant extension separately.', + $which, + $fname); + continue; + } + + if ($what == 'builtin-dll') { + $problems[] = sprintf( + 'The build of PHP you are running does not have the "%s" extension '. + 'enabled. Edit your php.ini file and uncomment the line which '. + 'reads "extension=%s".', + $which, + $which); + continue; + } + + if ($what == 'text') { + $problems[] = $which; + continue; + } + + $problems[] = sprintf( + 'The build of PHP you are running is missing the required function '. + '"%s()". Rebuild PHP or install the extension which provides "%s()".', + $fname, + $fname); + } + + if ($problems) { + if ($show_config) { + $problems[] = "PHP was built with this configure command:\n\n{$config}"; + } + $problems = implode("\n\n", $problems); + + throw new Exception($problems); + } + } + + private function loadConfiguration(PhutilArgumentParser $args) { + $engine = id(new ArcanistConfigurationEngine()) + ->setArguments($args); + + $working_copy = ArcanistWorkingCopy::newFromWorkingDirectory(getcwd()); + if ($working_copy) { + $engine->setWorkingCopy($working_copy); + } + + return $engine; + } + + private function loadLibraries( + PhutilArgumentParser $args, + ArcanistConfigurationSourceList $config) { + + // TOOLSETS: Make this work again -- or replace it entirely with package + // management? + return; + + $is_trace = $args->getArg('trace'); + + $load = array(); + $working_copy = $this->getWorkingCopy(); + + $cli_libraries = $args->getArg('library'); + if ($cli_libraries) { + $load[] = array( + '--library', + $cli_libraries, + ); + } else { + $system_config = $config->readSystemArcConfig(); + $load[] = array( + $config->getSystemArcConfigLocation(), + idx($system_config, 'load', array()), + ); + + $global_config = $config->readUserArcConfig(); + $load[] = array( + $config->getUserConfigurationFileLocation(), + idx($global_config, 'load', array()), + ); + + $load[] = array( + '.arcconfig', + $working_copy->getProjectConfig('load'), + ); + + $load[] = array( + // TODO: We could explain exactly where this is coming from more + // clearly. + './.../arc/config', + $working_copy->getLocalConfig('load'), + ); + + $load[] = array( + '--config load=...', + $config->getRuntimeConfig('load', array()), + ); + } + + foreach ($load as $spec) { + list($source, $libraries) = $spec; + if ($is_trace) { + $this->logTrace( + pht('LOAD'), + pht( + 'Loading libraries from "%s"...', + $source)); + } + + if (!$libraries) { + if ($is_trace) { + $this->logTrace(pht('NONE'), pht('Nothing to load.')); + } + continue; + } + + if (!is_array($libraries)) { + throw new PhutilArgumentUsageException( + pht( + 'Libraries specified by "%s" are not formatted correctly. '. + 'Expected a list of paths. Check your configuration.', + $source)); + } + + foreach ($libraries as $library) { + $this->loadLibrary($source, $library, $working_copy, $is_trace); + } + } + } + + private function loadLibrary( + $source, + $location, + ArcanistWorkingCopyIdentity $working_copy, + $is_trace) { + + // Try to resolve the library location. We look in several places, in + // order: + // + // 1. Inside the working copy. This is for phutil libraries within the + // project. For instance "library/src" will resolve to + // "./library/src" if it exists. + // 2. In the same directory as the working copy. This allows you to + // check out a library alongside a working copy and reference it. + // If we haven't resolved yet, "library/src" will try to resolve to + // "../library/src" if it exists. + // 3. Using normal libphutil resolution rules. Generally, this means + // that it checks for libraries next to libphutil, then libraries + // in the PHP include_path. + // + // Note that absolute paths will just resolve absolutely through rule (1). + + $resolved = false; + + // Check inside the working copy. This also checks absolute paths, since + // they'll resolve absolute and just ignore the project root. + $resolved_location = Filesystem::resolvePath( + $location, + $working_copy->getProjectRoot()); + if (Filesystem::pathExists($resolved_location)) { + $location = $resolved_location; + $resolved = true; + } + + // If we didn't find anything, check alongside the working copy. + if (!$resolved) { + $resolved_location = Filesystem::resolvePath( + $location, + dirname($working_copy->getProjectRoot())); + if (Filesystem::pathExists($resolved_location)) { + $location = $resolved_location; + $resolved = true; + } + } + + if ($is_trace) { + $this->logTrace( + pht('LOAD'), + pht('Loading phutil library from "%s"...', $location)); + } + + $error = null; + try { + phutil_load_library($location); + } catch (PhutilBootloaderException $ex) { + fwrite( + STDERR, + '%s', + tsprintf( + "** %s ** %s\n", + pht( + 'Failed to load phutil library at location "%s". This library '. + 'is specified by "%s". Check that the setting is correct and '. + 'the library is located in the right place.', + $location, + $source))); + + $prompt = pht('Continue without loading library?'); + if (!phutil_console_confirm($prompt)) { + throw $ex; + } + } catch (PhutilLibraryConflictException $ex) { + if ($ex->getLibrary() != 'arcanist') { + throw $ex; + } + + // NOTE: If you are running `arc` against itself, we ignore the library + // conflict created by loading the local `arc` library (in the current + // working directory) and continue without loading it. + + // This means we only execute code in the `arcanist/` directory which is + // associated with the binary you are running, whereas we would normally + // execute local code. + + // This can make `arc` development slightly confusing if your setup is + // especially bizarre, but it allows `arc` to be used in automation + // workflows more easily. For some context, see PHI13. + + $executing_directory = dirname(dirname(__FILE__)); + $working_directory = dirname($location); + + fwrite( + STDERR, + tsprintf( + "** %s ** %s\n", + pht('VERY META'), + pht( + 'You are running one copy of Arcanist (at path "%s") against '. + 'another copy of Arcanist (at path "%s"). Code in the current '. + 'working directory will not be loaded or executed.', + $executing_directory, + $working_directory))); + } + } + + private function newToolset(array $argv) { + $binary = basename($argv[0]); + + $toolsets = ArcanistToolset::newToolsetMap(); + if (!isset($toolsets[$binary])) { + throw new PhutilArgumentUsageException( + pht( + 'Arcanist toolset "%s" is unknown. The Arcanist binary should '. + 'be executed so that "argv[0]" identifies a supported toolset. '. + 'Rename the binary or install the library that provides the '. + 'desired toolset. Current available toolsets: %s.', + $binary, + implode(', ', array_keys($toolsets)))); + } + + return $toolsets[$binary]; + } + + private function newWorkflows(ArcanistToolset $toolset) { + $workflows = id(new PhutilClassMapQuery()) + ->setAncestorClass('ArcanistWorkflow') + ->execute(); + + foreach ($workflows as $key => $workflow) { + if (!$workflow->supportsToolset($toolset)) { + unset($workflows[$key]); + } + } + + $map = array(); + foreach ($workflows as $workflow) { + $key = $workflow->getWorkflowName(); + if (isset($map[$key])) { + throw new Exception( + pht( + 'Two workflows ("%s" and "%s") both have the same name ("%s") '. + 'and both support the current toolset ("%s", "%s"). Each '. + 'workflow in a given toolset must have a unique name.', + get_class($workflow), + get_class($map[$key]), + get_class($toolset), + $toolset->getToolsetKey())); + } + $map[$key] = id(clone $workflow) + ->setToolset($toolset); + } + + return $map; + } + + public function getWorkflows() { + return $this->workflows; + } + + public function getLogEngine() { + return $this->logEngine; + } + + private function applyAliasEffects(array $effects, array $argv) { + assert_instances_of($effects, 'ArcanistAliasEffect'); + + $log = $this->getLogEngine(); + + $command = null; + $arguments = null; + foreach ($effects as $effect) { + $message = $effect->getMessage(); + + if ($message !== null) { + $log->writeInfo(pht('ALIAS'), $message); + } + + if ($effect->getCommand()) { + $command = $effect->getCommand(); + $arguments = $effect->getArguments(); + } + } + + if ($command !== null) { + $argv = array_merge(array($command), $arguments); + } + + return $argv; + } + + private function installSignalHandler() { + $log = $this->getLogEngine(); + + if (!function_exists('pcntl_signal')) { + $log->writeTrace( + pht('PCNTL'), + pht( + 'Unable to install signal handler, pcntl_signal() unavailable. '. + 'Continuing without signal handling.')); + return; + } + + // NOTE: SIGHUP, SIGTERM and SIGWINCH are handled by "PhutilSignalRouter". + // This logic is largely similar to the logic there, but more specific to + // Arcanist workflows. + + pcntl_signal(SIGINT, array($this, 'routeSignal')); + } + + public function routeSignal($signo) { + switch ($signo) { + case SIGINT: + $this->routeInterruptSignal($signo); + break; + } + } + + private function routeInterruptSignal($signo) { + $log = $this->getLogEngine(); + + $last_interrupt = $this->lastInterruptTime; + $now = microtime(true); + $this->lastInterruptTime = $now; + + $should_exit = false; + + // If we received another SIGINT recently, always exit. This implements + // "press ^C twice in quick succession to exit" regardless of what the + // workflow may decide to do. + $interval = 2; + if ($last_interrupt !== null) { + if ($now - $last_interrupt < $interval) { + $should_exit = true; + } + } + + $handler = null; + if (!$should_exit) { + + // Look for an interrupt handler in the current workflow stack. + + $stack = $this->getWorkflowStack(); + foreach ($stack as $workflow) { + if ($workflow->canHandleSignal($signo)) { + $handler = $workflow; + break; + } + } + + // If no workflow in the current execution stack can handle an interrupt + // signal, just exit on the first interrupt. + + if (!$handler) { + $should_exit = true; + } + } + + // It's common for users to ^C on prompts. Write a newline before writing + // a response to the interrupt so the behavior is a little cleaner. This + // also avoids lines that read "^C [ INTERRUPT ] ...". + $log->writeNewline(); + + if ($should_exit) { + $log->writeHint( + pht('INTERRUPT'), + pht('Interrupted by SIGINT (^C).')); + exit(128 + $signo); + } + + $log->writeHint( + pht('INTERRUPT'), + pht('Press ^C again to exit.')); + + $handler->handleSignal($signo); + } + + public function pushWorkflow(ArcanistWorkflow $workflow) { + $this->stack[] = $workflow; + return $this; + } + + public function popWorkflow() { + if (!$this->stack) { + throw new Exception(pht('Trying to pop an empty workflow stack!')); + } + + return array_pop($this->stack); + } + + public function getWorkflowStack() { + return $this->stack; + } + +} diff --git a/src/toolset/ArcanistAlias.php b/src/toolset/ArcanistAlias.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistAlias.php @@ -0,0 +1,144 @@ +trigger = $key; + $alias->toolset = 'arc'; + $alias->command = $value; + } else if ($is_dict) { + try { + PhutilTypeSpec::checkMap( + $value, + array( + 'trigger' => 'string', + 'toolset' => 'string', + 'command' => 'list', + )); + + $alias->trigger = idx($value, 'trigger'); + $alias->toolset = idx($value, 'toolset'); + $alias->command = idx($value, 'command'); + } catch (PhutilTypeCheckException $ex) { + $alias->exception = new PhutilProxyException( + pht( + 'Found invalid alias definition (with key "%s").', + $key), + $ex); + } + } else { + $alias->exception = new Exception( + pht( + 'Expected alias definition (with key "%s") to be a dictionary.', + $key)); + } + + return $alias; + } + + public function setToolset($toolset) { + $this->toolset = $toolset; + return $this; + } + + public function getToolset() { + return $this->toolset; + } + + public function setTrigger($trigger) { + $this->trigger = $trigger; + return $this; + } + + public function getTrigger() { + return $this->trigger; + } + + public function setCommand(array $command) { + $this->command = $command; + return $this; + } + + public function getCommand() { + return $this->command; + } + + public function setException(Exception $exception) { + $this->exception = $exception; + return $this; + } + + public function getException() { + return $this->exception; + } + + public function isShellCommandAlias() { + $command = $this->getCommand(); + if (!$command) { + return false; + } + + $head = head($command); + return preg_match('/^!/', $head); + } + + public function getStorageDictionary() { + return array( + 'trigger' => $this->getTrigger(), + 'toolset' => $this->getToolset(), + 'command' => $this->getCommand(), + ); + } + + public function setConfigurationSource( + ArcanistConfigurationSource $configuration_source) { + $this->configurationSource = $configuration_source; + return $this; + } + + public function getConfigurationSource() { + return $this->configurationSource; + } + +} diff --git a/src/toolset/ArcanistAliasEffect.php b/src/toolset/ArcanistAliasEffect.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistAliasEffect.php @@ -0,0 +1,57 @@ +type = $type; + return $this; + } + + public function getType() { + return $this->type; + } + + public function setCommand($command) { + $this->command = $command; + return $this; + } + + public function getCommand() { + return $this->command; + } + + public function setArguments(array $arguments) { + $this->arguments = $arguments; + return $this; + } + + public function getArguments() { + return $this->arguments; + } + + public function setMessage($message) { + $this->message = $message; + return $this; + } + + public function getMessage() { + return $this->message; + } + +} diff --git a/src/toolset/ArcanistAliasEngine.php b/src/toolset/ArcanistAliasEngine.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistAliasEngine.php @@ -0,0 +1,254 @@ +runtime = $runtime; + return $this; + } + + public function getRuntime() { + return $this->runtime; + } + + public function setToolset(ArcanistToolset $toolset) { + $this->toolset = $toolset; + return $this; + } + + public function getToolset() { + return $this->toolset; + } + + public function setWorkflows(array $workflows) { + assert_instances_of($workflows, 'ArcanistWorkflow'); + $this->workflows = $workflows; + return $this; + } + + public function getWorkflows() { + return $this->workflows; + } + + public function setConfigurationSourceList( + ArcanistConfigurationSourceList $config) { + $this->configurationSourceList = $config; + return $this; + } + + public function getConfigurationSourceList() { + return $this->configurationSourceList; + } + + public function resolveAliases(array $argv) { + $aliases_key = ArcanistArcConfigurationEngineExtension::KEY_ALIASES; + $source_list = $this->getConfigurationSourceList(); + $aliases = $source_list->getConfig($aliases_key); + + $results = array(); + + // Identify aliases which had some kind of format or specification issue + // when loading config. We could possibly do this earlier, but it's nice + // to handle all the alias stuff in one place. + + foreach ($aliases as $key => $alias) { + $exception = $alias->getException(); + + if (!$exception) { + continue; + } + + // This alias is not defined properly, so we're going to ignore it. + unset($aliases[$key]); + + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_CONFIGURATION) + ->setMessage( + pht( + 'Configuration source ("%s") defines an invalid alias, which '. + 'will be ignored: %s', + $alias->getConfigurationSource()->getSourceDisplayName()), + $exception->getMessage()); + } + + $command = array_shift($argv); + + $stack = array(); + return $this->resolveAliasesForCommand( + $aliases, + $command, + $argv, + $results, + $stack); + } + + private function resolveAliasesForCommand( + array $aliases, + $command, + array $argv, + array $results, + array $stack) { + + $toolset = $this->getToolset(); + $toolset_key = $toolset->getToolsetKey(); + + // If we have a command which resolves to a real workflow, match it and + // finish resolution. You can not overwrite a real workflow with an alias. + + $workflows = $this->getWorkflows(); + if (isset($workflows[$command])) { + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_RESOLUTION) + ->setCommand($command) + ->setArguments($argv); + return $results; + } + + // Find all the aliases which match whatever the user typed, like "draft". + // We look for aliases in other toolsets, too, so we can provide the user + // a hint when they type "phage draft" and mean "arc draft". + + $matches = array(); + $toolset_matches = array(); + foreach ($aliases as $alias) { + if ($alias->getTrigger() === $command) { + $matches[] = $alias; + if ($alias->getToolset() == $toolset_key) { + $toolset_matches[] = $alias; + } + } + } + + if (!$toolset_matches) { + + // If the user typed "phage draft" and meant "arc draft", give them a + // hint that the alias exists somewhere else and they may have specified + // the wrong toolset. + + foreach ($matches as $match) { + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_SUGGEST) + ->setMessage( + pht( + 'No "%s %s" alias is defined, did you mean "%s %s"?', + $toolset_key, + $command, + $match->getToolset(), + $command)); + } + + // If the user misspells a command (like "arc hlep") and it doesn't match + // anything (no alias or workflow), we want to pass it through unmodified + // and let the parser try to correct the spelling into a real workflow + // later on. + + // However, if the user correctly types a command (like "arc draft") that + // resolves at least once (so it hits a valid alias) but does not + // ultimately resolve into a valid workflow, we want to treat this as a + // hard failure. + + // This could happen if you manually defined a bad alias, or a workflow + // you'd previously aliased to was removed, or you stacked aliases and + // then deleted one. + + if ($stack) { + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_NOTFOUND) + ->setMessage( + pht( + 'Alias resolved to "%s", but this is not a valid workflow or '. + 'alias name. This alias or workflow might have previously '. + 'existed and been removed.', + $command)); + } else { + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_RESOLUTION) + ->setCommand($command) + ->setArguments($argv); + } + + return $results; + } + + $alias = array_pop($toolset_matches); + foreach ($toolset_matches as $ignored_match) { + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_IGNORED) + ->setMessage( + pht( + 'Multiple configuration sources define an alias for "%s %s". '. + 'The definition in "%s" will be ignored.', + $toolset_key, + $command, + $ignored_match->getConfigurationSource()->getSourceDisplayName())); + } + + if ($alias->isShellCommandAlias()) { + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_SHELL) + ->setMessage( + pht( + '%s %s -> $ %s', + $toolset_key, + $command, + $alias->getShellCommand())) + ->setCommand($command) + ->setArgv($argv); + return $results; + } + + $alias_argv = $alias->getCommand(); + $alias_command = array_shift($alias_argv); + + if (isset($stack[$alias_command])) { + + $cycle = array_keys($stack); + $cycle[] = $alias_command; + $cycle = implode(' -> ', $cycle); + + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_CYCLE) + ->setMessage( + pht( + 'Alias definitions form a cycle which can not be resolved: %s.', + $cycle)); + + return $results; + } + + $stack[$alias_command] = true; + + $stack_limit = 16; + if (count($stack) >= $stack_limit) { + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_STACK) + ->setMessage( + pht( + 'Alias definitions form an unreasonably deep stack. A chain of '. + 'aliases may not resolve more than %s times.', + new PhutilNumber($stack_limit))); + return $results; + } + + $results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_ALIAS) + ->setMessage( + pht( + '%s %s -> %s %s', + $toolset_key, + $command, + $toolset_key, + $alias_command)); + + $argv = array_merge($alias_argv, $argv); + + return $this->resolveAliasesForCommand( + $aliases, + $alias_command, + $argv, + $results, + $stack); + } + + protected function newEffect($effect_type) { + return id(new ArcanistAliasEffect()) + ->setType($effect_type); + } + +} diff --git a/src/toolset/ArcanistArcToolset.php b/src/toolset/ArcanistArcToolset.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistArcToolset.php @@ -0,0 +1,26 @@ + 'conduit-uri', + 'param' => 'uri', + 'help' => pht('Connect to Phabricator install specified by __uri__.'), + ), + array( + 'name' => 'conduit-token', + 'param' => 'token', + 'help' => pht('Use a specific authentication token.'), + ), + array( + 'name' => 'anonymous', + 'help' => pht('Run workflow as a public user, without authenticating.'), + ), + ); + } + +} diff --git a/src/toolset/ArcanistPhageToolset.php b/src/toolset/ArcanistPhageToolset.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistPhageToolset.php @@ -0,0 +1,8 @@ +workflow = $workflow; + return $this; + } + + public function getWorkflow() { + return $this->workflow; + } + + public function isExecutable() { + return true; + } + + public function execute(PhutilArgumentParser $args) { + return $this->getWorkflow()->executeWorkflow($args); + } + +} diff --git a/src/toolset/ArcanistPrompt.php b/src/toolset/ArcanistPrompt.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistPrompt.php @@ -0,0 +1,151 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setWorkflow(ArcanistWorkflow $workflow) { + $this->workflow = $workflow; + return $this; + } + + public function getWorkflow() { + return $this->workflow; + } + + public function setDescription($description) { + $this->description = $description; + return $this; + } + + public function getDescription() { + return $this->description; + } + + public function setQuery($query) { + $this->query = $query; + return $this; + } + + public function getQuery() { + return $this->query; + } + + public function execute() { + $workflow = $this->getWorkflow(); + if ($workflow) { + $workflow_ok = $workflow->hasPrompt($this->getKey()); + } else { + $workflow_ok = false; + } + + if (!$workflow_ok) { + throw new Exception( + pht( + 'Prompt ("%s") is executing, but it is not properly bound to the '. + 'invoking workflow. You may have called "newPrompt()" to execute a '. + 'prompt instead of "getPrompt()". Use "newPrompt()" when defining '. + 'prompts and "getPrompt()" when executing them.', + $this->getKey())); + } + + $query = $this->getQuery(); + if (!strlen($query)) { + throw new Exception( + pht( + 'Prompt ("%s") has no query text!', + $this->getKey())); + } + + $options = '[y/N]'; + $default = 'N'; + + try { + phutil_console_require_tty(); + } catch (PhutilConsoleStdinNotInteractiveException $ex) { + // TOOLSETS: Clean this up to provide more details to the user about how + // they can configure prompts to be answered. + + // Throw after echoing the prompt so the user has some idea what happened. + echo $query."\n"; + throw $ex; + } + + // NOTE: We're making stdin nonblocking so that we can respond to signals + // immediately. If we don't, and you ^C during a prompt, the program does + // not handle the signal until fgets() returns. + + $stdin = fopen('php://stdin', 'r'); + if (!$stdin) { + throw new Exception(pht('Failed to open stdin for reading.')); + } + + $ok = stream_set_blocking($stdin, false); + if (!$ok) { + throw new Exception(pht('Unable to set stdin nonblocking.')); + } + + echo "\n"; + + $result = null; + while (true) { + echo tsprintf( + '** %s ** %s %s ', + '>>>', + $query, + $options); + + while (true) { + $read = array($stdin); + $write = array(); + $except = array(); + + $ok = stream_select($read, $write, $except, 1); + if ($ok === false) { + throw new Exception(pht('stream_select() failed!')); + } + + $response = fgets($stdin); + if (!strlen($response)) { + continue; + } + + break; + } + + $response = trim($response); + if (!strlen($response)) { + $response = $default; + } + + if (phutil_utf8_strtolower($response) == 'y') { + $result = true; + break; + } + + if (phutil_utf8_strtolower($response) == 'n') { + $result = false; + break; + } + } + + if (!$result) { + throw new ArcanistUserAbortException(); + } + + } + +} diff --git a/src/toolset/ArcanistToolset.php b/src/toolset/ArcanistToolset.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistToolset.php @@ -0,0 +1,22 @@ +getPhobjectClassConstant('TOOLSETKEY'); + } + + final public static function newToolsetMap() { + $toolsets = id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getToolsetKey') + ->execute(); + + return $toolsets; + } + + public function getToolsetArguments() { + return array(); + } + +} diff --git a/src/toolset/ArcanistWorkflowArgument.php b/src/toolset/ArcanistWorkflowArgument.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistWorkflowArgument.php @@ -0,0 +1,74 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setWildcard($wildcard) { + $this->wildcard = $wildcard; + return $this; + } + + public function getWildcard() { + return $this->wildcard; + } + + public function getPhutilSpecification() { + $spec = array( + 'name' => $this->getKey(), + ); + + if ($this->getWildcard()) { + $spec['wildcard'] = true; + } + + $parameter = $this->getParameter(); + if ($parameter !== null) { + $spec['param'] = $parameter; + } + + return $spec; + } + + public function setHelp($help) { + $this->help = $help; + return $this; + } + + public function getHelp() { + return $this->help; + } + + public function setParameter($parameter) { + $this->parameter = $parameter; + return $this; + } + + public function getParameter() { + return $this->parameter; + } + + public function setIsPathArgument($is_path_argument) { + $this->isPathArgument = $is_path_argument; + return $this; + } + + public function getIsPathArgument() { + return $this->isPathArgument; + } + +} diff --git a/src/toolset/ArcanistWorkflowInformation.php b/src/toolset/ArcanistWorkflowInformation.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistWorkflowInformation.php @@ -0,0 +1,27 @@ +help = $help; + return $this; + } + + public function getHelp() { + return $this->help; + } + + public function addExample($example) { + $this->examples[] = $example; + return $this; + } + + public function getExamples() { + return $this->examples; + } + +} diff --git a/src/workflow/ArcanistLiberateWorkflow.php b/src/workflow/ArcanistLiberateWorkflow.php --- a/src/workflow/ArcanistLiberateWorkflow.php +++ b/src/workflow/ArcanistLiberateWorkflow.php @@ -141,6 +141,8 @@ throw new ArcanistUsageException( pht("Unknown library version '%s'!", $version)); } + + echo tsprintf("%s\n", pht('Done.')); } private function getLibraryFormatVersion($path) { diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -108,6 +108,10 @@ */ abstract public function getCommandHelp(); + final public function supportsToolset($toolset) { + return ($toolset === 'arc'); + } + /* -( Conduit )------------------------------------------------------------ */ diff --git a/src/workingcopy/ArcanistGitWorkingCopy.php b/src/workingcopy/ArcanistGitWorkingCopy.php new file mode 100644 --- /dev/null +++ b/src/workingcopy/ArcanistGitWorkingCopy.php @@ -0,0 +1,25 @@ +getPath('.git'); + } + + protected function newWorkingCopyFromDirectories( + $working_directory, + $ancestor_directory) { + + if (!Filesystem::pathExists($ancestor_directory.'/.git')) { + return null; + } + + return new self(); + } + + public function newRepositoryAPI() { + return new ArcanistGitAPI($this->getPath()); + } + +} diff --git a/src/workingcopy/ArcanistMercurialWorkingCopy.php b/src/workingcopy/ArcanistMercurialWorkingCopy.php new file mode 100644 --- /dev/null +++ b/src/workingcopy/ArcanistMercurialWorkingCopy.php @@ -0,0 +1,25 @@ +getPath('.hg'); + } + + protected function newWorkingCopyFromDirectories( + $working_directory, + $ancestor_directory) { + + if (!Filesystem::pathExists($ancestor_directory.'/.hg')) { + return null; + } + + return new self(); + } + + public function newRepositoryAPI() { + return new ArcanistMercurialAPI($this->getPath()); + } + +} diff --git a/src/workingcopy/ArcanistSubversionWorkingCopy.php b/src/workingcopy/ArcanistSubversionWorkingCopy.php new file mode 100644 --- /dev/null +++ b/src/workingcopy/ArcanistSubversionWorkingCopy.php @@ -0,0 +1,77 @@ +getWorkingDirectory()); + $root = $this->getPath(); + foreach ($paths as $path) { + if (!Filesystem::isDescendant($path, $root)) { + break; + } + + $candidate = $path.'/.arcconfig'; + if (Filesystem::pathExists($candidate)) { + return $candidate; + } + } + + return parent::getProjectConfigurationFilePath(); + } + + public function getMetadataDirectory() { + return $this->getPath('.svn'); + } + + protected function newWorkingCopyFromDirectories( + $working_directory, + $ancestor_directory) { + + if (!Filesystem::pathExists($ancestor_directory.'/.svn')) { + return null; + } + + return new self(); + } + + protected function selectFromNestedWorkingCopies(array $candidates) { + // To select the best working copy in Subversion, we first walk up the + // tree looking for a working copy with an ".arcconfig" file. If we find + // one, this anchors us. + + foreach (array_reverse($candidates) as $candidate) { + $arcconfig = $candidate->getPath('.arcconfig'); + if (Filesystem::pathExists($arcconfig)) { + return $candidate; + } + } + + // If we didn't find one, we select the outermost working copy. This is + // because older versions of Subversion (prior to 1.7) put a ".svn" file + // in every directory, and all versions of Subversion allow you to check + // out any subdirectory of the project as a working copy. + + // We could possibly refine this by testing if the working copy was made + // with a recent version of Subversion and picking the deepest working copy + // if it was, similar to Git and Mercurial. + + return head($candidates); + } + + public function newRepositoryAPI() { + return new ArcanistSubversionAPI($this->getPath()); + } + +} diff --git a/src/workingcopy/ArcanistWorkingCopy.php b/src/workingcopy/ArcanistWorkingCopy.php new file mode 100644 --- /dev/null +++ b/src/workingcopy/ArcanistWorkingCopy.php @@ -0,0 +1,115 @@ +setAncestorClass(__CLASS__) + ->execute(); + + $paths = Filesystem::walkToRoot($path); + $paths = array_reverse($paths); + + $candidates = array(); + foreach ($paths as $path_key => $ancestor_path) { + foreach ($working_types as $working_type) { + + $working_copy = $working_type->newWorkingCopyFromDirectories( + $path, + $ancestor_path); + if (!$working_copy) { + continue; + } + + $working_copy->path = $ancestor_path; + $working_copy->workingDirectory = $path; + + $candidates[] = $working_copy; + } + } + + // If we've found multiple candidate working copies, we need to pick one. + // We let the innermost working copy pick the best candidate from among + // candidates of the same type. The rules for Git and Mercurial differ + // slightly from the rules for Subversion. + + if ($candidates) { + $deepest = last($candidates); + + foreach ($candidates as $key => $candidate) { + if (get_class($candidate) != get_class($deepest)) { + unset($candidates[$key]); + } + } + $candidates = array_values($candidates); + + return $deepest->selectFromNestedWorkingCopies($candidates); + } + + return null; + } + + abstract protected function newWorkingCopyFromDirectories( + $working_directory, + $ancestor_directory); + + final public function getPath($to_file = null) { + return Filesystem::concatenatePaths( + array( + $this->path, + $to_file, + )); + } + + final public function getWorkingDirectory() { + return $this->workingDirectory; + } + + public function getProjectConfigurationFilePath() { + return $this->getPath('.arcconfig'); + } + + public function getLocalConfigurationFilePath() { + if ($this->hasMetadataDirectory()) { + return $this->getMetadataPath('arc/config'); + } + + return null; + } + + public function getMetadataDirectory() { + return null; + } + + final public function hasMetadataDirectory() { + return ($this->getMetadataDirectory() !== null); + } + + final public function getMetadataPath($to_file = null) { + if (!$this->hasMetadataDirectory()) { + throw new Exception( + pht( + 'This working copy has no metadata directory, so you can not '. + 'resolve metadata paths within it.')); + } + + return Filesystem::concatenatePaths( + array( + $this->getMetadataDirectory(), + $to_file, + )); + } + + protected function selectFromNestedWorkingCopies(array $candidates) { + // Normally, the best working copy in a stack is the deepest working copy. + // Subversion uses slightly different rules. + return last($candidates); + } + + abstract public function newRepositoryAPI(); + +} diff --git a/src/workingcopy/ArcanistWorkingCopyPath.php b/src/workingcopy/ArcanistWorkingCopyPath.php new file mode 100644 --- /dev/null +++ b/src/workingcopy/ArcanistWorkingCopyPath.php @@ -0,0 +1,129 @@ +path = $path; + return $this; + } + + public function getPath() { + return $this->path; + } + + public function setData($data) { + $this->data = $data; + return $this; + } + + public function getData() { + if ($this->data === null) { + throw new Exception( + pht( + 'No data provided for path "%s".', + $this->getDescription())); + } + + return $this->data; + } + + public function getDataAsLines() { + if ($this->dataAsLines === null) { + $lines = phutil_split_lines($this->getData()); + $this->dataAsLines = $lines; + } + + return $this->dataAsLines; + } + + public function setMode($mode) { + $this->mode = $mode; + return $this; + } + + public function getMode() { + if ($this->mode === null) { + throw new Exception( + pht( + 'No mode provided for path "%s".', + $this->getDescription())); + } + + return $this->mode; + } + + public function isExecutable() { + $mode = $this->getMode(); + return (bool)($mode & 0111); + } + + public function isBinary() { + if ($this->binary === null) { + $data = $this->getData(); + $is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($data); + $this->binary = $is_binary; + } + + return $this->binary; + } + + public function getMimeType() { + if ($this->mimeType === null) { + // TOOLSETS: This is not terribly efficient on real repositories since + // it re-writes files which are often already on disk, but is good for + // unit tests. + + $tmp = new TempFile(); + Filesystem::writeFile($tmp, $this->getData()); + $mime = Filesystem::getMimeType($tmp); + + $this->mimeType = $mime; + } + + return $this->mimeType; + } + + + public function getBasename() { + return basename($this->getPath()); + } + + public function getLineAndCharFromOffset($offset) { + if ($this->charMap === null) { + $char_map = array(); + $line_map = array(); + + $lines = $this->getDataAsLines(); + + $line_number = 0; + $line_start = 0; + foreach ($lines as $line) { + $len = strlen($line); + $line_map[] = $line_start; + $line_start += $len; + for ($ii = 0; $ii < $len; $ii++) { + $char_map[] = $line_number; + } + $line_number++; + } + + $this->charMap = $char_map; + $this->lineMap = $line_map; + } + + $line = $this->charMap[$offset]; + $char = $offset - $this->lineMap[$line]; + + return array($line, $char); + } + +} diff --git a/support/init/init-arcanist.php b/support/init/init-arcanist.php new file mode 100644 --- /dev/null +++ b/support/init/init-arcanist.php @@ -0,0 +1,6 @@ +execute($argv); diff --git a/support/init/init-script.php b/support/init/init-script.php --- a/support/init/init-script.php +++ b/support/init/init-script.php @@ -56,6 +56,20 @@ ini_set($config_key, $config_value); } + $php_version = phpversion(); + $min_version = '5.5.0'; + if (version_compare($php_version, $min_version, '<')) { + echo sprintf( + 'UPGRADE PHP: '. + 'The installed version of PHP ("%s") is too old to run Arcanist. '. + 'Update PHP to at least the minimum required version ("%s").', + $php_version, + $min_version); + echo "\n"; + + exit(1); + } + if (!ini_get('date.timezone')) { // If the timezone isn't set, PHP issues a warning whenever you try to parse // a date (like those from Git or Mercurial logs), even if the date contains