diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f923b0bd..31e4bd10 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,1916 +1,1934 @@ 2, 'class' => array( 'AASTNode' => 'parser/aast/api/AASTNode.php', 'AASTNodeList' => 'parser/aast/api/AASTNodeList.php', 'AASTToken' => 'parser/aast/api/AASTToken.php', 'AASTTree' => 'parser/aast/api/AASTTree.php', 'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php', 'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php', 'ArcanistAbstractMethodBodyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractMethodBodyXHPASTLinterRule.php', 'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => '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' => 'toolset/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', 'ArcanistArcWorkflow' => 'toolset/workflow/ArcanistArcWorkflow.php', 'ArcanistArrayCombineXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArrayCombineXHPASTLinterRule.php', 'ArcanistArrayCombineXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistArrayCombineXHPASTLinterRuleTestCase.php', 'ArcanistArrayIndexSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArrayIndexSpacingXHPASTLinterRule.php', 'ArcanistArrayIndexSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistArrayIndexSpacingXHPASTLinterRuleTestCase.php', 'ArcanistArraySeparatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArraySeparatorXHPASTLinterRule.php', 'ArcanistArraySeparatorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistArraySeparatorXHPASTLinterRuleTestCase.php', 'ArcanistArrayValueXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArrayValueXHPASTLinterRule.php', 'ArcanistArrayValueXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistArrayValueXHPASTLinterRuleTestCase.php', 'ArcanistBaseCommitParser' => 'parser/ArcanistBaseCommitParser.php', 'ArcanistBaseCommitParserTestCase' => 'parser/__tests__/ArcanistBaseCommitParserTestCase.php', 'ArcanistBaseXHPASTLinter' => 'lint/linter/ArcanistBaseXHPASTLinter.php', 'ArcanistBinaryExpressionSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBinaryExpressionSpacingXHPASTLinterRule.php', 'ArcanistBinaryExpressionSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBinaryExpressionSpacingXHPASTLinterRuleTestCase.php', 'ArcanistBinaryNumericScalarCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBinaryNumericScalarCasingXHPASTLinterRule.php', 'ArcanistBinaryNumericScalarCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBinaryNumericScalarCasingXHPASTLinterRuleTestCase.php', 'ArcanistBlacklistedFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBlacklistedFunctionXHPASTLinterRule.php', 'ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase.php', 'ArcanistBlindlyTrustHTTPEngineExtension' => 'configuration/ArcanistBlindlyTrustHTTPEngineExtension.php', 'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php', 'ArcanistBoolConfigOption' => 'config/option/ArcanistBoolConfigOption.php', 'ArcanistBraceFormattingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php', 'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBraceFormattingXHPASTLinterRuleTestCase.php', 'ArcanistBranchRef' => 'ref/ArcanistBranchRef.php', 'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php', 'ArcanistBrowseCommitHardpointQuery' => 'browse/query/ArcanistBrowseCommitHardpointQuery.php', 'ArcanistBrowseCommitURIHardpointQuery' => 'browse/query/ArcanistBrowseCommitURIHardpointQuery.php', 'ArcanistBrowseObjectNameURIHardpointQuery' => 'browse/query/ArcanistBrowseObjectNameURIHardpointQuery.php', 'ArcanistBrowsePathURIHardpointQuery' => 'browse/query/ArcanistBrowsePathURIHardpointQuery.php', 'ArcanistBrowseRef' => 'browse/ref/ArcanistBrowseRef.php', 'ArcanistBrowseRefInspector' => 'inspector/ArcanistBrowseRefInspector.php', 'ArcanistBrowseRevisionURIHardpointQuery' => 'browse/query/ArcanistBrowseRevisionURIHardpointQuery.php', 'ArcanistBrowseURIHardpointQuery' => 'browse/query/ArcanistBrowseURIHardpointQuery.php', 'ArcanistBrowseURIRef' => 'browse/ref/ArcanistBrowseURIRef.php', 'ArcanistBrowseWorkflow' => 'browse/workflow/ArcanistBrowseWorkflow.php', 'ArcanistBuildBuildplanHardpointQuery' => 'ref/build/ArcanistBuildBuildplanHardpointQuery.php', 'ArcanistBuildPlanRef' => 'ref/buildplan/ArcanistBuildPlanRef.php', 'ArcanistBuildPlanSymbolRef' => 'ref/buildplan/ArcanistBuildPlanSymbolRef.php', 'ArcanistBuildRef' => 'ref/build/ArcanistBuildRef.php', 'ArcanistBuildSymbolRef' => 'ref/build/ArcanistBuildSymbolRef.php', 'ArcanistBuildableBuildsHardpointQuery' => 'ref/buildable/ArcanistBuildableBuildsHardpointQuery.php', 'ArcanistBuildableRef' => 'ref/buildable/ArcanistBuildableRef.php', 'ArcanistBuildableSymbolRef' => 'ref/buildable/ArcanistBuildableSymbolRef.php', 'ArcanistBundle' => 'parser/ArcanistBundle.php', 'ArcanistBundleTestCase' => 'parser/__tests__/ArcanistBundleTestCase.php', 'ArcanistCSSLintLinter' => 'lint/linter/ArcanistCSSLintLinter.php', 'ArcanistCSSLintLinterTestCase' => 'lint/linter/__tests__/ArcanistCSSLintLinterTestCase.php', 'ArcanistCSharpLinter' => 'lint/linter/ArcanistCSharpLinter.php', 'ArcanistCallConduitWorkflow' => 'workflow/ArcanistCallConduitWorkflow.php', 'ArcanistCallParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCallParenthesesXHPASTLinterRule.php', 'ArcanistCallParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistCallParenthesesXHPASTLinterRuleTestCase.php', 'ArcanistCallTimePassByReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCallTimePassByReferenceXHPASTLinterRule.php', 'ArcanistCallTimePassByReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistCallTimePassByReferenceXHPASTLinterRuleTestCase.php', 'ArcanistCapabilityNotSupportedException' => 'workflow/exception/ArcanistCapabilityNotSupportedException.php', 'ArcanistCastSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCastSpacingXHPASTLinterRule.php', 'ArcanistCastSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistCastSpacingXHPASTLinterRuleTestCase.php', 'ArcanistCheckstyleXMLLintRenderer' => 'lint/renderer/ArcanistCheckstyleXMLLintRenderer.php', 'ArcanistChmodLinter' => 'lint/linter/ArcanistChmodLinter.php', 'ArcanistChmodLinterTestCase' => 'lint/linter/__tests__/ArcanistChmodLinterTestCase.php', 'ArcanistClassExtendsObjectXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassExtendsObjectXHPASTLinterRule.php', 'ArcanistClassExtendsObjectXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistClassExtendsObjectXHPASTLinterRuleTestCase.php', 'ArcanistClassFilenameMismatchXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassFilenameMismatchXHPASTLinterRule.php', 'ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule.php', 'ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase.php', 'ArcanistClassNameLiteralXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassNameLiteralXHPASTLinterRule.php', 'ArcanistClassNameLiteralXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistClassNameLiteralXHPASTLinterRuleTestCase.php', 'ArcanistCloseRevisionWorkflow' => 'workflow/ArcanistCloseRevisionWorkflow.php', 'ArcanistClosureLinter' => 'lint/linter/ArcanistClosureLinter.php', 'ArcanistClosureLinterTestCase' => 'lint/linter/__tests__/ArcanistClosureLinterTestCase.php', 'ArcanistCoffeeLintLinter' => 'lint/linter/ArcanistCoffeeLintLinter.php', 'ArcanistCoffeeLintLinterTestCase' => 'lint/linter/__tests__/ArcanistCoffeeLintLinterTestCase.php', 'ArcanistCommand' => 'toolset/command/ArcanistCommand.php', 'ArcanistCommentRemover' => 'parser/ArcanistCommentRemover.php', 'ArcanistCommentRemoverTestCase' => 'parser/__tests__/ArcanistCommentRemoverTestCase.php', 'ArcanistCommentSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentSpacingXHPASTLinterRule.php', 'ArcanistCommentStyleXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentStyleXHPASTLinterRule.php', 'ArcanistCommentStyleXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistCommentStyleXHPASTLinterRuleTestCase.php', 'ArcanistCommitRef' => 'ref/commit/ArcanistCommitRef.php', 'ArcanistCommitSymbolRef' => 'ref/commit/ArcanistCommitSymbolRef.php', 'ArcanistCommitSymbolRefInspector' => 'ref/commit/ArcanistCommitSymbolRefInspector.php', 'ArcanistCommitUpstreamHardpointQuery' => 'query/ArcanistCommitUpstreamHardpointQuery.php', 'ArcanistCommitWorkflow' => 'workflow/ArcanistCommitWorkflow.php', 'ArcanistCompilerLintRenderer' => 'lint/renderer/ArcanistCompilerLintRenderer.php', 'ArcanistComposerLinter' => 'lint/linter/ArcanistComposerLinter.php', 'ArcanistComprehensiveLintEngine' => 'lint/engine/ArcanistComprehensiveLintEngine.php', 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php', 'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConcatenationOperatorXHPASTLinterRuleTestCase.php', 'ArcanistConduitCall' => 'conduit/ArcanistConduitCall.php', 'ArcanistConduitEngine' => 'conduit/ArcanistConduitEngine.php', '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', 'ArcanistConstructorParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConstructorParenthesesXHPASTLinterRuleTestCase.php', 'ArcanistContinueInsideSwitchXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistContinueInsideSwitchXHPASTLinterRule.php', 'ArcanistContinueInsideSwitchXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistContinueInsideSwitchXHPASTLinterRuleTestCase.php', 'ArcanistControlStatementSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistControlStatementSpacingXHPASTLinterRule.php', 'ArcanistControlStatementSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistControlStatementSpacingXHPASTLinterRuleTestCase.php', 'ArcanistCoverWorkflow' => 'workflow/ArcanistCoverWorkflow.php', 'ArcanistCppcheckLinter' => 'lint/linter/ArcanistCppcheckLinter.php', 'ArcanistCppcheckLinterTestCase' => 'lint/linter/__tests__/ArcanistCppcheckLinterTestCase.php', 'ArcanistCpplintLinter' => 'lint/linter/ArcanistCpplintLinter.php', 'ArcanistCpplintLinterTestCase' => 'lint/linter/__tests__/ArcanistCpplintLinterTestCase.php', 'ArcanistCurlyBraceArrayIndexXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCurlyBraceArrayIndexXHPASTLinterRule.php', 'ArcanistCurlyBraceArrayIndexXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistCurlyBraceArrayIndexXHPASTLinterRuleTestCase.php', 'ArcanistDeclarationParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDeclarationParenthesesXHPASTLinterRule.php', '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', 'ArcanistDiffHunk' => 'parser/diff/ArcanistDiffHunk.php', 'ArcanistDiffParser' => 'parser/ArcanistDiffParser.php', 'ArcanistDiffParserTestCase' => 'parser/__tests__/ArcanistDiffParserTestCase.php', 'ArcanistDiffUtils' => 'difference/ArcanistDiffUtils.php', 'ArcanistDiffUtilsTestCase' => 'difference/__tests__/ArcanistDiffUtilsTestCase.php', 'ArcanistDiffVectorNode' => 'difference/ArcanistDiffVectorNode.php', 'ArcanistDiffVectorTree' => 'difference/ArcanistDiffVectorTree.php', 'ArcanistDiffWorkflow' => 'workflow/ArcanistDiffWorkflow.php', 'ArcanistDifferentialCommitMessage' => 'differential/ArcanistDifferentialCommitMessage.php', 'ArcanistDifferentialCommitMessageParserException' => 'differential/ArcanistDifferentialCommitMessageParserException.php', 'ArcanistDifferentialDependencyGraph' => 'differential/ArcanistDifferentialDependencyGraph.php', 'ArcanistDifferentialRevisionHash' => 'differential/constants/ArcanistDifferentialRevisionHash.php', 'ArcanistDifferentialRevisionStatus' => 'differential/constants/ArcanistDifferentialRevisionStatus.php', 'ArcanistDisplayRef' => 'ref/ArcanistDisplayRef.php', 'ArcanistDisplayRefInterface' => 'ref/ArcanistDisplayRefInterface.php', 'ArcanistDoubleQuoteXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDoubleQuoteXHPASTLinterRule.php', 'ArcanistDoubleQuoteXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDoubleQuoteXHPASTLinterRuleTestCase.php', 'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php', 'ArcanistDuplicateKeysInArrayXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDuplicateKeysInArrayXHPASTLinterRule.php', 'ArcanistDuplicateKeysInArrayXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDuplicateKeysInArrayXHPASTLinterRuleTestCase.php', 'ArcanistDuplicateSwitchCaseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDuplicateSwitchCaseXHPASTLinterRule.php', 'ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase.php', 'ArcanistDynamicDefineXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDynamicDefineXHPASTLinterRule.php', 'ArcanistDynamicDefineXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDynamicDefineXHPASTLinterRuleTestCase.php', 'ArcanistElseIfUsageXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistElseIfUsageXHPASTLinterRule.php', 'ArcanistElseIfUsageXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistElseIfUsageXHPASTLinterRuleTestCase.php', 'ArcanistEmptyFileXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistEmptyFileXHPASTLinterRule.php', 'ArcanistEmptyStatementXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistEmptyStatementXHPASTLinterRule.php', 'ArcanistEmptyStatementXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistEmptyStatementXHPASTLinterRuleTestCase.php', 'ArcanistEventType' => 'events/constant/ArcanistEventType.php', 'ArcanistExitExpressionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExitExpressionXHPASTLinterRule.php', 'ArcanistExitExpressionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistExitExpressionXHPASTLinterRuleTestCase.php', 'ArcanistExportWorkflow' => 'workflow/ArcanistExportWorkflow.php', 'ArcanistExternalLinter' => 'lint/linter/ArcanistExternalLinter.php', 'ArcanistExternalLinterTestCase' => 'lint/linter/__tests__/ArcanistExternalLinterTestCase.php', 'ArcanistExtractUseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php', 'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistExtractUseXHPASTLinterRuleTestCase.php', 'ArcanistFeatureBaseWorkflow' => 'workflow/ArcanistFeatureBaseWorkflow.php', 'ArcanistFeatureWorkflow' => 'workflow/ArcanistFeatureWorkflow.php', 'ArcanistFileConfigurationSource' => 'config/source/ArcanistFileConfigurationSource.php', 'ArcanistFileDataRef' => 'upload/ArcanistFileDataRef.php', 'ArcanistFileRef' => 'ref/file/ArcanistFileRef.php', 'ArcanistFileSymbolRef' => 'ref/file/ArcanistFileSymbolRef.php', 'ArcanistFileUploader' => 'upload/ArcanistFileUploader.php', 'ArcanistFilenameLinter' => 'lint/linter/ArcanistFilenameLinter.php', 'ArcanistFilenameLinterTestCase' => 'lint/linter/__tests__/ArcanistFilenameLinterTestCase.php', 'ArcanistFilesystemAPI' => 'repository/api/ArcanistFilesystemAPI.php', 'ArcanistFilesystemConfigurationSource' => 'config/source/ArcanistFilesystemConfigurationSource.php', 'ArcanistFilesystemWorkingCopy' => 'workingcopy/ArcanistFilesystemWorkingCopy.php', 'ArcanistFlake8Linter' => 'lint/linter/ArcanistFlake8Linter.php', 'ArcanistFlake8LinterTestCase' => 'lint/linter/__tests__/ArcanistFlake8LinterTestCase.php', 'ArcanistFormattedStringXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistFormattedStringXHPASTLinterRule.php', 'ArcanistFormattedStringXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistFormattedStringXHPASTLinterRuleTestCase.php', 'ArcanistFunctionCallShouldBeTypeCastXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistFunctionCallShouldBeTypeCastXHPASTLinterRule.php', 'ArcanistFunctionCallShouldBeTypeCastXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistFunctionCallShouldBeTypeCastXHPASTLinterRuleTestCase.php', 'ArcanistFutureLinter' => 'lint/linter/ArcanistFutureLinter.php', 'ArcanistGeneratedLinter' => 'lint/linter/ArcanistGeneratedLinter.php', 'ArcanistGeneratedLinterTestCase' => 'lint/linter/__tests__/ArcanistGeneratedLinterTestCase.php', 'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php', 'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php', 'ArcanistGitCommitMessageHardpointQuery' => 'query/ArcanistGitCommitMessageHardpointQuery.php', 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php', - 'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php', + 'ArcanistGitLandEngine' => 'land/engine/ArcanistGitLandEngine.php', 'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php', 'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php', 'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php', 'ArcanistGlobalVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistGlobalVariableXHPASTLinterRule.php', 'ArcanistGlobalVariableXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistGlobalVariableXHPASTLinterRuleTestCase.php', 'ArcanistGoLintLinter' => 'lint/linter/ArcanistGoLintLinter.php', 'ArcanistGoLintLinterTestCase' => 'lint/linter/__tests__/ArcanistGoLintLinterTestCase.php', 'ArcanistGoTestResultParser' => 'unit/parser/ArcanistGoTestResultParser.php', 'ArcanistGoTestResultParserTestCase' => 'unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php', 'ArcanistHLintLinter' => 'lint/linter/ArcanistHLintLinter.php', 'ArcanistHLintLinterTestCase' => 'lint/linter/__tests__/ArcanistHLintLinterTestCase.php', 'ArcanistHardpoint' => 'hardpoint/ArcanistHardpoint.php', 'ArcanistHardpointEngine' => 'hardpoint/ArcanistHardpointEngine.php', 'ArcanistHardpointFutureList' => 'hardpoint/ArcanistHardpointFutureList.php', 'ArcanistHardpointList' => 'hardpoint/ArcanistHardpointList.php', 'ArcanistHardpointObject' => 'hardpoint/ArcanistHardpointObject.php', 'ArcanistHardpointQuery' => 'hardpoint/ArcanistHardpointQuery.php', 'ArcanistHardpointRequest' => 'hardpoint/ArcanistHardpointRequest.php', 'ArcanistHardpointRequestList' => 'hardpoint/ArcanistHardpointRequestList.php', 'ArcanistHardpointTask' => 'hardpoint/ArcanistHardpointTask.php', 'ArcanistHardpointTaskResult' => 'hardpoint/ArcanistHardpointTaskResult.php', 'ArcanistHelpWorkflow' => 'toolset/workflow/ArcanistHelpWorkflow.php', 'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule.php', 'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase.php', 'ArcanistHgClientChannel' => 'hgdaemon/ArcanistHgClientChannel.php', 'ArcanistHgProxyClient' => 'hgdaemon/ArcanistHgProxyClient.php', 'ArcanistHgProxyServer' => 'hgdaemon/ArcanistHgProxyServer.php', 'ArcanistHgServerChannel' => 'hgdaemon/ArcanistHgServerChannel.php', 'ArcanistImplicitConstructorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplicitConstructorXHPASTLinterRule.php', 'ArcanistImplicitConstructorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistImplicitConstructorXHPASTLinterRuleTestCase.php', 'ArcanistImplicitFallthroughXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplicitFallthroughXHPASTLinterRule.php', 'ArcanistImplicitFallthroughXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistImplicitFallthroughXHPASTLinterRuleTestCase.php', 'ArcanistImplicitVisibilityXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplicitVisibilityXHPASTLinterRule.php', 'ArcanistImplicitVisibilityXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistImplicitVisibilityXHPASTLinterRuleTestCase.php', 'ArcanistImplodeArgumentOrderXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistImplodeArgumentOrderXHPASTLinterRule.php', 'ArcanistImplodeArgumentOrderXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistImplodeArgumentOrderXHPASTLinterRuleTestCase.php', 'ArcanistInlineHTMLXHPASTLinterRule' => 'lint/linter/ArcanistInlineHTMLXHPASTLinterRule.php', 'ArcanistInlineHTMLXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistInlineHTMLXHPASTLinterRuleTestCase.php', 'ArcanistInnerFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInnerFunctionXHPASTLinterRule.php', 'ArcanistInnerFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistInnerFunctionXHPASTLinterRuleTestCase.php', 'ArcanistInspectWorkflow' => 'workflow/ArcanistInspectWorkflow.php', 'ArcanistInstallCertificateWorkflow' => 'workflow/ArcanistInstallCertificateWorkflow.php', 'ArcanistInstanceOfOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInstanceOfOperatorXHPASTLinterRule.php', 'ArcanistInstanceofOperatorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistInstanceofOperatorXHPASTLinterRuleTestCase.php', 'ArcanistInterfaceAbstractMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInterfaceAbstractMethodXHPASTLinterRule.php', 'ArcanistInterfaceAbstractMethodXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistInterfaceAbstractMethodXHPASTLinterRuleTestCase.php', 'ArcanistInterfaceMethodBodyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInterfaceMethodBodyXHPASTLinterRule.php', 'ArcanistInterfaceMethodBodyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistInterfaceMethodBodyXHPASTLinterRuleTestCase.php', 'ArcanistInvalidDefaultParameterXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInvalidDefaultParameterXHPASTLinterRule.php', 'ArcanistInvalidDefaultParameterXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistInvalidDefaultParameterXHPASTLinterRuleTestCase.php', 'ArcanistInvalidModifiersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php', 'ArcanistInvalidModifiersXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistInvalidModifiersXHPASTLinterRuleTestCase.php', 'ArcanistInvalidOctalNumericScalarXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistInvalidOctalNumericScalarXHPASTLinterRule.php', 'ArcanistInvalidOctalNumericScalarXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistInvalidOctalNumericScalarXHPASTLinterRuleTestCase.php', 'ArcanistIsAShouldBeInstanceOfXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistIsAShouldBeInstanceOfXHPASTLinterRule.php', 'ArcanistIsAShouldBeInstanceOfXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistIsAShouldBeInstanceOfXHPASTLinterRuleTestCase.php', 'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php', 'ArcanistJSHintLinterTestCase' => 'lint/linter/__tests__/ArcanistJSHintLinterTestCase.php', 'ArcanistJSONLintLinter' => 'lint/linter/ArcanistJSONLintLinter.php', 'ArcanistJSONLintRenderer' => 'lint/renderer/ArcanistJSONLintRenderer.php', 'ArcanistJSONLinter' => 'lint/linter/ArcanistJSONLinter.php', 'ArcanistJSONLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLinterTestCase.php', 'ArcanistJscsLinter' => 'lint/linter/ArcanistJscsLinter.php', 'ArcanistJscsLinterTestCase' => 'lint/linter/__tests__/ArcanistJscsLinterTestCase.php', 'ArcanistKeywordCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php', 'ArcanistKeywordCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistKeywordCasingXHPASTLinterRuleTestCase.php', 'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLambdaFuncFunctionXHPASTLinterRule.php', 'ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase.php', - 'ArcanistLandEngine' => 'land/ArcanistLandEngine.php', + 'ArcanistLandCommit' => 'land/ArcanistLandCommit.php', + 'ArcanistLandCommitSet' => 'land/ArcanistLandCommitSet.php', + 'ArcanistLandEngine' => 'land/engine/ArcanistLandEngine.php', + 'ArcanistLandSymbol' => 'land/ArcanistLandSymbol.php', + 'ArcanistLandTarget' => 'land/ArcanistLandTarget.php', 'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php', 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php', 'ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase.php', 'ArcanistLesscLinter' => 'lint/linter/ArcanistLesscLinter.php', 'ArcanistLesscLinterTestCase' => 'lint/linter/__tests__/ArcanistLesscLinterTestCase.php', 'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php', 'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php', 'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php', 'ArcanistLintMessageTestCase' => 'lint/__tests__/ArcanistLintMessageTestCase.php', 'ArcanistLintPatcher' => 'lint/ArcanistLintPatcher.php', 'ArcanistLintRenderer' => 'lint/renderer/ArcanistLintRenderer.php', 'ArcanistLintResult' => 'lint/ArcanistLintResult.php', 'ArcanistLintSeverity' => 'lint/ArcanistLintSeverity.php', 'ArcanistLintWorkflow' => 'workflow/ArcanistLintWorkflow.php', 'ArcanistLinter' => 'lint/linter/ArcanistLinter.php', 'ArcanistLinterStandard' => 'lint/linter/standards/ArcanistLinterStandard.php', 'ArcanistLinterStandardTestCase' => 'lint/linter/standards/__tests__/ArcanistLinterStandardTestCase.php', 'ArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistLinterTestCase.php', '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', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase.php', 'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php', + 'ArcanistMercurialCommitMessageHardpointQuery' => 'query/ArcanistMercurialCommitMessageHardpointQuery.php', + 'ArcanistMercurialLandEngine' => 'land/engine/ArcanistMercurialLandEngine.php', + 'ArcanistMercurialLocalState' => 'repository/state/ArcanistMercurialLocalState.php', 'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php', 'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php', 'ArcanistMercurialWorkingCopy' => 'workingcopy/ArcanistMercurialWorkingCopy.php', + 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php', 'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php', 'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php', 'ArcanistMessageRevisionHardpointQuery' => 'query/ArcanistMessageRevisionHardpointQuery.php', 'ArcanistMissingArgumentTerminatorException' => 'exception/ArcanistMissingArgumentTerminatorException.php', 'ArcanistMissingLinterException' => 'lint/linter/exception/ArcanistMissingLinterException.php', 'ArcanistModifierOrderingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php', 'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistModifierOrderingXHPASTLinterRuleTestCase.php', 'ArcanistMultiSourceConfigOption' => 'config/option/ArcanistMultiSourceConfigOption.php', 'ArcanistNamespaceFirstStatementXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNamespaceFirstStatementXHPASTLinterRule.php', 'ArcanistNamespaceFirstStatementXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistNamespaceFirstStatementXHPASTLinterRuleTestCase.php', 'ArcanistNamingConventionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNamingConventionsXHPASTLinterRule.php', 'ArcanistNamingConventionsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistNamingConventionsXHPASTLinterRuleTestCase.php', 'ArcanistNestedNamespacesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNestedNamespacesXHPASTLinterRule.php', 'ArcanistNestedNamespacesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistNestedNamespacesXHPASTLinterRuleTestCase.php', 'ArcanistNewlineAfterOpenTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNewlineAfterOpenTagXHPASTLinterRule.php', 'ArcanistNewlineAfterOpenTagXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistNewlineAfterOpenTagXHPASTLinterRuleTestCase.php', 'ArcanistNoEffectException' => 'exception/usage/ArcanistNoEffectException.php', 'ArcanistNoEngineException' => 'exception/usage/ArcanistNoEngineException.php', 'ArcanistNoLintLinter' => 'lint/linter/ArcanistNoLintLinter.php', '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', 'ArcanistObjectListHardpoint' => 'hardpoint/ArcanistObjectListHardpoint.php', 'ArcanistObjectOperatorSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistObjectOperatorSpacingXHPASTLinterRule.php', 'ArcanistObjectOperatorSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistObjectOperatorSpacingXHPASTLinterRuleTestCase.php', 'ArcanistPEP8Linter' => 'lint/linter/ArcanistPEP8Linter.php', 'ArcanistPEP8LinterTestCase' => 'lint/linter/__tests__/ArcanistPEP8LinterTestCase.php', 'ArcanistPHPCloseTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPCloseTagXHPASTLinterRule.php', 'ArcanistPHPCloseTagXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPCloseTagXHPASTLinterRuleTestCase.php', 'ArcanistPHPCompatibilityXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php', 'ArcanistPHPCompatibilityXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPCompatibilityXHPASTLinterRuleTestCase.php', 'ArcanistPHPEchoTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPEchoTagXHPASTLinterRule.php', 'ArcanistPHPEchoTagXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPEchoTagXHPASTLinterRuleTestCase.php', 'ArcanistPHPOpenTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPOpenTagXHPASTLinterRule.php', 'ArcanistPHPOpenTagXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPOpenTagXHPASTLinterRuleTestCase.php', 'ArcanistPHPShortTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPShortTagXHPASTLinterRule.php', 'ArcanistPHPShortTagXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPShortTagXHPASTLinterRuleTestCase.php', 'ArcanistPaamayimNekudotayimSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPaamayimNekudotayimSpacingXHPASTLinterRule.php', 'ArcanistPaamayimNekudotayimSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPaamayimNekudotayimSpacingXHPASTLinterRuleTestCase.php', 'ArcanistParentMemberReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistParentMemberReferenceXHPASTLinterRule.php', 'ArcanistParentMemberReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistParentMemberReferenceXHPASTLinterRuleTestCase.php', 'ArcanistParenthesesSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistParenthesesSpacingXHPASTLinterRule.php', 'ArcanistParenthesesSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistParenthesesSpacingXHPASTLinterRuleTestCase.php', 'ArcanistParseStrUseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistParseStrUseXHPASTLinterRule.php', 'ArcanistParseStrUseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistParseStrUseXHPASTLinterRuleTestCase.php', 'ArcanistPasteRef' => 'ref/paste/ArcanistPasteRef.php', 'ArcanistPasteSymbolRef' => 'ref/paste/ArcanistPasteSymbolRef.php', 'ArcanistPasteWorkflow' => 'workflow/ArcanistPasteWorkflow.php', 'ArcanistPatchWorkflow' => 'workflow/ArcanistPatchWorkflow.php', 'ArcanistPhpLinter' => 'lint/linter/ArcanistPhpLinter.php', 'ArcanistPhpLinterTestCase' => 'lint/linter/__tests__/ArcanistPhpLinterTestCase.php', 'ArcanistPhpcsLinter' => 'lint/linter/ArcanistPhpcsLinter.php', 'ArcanistPhpcsLinterTestCase' => 'lint/linter/__tests__/ArcanistPhpcsLinterTestCase.php', 'ArcanistPhpunitTestResultParser' => 'unit/parser/ArcanistPhpunitTestResultParser.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', 'ArcanistProjectConfigurationSource' => 'config/source/ArcanistProjectConfigurationSource.php', 'ArcanistPrompt' => 'toolset/ArcanistPrompt.php', 'ArcanistPromptsWorkflow' => 'toolset/workflow/ArcanistPromptsWorkflow.php', 'ArcanistPublicPropertyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPublicPropertyXHPASTLinterRule.php', 'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPublicPropertyXHPASTLinterRuleTestCase.php', 'ArcanistPuppetLintLinter' => 'lint/linter/ArcanistPuppetLintLinter.php', 'ArcanistPuppetLintLinterTestCase' => 'lint/linter/__tests__/ArcanistPuppetLintLinterTestCase.php', 'ArcanistPyFlakesLinter' => 'lint/linter/ArcanistPyFlakesLinter.php', 'ArcanistPyFlakesLinterTestCase' => 'lint/linter/__tests__/ArcanistPyFlakesLinterTestCase.php', 'ArcanistPyLintLinter' => 'lint/linter/ArcanistPyLintLinter.php', 'ArcanistPyLintLinterTestCase' => 'lint/linter/__tests__/ArcanistPyLintLinterTestCase.php', 'ArcanistRaggedClassTreeEdgeXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistRaggedClassTreeEdgeXHPASTLinterRule.php', 'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase.php', 'ArcanistRef' => 'ref/ArcanistRef.php', 'ArcanistRefInspector' => 'inspector/ArcanistRefInspector.php', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', 'ArcanistRepositoryLocalState' => 'repository/state/ArcanistRepositoryLocalState.php', 'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php', 'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php', 'ArcanistReusedIteratorReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedIteratorReferenceXHPASTLinterRuleTestCase.php', 'ArcanistReusedIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php', 'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedIteratorXHPASTLinterRuleTestCase.php', 'ArcanistRevisionAuthorHardpointQuery' => 'ref/revision/ArcanistRevisionAuthorHardpointQuery.php', 'ArcanistRevisionBuildableHardpointQuery' => 'ref/revision/ArcanistRevisionBuildableHardpointQuery.php', 'ArcanistRevisionCommitMessageHardpointQuery' => 'ref/revision/ArcanistRevisionCommitMessageHardpointQuery.php', 'ArcanistRevisionParentRevisionsHardpointQuery' => 'ref/revision/ArcanistRevisionParentRevisionsHardpointQuery.php', 'ArcanistRevisionRef' => 'ref/revision/ArcanistRevisionRef.php', 'ArcanistRevisionRefSource' => 'ref/ArcanistRevisionRefSource.php', 'ArcanistRevisionSymbolRef' => 'ref/revision/ArcanistRevisionSymbolRef.php', 'ArcanistRuboCopLinter' => 'lint/linter/ArcanistRuboCopLinter.php', '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', 'ArcanistRuntimeHardpointQuery' => 'toolset/query/ArcanistRuntimeHardpointQuery.php', 'ArcanistScalarHardpoint' => 'hardpoint/ArcanistScalarHardpoint.php', 'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSelfClassReferenceXHPASTLinterRule.php', 'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSelfClassReferenceXHPASTLinterRuleTestCase.php', 'ArcanistSelfMemberReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php', 'ArcanistSelfMemberReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSelfMemberReferenceXHPASTLinterRuleTestCase.php', 'ArcanistSemicolonSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSemicolonSpacingXHPASTLinterRule.php', 'ArcanistSemicolonSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSemicolonSpacingXHPASTLinterRuleTestCase.php', 'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php', 'ArcanistSetting' => 'configuration/ArcanistSetting.php', 'ArcanistSettings' => 'configuration/ArcanistSettings.php', 'ArcanistShellCompleteWorkflow' => 'toolset/workflow/ArcanistShellCompleteWorkflow.php', 'ArcanistSimpleSymbolHardpointQuery' => 'ref/simple/ArcanistSimpleSymbolHardpointQuery.php', 'ArcanistSimpleSymbolRef' => 'ref/simple/ArcanistSimpleSymbolRef.php', 'ArcanistSimpleSymbolRefInspector' => 'ref/simple/ArcanistSimpleSymbolRefInspector.php', 'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php', 'ArcanistSingleSourceConfigOption' => 'config/option/ArcanistSingleSourceConfigOption.php', 'ArcanistSlownessXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php', 'ArcanistSlownessXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSlownessXHPASTLinterRuleTestCase.php', 'ArcanistSpellingLinter' => 'lint/linter/ArcanistSpellingLinter.php', 'ArcanistSpellingLinterTestCase' => 'lint/linter/__tests__/ArcanistSpellingLinterTestCase.php', 'ArcanistStaticThisXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistStaticThisXHPASTLinterRule.php', 'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistStaticThisXHPASTLinterRuleTestCase.php', 'ArcanistStringConfigOption' => 'config/option/ArcanistStringConfigOption.php', 'ArcanistStringListConfigOption' => 'config/option/ArcanistStringListConfigOption.php', 'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php', 'ArcanistSubversionWorkingCopy' => 'workingcopy/ArcanistSubversionWorkingCopy.php', 'ArcanistSummaryLintRenderer' => 'lint/renderer/ArcanistSummaryLintRenderer.php', 'ArcanistSymbolEngine' => 'ref/symbol/ArcanistSymbolEngine.php', 'ArcanistSymbolRef' => 'ref/symbol/ArcanistSymbolRef.php', 'ArcanistSyntaxErrorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSyntaxErrorXHPASTLinterRule.php', 'ArcanistSystemConfigurationSource' => 'config/source/ArcanistSystemConfigurationSource.php', 'ArcanistTaskRef' => 'ref/task/ArcanistTaskRef.php', 'ArcanistTaskSymbolRef' => 'ref/task/ArcanistTaskSymbolRef.php', 'ArcanistTasksWorkflow' => 'workflow/ArcanistTasksWorkflow.php', 'ArcanistTautologicalExpressionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistTautologicalExpressionXHPASTLinterRule.php', 'ArcanistTautologicalExpressionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistTautologicalExpressionXHPASTLinterRuleTestCase.php', 'ArcanistTerminalStringInterface' => 'xsprintf/ArcanistTerminalStringInterface.php', 'ArcanistTestResultParser' => 'unit/parser/ArcanistTestResultParser.php', 'ArcanistTestXHPASTLintSwitchHook' => 'lint/linter/__tests__/ArcanistTestXHPASTLintSwitchHook.php', 'ArcanistTextLinter' => 'lint/linter/ArcanistTextLinter.php', 'ArcanistTextLinterTestCase' => 'lint/linter/__tests__/ArcanistTextLinterTestCase.php', 'ArcanistThisReassignmentXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistThisReassignmentXHPASTLinterRule.php', 'ArcanistThisReassignmentXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistThisReassignmentXHPASTLinterRuleTestCase.php', 'ArcanistToStringExceptionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistToStringExceptionXHPASTLinterRule.php', 'ArcanistToStringExceptionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistToStringExceptionXHPASTLinterRuleTestCase.php', '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', 'ArcanistUnaryPostfixExpressionSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnaryPostfixExpressionSpacingXHPASTLinterRuleTestCase.php', 'ArcanistUnaryPrefixExpressionSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnaryPrefixExpressionSpacingXHPASTLinterRule.php', 'ArcanistUnaryPrefixExpressionSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnaryPrefixExpressionSpacingXHPASTLinterRuleTestCase.php', 'ArcanistUndeclaredVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php', 'ArcanistUndeclaredVariableXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUndeclaredVariableXHPASTLinterRuleTestCase.php', 'ArcanistUnexpectedReturnValueXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnexpectedReturnValueXHPASTLinterRule.php', 'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase.php', 'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.php', 'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php', 'ArcanistUnitTestEngine' => 'unit/engine/ArcanistUnitTestEngine.php', 'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php', 'ArcanistUnitTestResultTestCase' => 'unit/__tests__/ArcanistUnitTestResultTestCase.php', 'ArcanistUnitTestableLintEngine' => 'lint/engine/ArcanistUnitTestableLintEngine.php', 'ArcanistUnitWorkflow' => 'workflow/ArcanistUnitWorkflow.php', 'ArcanistUnnecessaryFinalModifierXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnnecessaryFinalModifierXHPASTLinterRule.php', 'ArcanistUnnecessaryFinalModifierXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnnecessaryFinalModifierXHPASTLinterRuleTestCase.php', 'ArcanistUnnecessarySemicolonXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnnecessarySemicolonXHPASTLinterRule.php', 'ArcanistUnnecessarySymbolAliasXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnnecessarySymbolAliasXHPASTLinterRule.php', 'ArcanistUnnecessarySymbolAliasXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnnecessarySymbolAliasXHPASTLinterRuleTestCase.php', 'ArcanistUnsafeDynamicStringXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnsafeDynamicStringXHPASTLinterRule.php', 'ArcanistUnsafeDynamicStringXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnsafeDynamicStringXHPASTLinterRuleTestCase.php', 'ArcanistUpgradeWorkflow' => 'workflow/ArcanistUpgradeWorkflow.php', 'ArcanistUploadWorkflow' => 'workflow/ArcanistUploadWorkflow.php', 'ArcanistUsageException' => 'exception/ArcanistUsageException.php', 'ArcanistUseStatementNamespacePrefixXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUseStatementNamespacePrefixXHPASTLinterRule.php', 'ArcanistUseStatementNamespacePrefixXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUseStatementNamespacePrefixXHPASTLinterRuleTestCase.php', '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', 'ArcanistUserRef' => 'ref/user/ArcanistUserRef.php', 'ArcanistUserSymbolHardpointQuery' => 'ref/user/ArcanistUserSymbolHardpointQuery.php', 'ArcanistUserSymbolRef' => 'ref/user/ArcanistUserSymbolRef.php', 'ArcanistUserSymbolRefInspector' => 'ref/user/ArcanistUserSymbolRefInspector.php', 'ArcanistVariableReferenceSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistVariableReferenceSpacingXHPASTLinterRule.php', 'ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase.php', 'ArcanistVariableVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistVariableVariableXHPASTLinterRule.php', 'ArcanistVariableVariableXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistVariableVariableXHPASTLinterRuleTestCase.php', 'ArcanistVectorHardpoint' => 'hardpoint/ArcanistVectorHardpoint.php', 'ArcanistVersionWorkflow' => 'toolset/workflow/ArcanistVersionWorkflow.php', 'ArcanistWeldWorkflow' => 'workflow/ArcanistWeldWorkflow.php', 'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php', 'ArcanistWildConfigOption' => 'config/option/ArcanistWildConfigOption.php', 'ArcanistWorkflow' => 'workflow/ArcanistWorkflow.php', 'ArcanistWorkflowArgument' => 'toolset/ArcanistWorkflowArgument.php', 'ArcanistWorkflowGitHardpointQuery' => 'query/ArcanistWorkflowGitHardpointQuery.php', 'ArcanistWorkflowInformation' => 'toolset/ArcanistWorkflowInformation.php', + 'ArcanistWorkflowMercurialHardpointQuery' => 'query/ArcanistWorkflowMercurialHardpointQuery.php', 'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php', 'ArcanistWorkingCopyCommitHardpointQuery' => 'query/ArcanistWorkingCopyCommitHardpointQuery.php', 'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php', 'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.php', 'ArcanistWorkingCopyPath' => 'workingcopy/ArcanistWorkingCopyPath.php', 'ArcanistWorkingCopyStateRef' => 'ref/ArcanistWorkingCopyStateRef.php', 'ArcanistWorkingCopyStateRefInspector' => 'inspector/ArcanistWorkingCopyStateRefInspector.php', 'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php', 'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php', 'ArcanistXHPASTLintSwitchHook' => 'lint/linter/xhpast/ArcanistXHPASTLintSwitchHook.php', 'ArcanistXHPASTLinter' => 'lint/linter/ArcanistXHPASTLinter.php', 'ArcanistXHPASTLinterRule' => 'lint/linter/xhpast/ArcanistXHPASTLinterRule.php', 'ArcanistXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistXHPASTLinterRuleTestCase.php', 'ArcanistXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php', 'ArcanistXMLLinter' => 'lint/linter/ArcanistXMLLinter.php', 'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php', 'ArcanistXUnitTestResultParser' => 'unit/parser/ArcanistXUnitTestResultParser.php', 'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php', 'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php', 'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php', 'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php', 'CommandException' => 'future/exec/CommandException.php', 'ConduitClient' => 'conduit/ConduitClient.php', 'ConduitClientException' => 'conduit/ConduitClientException.php', 'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php', 'ConduitFuture' => 'conduit/ConduitFuture.php', 'ConduitSearchFuture' => 'conduit/ConduitSearchFuture.php', 'ExecFuture' => 'future/exec/ExecFuture.php', 'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php', 'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php', 'FileFinder' => 'filesystem/FileFinder.php', 'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php', 'FileList' => 'filesystem/FileList.php', 'Filesystem' => 'filesystem/Filesystem.php', 'FilesystemException' => 'filesystem/FilesystemException.php', 'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php', 'Future' => 'future/Future.php', 'FutureAgent' => 'conduit/FutureAgent.php', 'FutureIterator' => 'future/FutureIterator.php', 'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php', 'FuturePool' => 'future/FuturePool.php', 'FutureProxy' => 'future/FutureProxy.php', 'HTTPFuture' => 'future/http/HTTPFuture.php', 'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php', 'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php', 'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php', 'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php', 'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php', 'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php', 'HTTPSFuture' => 'future/http/HTTPSFuture.php', 'ImmediateFuture' => 'future/ImmediateFuture.php', 'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.php', 'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php', 'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php', 'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php', 'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php', 'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php', 'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php', 'NoseTestEngine' => 'unit/engine/NoseTestEngine.php', 'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php', 'PhageAction' => 'phage/action/PhageAction.php', 'PhageAgentAction' => 'phage/action/PhageAgentAction.php', 'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php', 'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php', 'PhageExecWorkflow' => 'phage/workflow/PhageExecWorkflow.php', 'PhageExecuteAction' => 'phage/action/PhageExecuteAction.php', 'PhageLocalAction' => 'phage/action/PhageLocalAction.php', 'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php', 'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php', 'PhagePlanAction' => 'phage/action/PhagePlanAction.php', 'PhageToolset' => 'phage/toolset/PhageToolset.php', 'PhageWorkflow' => 'phage/workflow/PhageWorkflow.php', 'Phobject' => 'object/Phobject.php', 'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php', 'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php', 'PhpunitTestEngineTestCase' => 'unit/engine/__tests__/PhpunitTestEngineTestCase.php', 'PhutilAWSCloudFormationFuture' => 'future/aws/PhutilAWSCloudFormationFuture.php', 'PhutilAWSCloudWatchFuture' => 'future/aws/PhutilAWSCloudWatchFuture.php', 'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php', 'PhutilAWSException' => 'future/aws/PhutilAWSException.php', 'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php', 'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php', 'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php', 'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php', 'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php', 'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php', 'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php', 'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php', 'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php', 'PhutilAggregateException' => 'error/PhutilAggregateException.php', 'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php', 'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php', 'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php', 'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php', 'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php', 'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php', 'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php', 'PhutilArgumentSpellingCorrector' => 'parser/argument/PhutilArgumentSpellingCorrector.php', 'PhutilArgumentSpellingCorrectorTestCase' => 'parser/argument/__tests__/PhutilArgumentSpellingCorrectorTestCase.php', 'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php', 'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php', 'PhutilArray' => 'utils/PhutilArray.php', 'PhutilArrayCheck' => 'utils/PhutilArrayCheck.php', 'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php', 'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php', 'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php', 'PhutilBacktraceSignalHandler' => 'future/exec/PhutilBacktraceSignalHandler.php', 'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php', 'PhutilBinaryAnalyzer' => 'filesystem/binary/PhutilBinaryAnalyzer.php', 'PhutilBinaryAnalyzerTestCase' => 'filesystem/binary/__tests__/PhutilBinaryAnalyzerTestCase.php', 'PhutilBootloader' => 'init/lib/PhutilBootloader.php', 'PhutilBootloaderException' => 'init/lib/PhutilBootloaderException.php', 'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php', 'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php', 'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php', 'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php', 'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php', 'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php', 'PhutilCIDRList' => 'ip/PhutilCIDRList.php', 'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php', 'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php', 'PhutilChannel' => 'channel/PhutilChannel.php', 'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php', 'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php', 'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php', 'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php', 'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php', 'PhutilCloudWatchMetric' => 'future/aws/PhutilCloudWatchMetric.php', 'PhutilCommandString' => 'xsprintf/PhutilCommandString.php', 'PhutilConsole' => 'console/PhutilConsole.php', 'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php', 'PhutilConsoleError' => 'console/view/PhutilConsoleError.php', 'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php', 'PhutilConsoleInfo' => 'console/view/PhutilConsoleInfo.php', 'PhutilConsoleList' => 'console/view/PhutilConsoleList.php', 'PhutilConsoleLogLine' => 'console/view/PhutilConsoleLogLine.php', 'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php', 'PhutilConsoleMetrics' => 'console/PhutilConsoleMetrics.php', 'PhutilConsoleMetricsSignalHandler' => 'future/exec/PhutilConsoleMetricsSignalHandler.php', 'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php', 'PhutilConsoleProgressSink' => 'progress/PhutilConsoleProgressSink.php', 'PhutilConsoleServer' => 'console/PhutilConsoleServer.php', 'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php', 'PhutilConsoleSkip' => 'console/view/PhutilConsoleSkip.php', 'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php', 'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php', 'PhutilConsoleView' => 'console/view/PhutilConsoleView.php', 'PhutilConsoleWarning' => 'console/view/PhutilConsoleWarning.php', 'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php', 'PhutilCowsay' => 'utils/PhutilCowsay.php', 'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php', 'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php', 'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php', 'PhutilDOMNode' => 'parser/html/PhutilDOMNode.php', 'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php', 'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php', 'PhutilDiffBinaryAnalyzer' => 'filesystem/binary/PhutilDiffBinaryAnalyzer.php', 'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php', 'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php', 'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php', 'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php', 'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php', 'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php', 'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php', 'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php', 'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php', 'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php', 'PhutilEmojiLocale' => 'internationalization/locales/PhutilEmojiLocale.php', 'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php', 'PhutilErrorHandler' => 'error/PhutilErrorHandler.php', 'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php', 'PhutilErrorTrap' => 'error/PhutilErrorTrap.php', 'PhutilEvent' => 'events/PhutilEvent.php', 'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php', 'PhutilEventEngine' => 'events/PhutilEventEngine.php', 'PhutilEventListener' => 'events/PhutilEventListener.php', 'PhutilEventType' => 'events/constant/PhutilEventType.php', 'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php', 'PhutilExecChannel' => 'channel/PhutilExecChannel.php', 'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php', 'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php', 'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php', 'PhutilFileLock' => 'filesystem/PhutilFileLock.php', 'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php', 'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php', 'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php', 'PhutilGitBinaryAnalyzer' => 'filesystem/binary/PhutilGitBinaryAnalyzer.php', 'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php', 'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php', 'PhutilGitURI' => 'parser/PhutilGitURI.php', 'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php', 'PhutilHTMLParser' => 'parser/html/PhutilHTMLParser.php', 'PhutilHTMLParserTestCase' => 'parser/html/__tests__/PhutilHTMLParserTestCase.php', 'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php', 'PhutilHTTPResponse' => 'parser/http/PhutilHTTPResponse.php', 'PhutilHTTPResponseParser' => 'parser/http/PhutilHTTPResponseParser.php', 'PhutilHTTPResponseParserTestCase' => 'parser/http/__tests__/PhutilHTTPResponseParserTestCase.php', 'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php', 'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php', 'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php', 'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php', 'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php', 'PhutilIPAddress' => 'ip/PhutilIPAddress.php', 'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php', 'PhutilIPv4Address' => 'ip/PhutilIPv4Address.php', 'PhutilIPv6Address' => 'ip/PhutilIPv6Address.php', 'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php', 'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php', 'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php', 'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php', 'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php', 'PhutilJSON' => 'parser/PhutilJSON.php', 'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php', 'PhutilJSONParser' => 'parser/PhutilJSONParser.php', 'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php', 'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php', 'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php', 'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php', 'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php', 'PhutilJavaFragmentLexer' => 'lexer/PhutilJavaFragmentLexer.php', 'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php', 'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php', 'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php', 'PhutilLexer' => 'lexer/PhutilLexer.php', 'PhutilLibraryConflictException' => 'init/lib/PhutilLibraryConflictException.php', 'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php', 'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php', 'PhutilLocale' => 'internationalization/PhutilLocale.php', 'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php', 'PhutilLock' => 'filesystem/PhutilLock.php', 'PhutilLockException' => 'filesystem/PhutilLockException.php', 'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php', 'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php', 'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php', 'PhutilMercurialBinaryAnalyzer' => 'filesystem/binary/PhutilMercurialBinaryAnalyzer.php', 'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php', 'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php', 'PhutilMissingSymbolException' => 'init/lib/PhutilMissingSymbolException.php', 'PhutilModuleUtilsTestCase' => 'init/lib/__tests__/PhutilModuleUtilsTestCase.php', 'PhutilNumber' => 'internationalization/PhutilNumber.php', 'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php', 'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php', 'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php', 'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php', 'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php', 'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php', 'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php', 'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php', 'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php', 'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php', 'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php', 'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php', 'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php', 'PhutilPerson' => 'internationalization/PhutilPerson.php', 'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php', 'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php', 'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php', 'PhutilPortugueseBrazilLocale' => 'internationalization/locales/PhutilPortugueseBrazilLocale.php', 'PhutilPortuguesePortugalLocale' => 'internationalization/locales/PhutilPortuguesePortugalLocale.php', 'PhutilPostmarkFuture' => 'future/postmark/PhutilPostmarkFuture.php', 'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php', 'PhutilProcessQuery' => 'filesystem/PhutilProcessQuery.php', 'PhutilProcessRef' => 'filesystem/PhutilProcessRef.php', 'PhutilProcessRefTestCase' => 'filesystem/__tests__/PhutilProcessRefTestCase.php', 'PhutilProgressSink' => 'progress/PhutilProgressSink.php', 'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php', 'PhutilProxyException' => 'error/PhutilProxyException.php', 'PhutilProxyIterator' => 'utils/PhutilProxyIterator.php', 'PhutilPygmentizeBinaryAnalyzer' => 'filesystem/binary/PhutilPygmentizeBinaryAnalyzer.php', 'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php', 'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php', 'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php', 'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php', 'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php', 'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php', 'PhutilRope' => 'utils/PhutilRope.php', 'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php', 'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php', 'PhutilShellLexer' => 'lexer/PhutilShellLexer.php', 'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php', 'PhutilSignalHandler' => 'future/exec/PhutilSignalHandler.php', 'PhutilSignalRouter' => 'future/exec/PhutilSignalRouter.php', 'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php', 'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php', 'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php', 'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php', 'PhutilSimplifiedChineseLocale' => 'internationalization/locales/PhutilSimplifiedChineseLocale.php', 'PhutilSlackFuture' => 'future/slack/PhutilSlackFuture.php', 'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php', 'PhutilSortVector' => 'utils/PhutilSortVector.php', 'PhutilSpanishSpainLocale' => 'internationalization/locales/PhutilSpanishSpainLocale.php', 'PhutilStreamIterator' => 'utils/PhutilStreamIterator.php', 'PhutilSubversionBinaryAnalyzer' => 'filesystem/binary/PhutilSubversionBinaryAnalyzer.php', 'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php', 'PhutilSystem' => 'utils/PhutilSystem.php', 'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php', 'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php', 'PhutilTestCase' => 'unit/engine/phutil/PhutilTestCase.php', 'PhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/PhutilTestCaseTestCase.php', 'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php', 'PhutilTestSkippedException' => 'unit/engine/phutil/testcase/PhutilTestSkippedException.php', 'PhutilTestTerminatedException' => 'unit/engine/phutil/testcase/PhutilTestTerminatedException.php', 'PhutilTraditionalChineseLocale' => 'internationalization/locales/PhutilTraditionalChineseLocale.php', 'PhutilTranslation' => 'internationalization/PhutilTranslation.php', 'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php', 'PhutilTranslator' => 'internationalization/PhutilTranslator.php', 'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php', 'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php', 'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php', 'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php', 'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php', 'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php', 'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php', 'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php', 'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php', 'PhutilURI' => 'parser/PhutilURI.php', 'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php', 'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php', 'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php', 'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php', 'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php', 'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php', 'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php', 'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php', 'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php', 'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php', 'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php', 'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php', 'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php', 'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php', 'PytestTestEngine' => 'unit/engine/PytestTestEngine.php', 'TempFile' => 'filesystem/TempFile.php', 'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php', 'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php', 'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php', 'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php', 'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php', 'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php', 'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php', 'XUnitTestEngine' => 'unit/engine/XUnitTestEngine.php', 'XUnitTestResultParserTestCase' => 'unit/parser/__tests__/XUnitTestResultParserTestCase.php', 'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php', ), 'function' => array( '__phutil_autoload' => 'init/init-library.php', 'array_fuse' => 'utils/utils.php', 'array_interleave' => 'utils/utils.php', 'array_mergev' => 'utils/utils.php', 'array_select_keys' => 'utils/utils.php', 'assert_instances_of' => 'utils/utils.php', 'assert_same_keys' => 'utils/utils.php', 'assert_stringlike' => 'utils/utils.php', 'coalesce' => 'utils/utils.php', 'csprintf' => 'xsprintf/csprintf.php', 'exec_manual' => 'future/exec/execx.php', 'execx' => 'future/exec/execx.php', 'head' => 'utils/utils.php', 'head_key' => 'utils/utils.php', 'hgsprintf' => 'xsprintf/hgsprintf.php', 'id' => 'utils/utils.php', 'idx' => 'utils/utils.php', 'idxv' => 'utils/utils.php', 'ifilter' => 'utils/utils.php', 'igroup' => 'utils/utils.php', 'ipull' => 'utils/utils.php', 'isort' => 'utils/utils.php', 'jsprintf' => 'xsprintf/jsprintf.php', 'last' => 'utils/utils.php', 'last_key' => 'utils/utils.php', 'ldap_sprintf' => 'xsprintf/ldapsprintf.php', 'mfilter' => 'utils/utils.php', 'mgroup' => 'utils/utils.php', 'mpull' => 'utils/utils.php', 'msort' => 'utils/utils.php', 'msortv' => 'utils/utils.php', 'newv' => 'utils/utils.php', 'nonempty' => 'utils/utils.php', 'phlog' => 'error/phlog.php', 'pht' => 'internationalization/pht.php', 'phutil_build_http_querystring' => 'utils/utils.php', 'phutil_build_http_querystring_from_pairs' => 'utils/utils.php', 'phutil_censor_credentials' => 'utils/utils.php', 'phutil_console_confirm' => 'console/format.php', 'phutil_console_format' => 'console/format.php', 'phutil_console_get_terminal_width' => 'console/format.php', 'phutil_console_prompt' => 'console/format.php', 'phutil_console_require_tty' => 'console/format.php', 'phutil_console_select' => 'console/format.php', 'phutil_console_wrap' => 'console/format.php', 'phutil_count' => 'internationalization/pht.php', 'phutil_date_format' => 'utils/viewutils.php', 'phutil_decode_mime_header' => 'utils/utils.php', 'phutil_deprecated' => 'init/lib/moduleutils.php', 'phutil_describe_type' => 'utils/utils.php', 'phutil_encode_log' => 'utils/utils.php', 'phutil_error_listener_example' => 'error/phlog.php', 'phutil_escape_uri' => 'utils/utils.php', 'phutil_escape_uri_path_component' => 'utils/utils.php', 'phutil_fnmatch' => 'utils/utils.php', 'phutil_format_bytes' => 'utils/viewutils.php', 'phutil_format_relative_time' => 'utils/viewutils.php', 'phutil_format_relative_time_detailed' => 'utils/viewutils.php', 'phutil_format_units_generic' => 'utils/viewutils.php', 'phutil_fwrite_nonblocking_stream' => 'utils/utils.php', 'phutil_get_current_library_name' => 'init/lib/moduleutils.php', 'phutil_get_library_name_for_root' => 'init/lib/moduleutils.php', 'phutil_get_library_root' => 'init/lib/moduleutils.php', 'phutil_get_library_root_for_path' => 'init/lib/moduleutils.php', 'phutil_get_signal_name' => 'future/exec/execx.php', 'phutil_get_system_locale' => 'utils/utf8.php', 'phutil_glue' => 'utils/utils.php', 'phutil_hashes_are_identical' => 'utils/utils.php', 'phutil_http_parameter_pair' => 'utils/utils.php', 'phutil_ini_decode' => 'utils/utils.php', 'phutil_is_hiphop_runtime' => 'utils/utils.php', 'phutil_is_interactive' => 'utils/utils.php', 'phutil_is_natural_list' => 'utils/utils.php', 'phutil_is_noninteractive' => 'utils/utils.php', 'phutil_is_system_locale_available' => 'utils/utf8.php', 'phutil_is_utf8' => 'utils/utf8.php', 'phutil_is_utf8_slowly' => 'utils/utf8.php', 'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php', 'phutil_is_windows' => 'utils/utils.php', 'phutil_json_decode' => 'utils/utils.php', 'phutil_json_encode' => 'utils/utils.php', 'phutil_load_library' => 'init/lib/moduleutils.php', 'phutil_loggable_string' => 'utils/utils.php', 'phutil_microseconds_since' => 'utils/utils.php', 'phutil_parse_bytes' => 'utils/viewutils.php', 'phutil_passthru' => 'future/exec/execx.php', 'phutil_person' => 'internationalization/pht.php', 'phutil_register_library' => 'init/lib/core.php', 'phutil_register_library_map' => 'init/lib/core.php', 'phutil_set_system_locale' => 'utils/utf8.php', 'phutil_split_lines' => 'utils/utils.php', 'phutil_string_cast' => 'utils/utils.php', 'phutil_unescape_uri_path_component' => 'utils/utils.php', 'phutil_units' => 'utils/utils.php', 'phutil_utf8_console_strlen' => 'utils/utf8.php', 'phutil_utf8_convert' => 'utils/utf8.php', 'phutil_utf8_encode_codepoint' => 'utils/utf8.php', 'phutil_utf8_hard_wrap' => 'utils/utf8.php', 'phutil_utf8_hard_wrap_html' => 'utils/utf8.php', 'phutil_utf8_is_cjk' => 'utils/utf8.php', 'phutil_utf8_is_combining_character' => 'utils/utf8.php', 'phutil_utf8_strlen' => 'utils/utf8.php', 'phutil_utf8_strtolower' => 'utils/utf8.php', 'phutil_utf8_strtoupper' => 'utils/utf8.php', 'phutil_utf8_strtr' => 'utils/utf8.php', 'phutil_utf8_ucwords' => 'utils/utf8.php', 'phutil_utf8ize' => 'utils/utf8.php', 'phutil_utf8v' => 'utils/utf8.php', 'phutil_utf8v_codepoints' => 'utils/utf8.php', 'phutil_utf8v_combine_characters' => 'utils/utf8.php', 'phutil_utf8v_combined' => 'utils/utf8.php', 'phutil_validate_json' => 'utils/utils.php', 'phutil_var_export' => 'utils/utils.php', 'ppull' => 'utils/utils.php', 'pregsprintf' => 'xsprintf/pregsprintf.php', 'tsprintf' => 'xsprintf/tsprintf.php', 'urisprintf' => 'xsprintf/urisprintf.php', 'vcsprintf' => 'xsprintf/csprintf.php', 'vjsprintf' => 'xsprintf/jsprintf.php', 'vurisprintf' => 'xsprintf/urisprintf.php', 'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php', 'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php', 'xsprintf' => 'xsprintf/xsprintf.php', 'xsprintf_callback_example' => 'xsprintf/xsprintf.php', 'xsprintf_command' => 'xsprintf/csprintf.php', 'xsprintf_javascript' => 'xsprintf/jsprintf.php', 'xsprintf_ldap' => 'xsprintf/ldapsprintf.php', 'xsprintf_mercurial' => 'xsprintf/hgsprintf.php', 'xsprintf_regex' => 'xsprintf/pregsprintf.php', 'xsprintf_terminal' => 'xsprintf/tsprintf.php', 'xsprintf_uri' => 'xsprintf/urisprintf.php', ), 'xmap' => array( 'AASTNode' => 'Phobject', 'AASTNodeList' => array( 'Phobject', 'Countable', 'Iterator', ), 'AASTToken' => 'Phobject', 'AASTTree' => 'Phobject', 'AbstractDirectedGraph' => 'Phobject', 'AbstractDirectedGraphTestCase' => 'PhutilTestCase', 'ArcanistAbstractMethodBodyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAlias' => 'Phobject', 'ArcanistAliasEffect' => 'Phobject', 'ArcanistAliasEngine' => 'Phobject', 'ArcanistAliasFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAliasWorkflow' => 'ArcanistWorkflow', 'ArcanistAliasesConfigOption' => 'ArcanistMultiSourceConfigOption', 'ArcanistAmendWorkflow' => 'ArcanistArcWorkflow', 'ArcanistAnoidWorkflow' => 'ArcanistArcWorkflow', 'ArcanistArcConfigurationEngineExtension' => 'ArcanistConfigurationEngineExtension', 'ArcanistArcToolset' => 'ArcanistToolset', 'ArcanistArcWorkflow' => 'ArcanistWorkflow', 'ArcanistArrayCombineXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistArrayCombineXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistArrayIndexSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistArrayIndexSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistArraySeparatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistArraySeparatorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistArrayValueXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistArrayValueXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBaseCommitParser' => 'Phobject', 'ArcanistBaseCommitParserTestCase' => 'PhutilTestCase', 'ArcanistBaseXHPASTLinter' => 'ArcanistFutureLinter', 'ArcanistBinaryExpressionSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBinaryExpressionSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBinaryNumericScalarCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBinaryNumericScalarCasingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBlacklistedFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBlindlyTrustHTTPEngineExtension' => 'PhutilHTTPEngineExtension', 'ArcanistBookmarkWorkflow' => 'ArcanistFeatureBaseWorkflow', 'ArcanistBoolConfigOption' => 'ArcanistSingleSourceConfigOption', 'ArcanistBraceFormattingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBranchRef' => 'ArcanistRef', 'ArcanistBranchWorkflow' => 'ArcanistFeatureBaseWorkflow', 'ArcanistBrowseCommitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistBrowseCommitURIHardpointQuery' => 'ArcanistBrowseURIHardpointQuery', 'ArcanistBrowseObjectNameURIHardpointQuery' => 'ArcanistBrowseURIHardpointQuery', 'ArcanistBrowsePathURIHardpointQuery' => 'ArcanistBrowseURIHardpointQuery', 'ArcanistBrowseRef' => 'ArcanistRef', 'ArcanistBrowseRefInspector' => 'ArcanistRefInspector', 'ArcanistBrowseRevisionURIHardpointQuery' => 'ArcanistBrowseURIHardpointQuery', 'ArcanistBrowseURIHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistBrowseURIRef' => 'ArcanistRef', 'ArcanistBrowseWorkflow' => 'ArcanistArcWorkflow', 'ArcanistBuildBuildplanHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistBuildPlanRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', ), 'ArcanistBuildPlanSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistBuildRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', ), 'ArcanistBuildSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistBuildableBuildsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistBuildableRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', ), 'ArcanistBuildableSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistBundle' => 'Phobject', 'ArcanistBundleTestCase' => 'PhutilTestCase', 'ArcanistCSSLintLinter' => 'ArcanistExternalLinter', 'ArcanistCSSLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCSharpLinter' => 'ArcanistLinter', 'ArcanistCallConduitWorkflow' => 'ArcanistArcWorkflow', 'ArcanistCallParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCallParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistCallTimePassByReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCallTimePassByReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistCapabilityNotSupportedException' => 'Exception', 'ArcanistCastSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCastSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistCheckstyleXMLLintRenderer' => 'ArcanistLintRenderer', 'ArcanistChmodLinter' => 'ArcanistLinter', 'ArcanistChmodLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistClassExtendsObjectXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistClassExtendsObjectXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistClassFilenameMismatchXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistClassNameLiteralXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistClassNameLiteralXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistCloseRevisionWorkflow' => 'ArcanistWorkflow', 'ArcanistClosureLinter' => 'ArcanistExternalLinter', 'ArcanistClosureLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCoffeeLintLinter' => 'ArcanistExternalLinter', 'ArcanistCoffeeLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCommand' => 'Phobject', 'ArcanistCommentRemover' => 'Phobject', 'ArcanistCommentRemoverTestCase' => 'PhutilTestCase', 'ArcanistCommentSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCommentStyleXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCommentStyleXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistCommitRef' => 'ArcanistRef', 'ArcanistCommitSymbolRef' => 'ArcanistSymbolRef', 'ArcanistCommitSymbolRefInspector' => 'ArcanistRefInspector', 'ArcanistCommitUpstreamHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistCommitWorkflow' => 'ArcanistWorkflow', 'ArcanistCompilerLintRenderer' => 'ArcanistLintRenderer', 'ArcanistComposerLinter' => 'ArcanistLinter', 'ArcanistComprehensiveLintEngine' => 'ArcanistLintEngine', 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', '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', 'ArcanistConstructorParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistContinueInsideSwitchXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistContinueInsideSwitchXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistControlStatementSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistControlStatementSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistCoverWorkflow' => 'ArcanistWorkflow', 'ArcanistCppcheckLinter' => 'ArcanistExternalLinter', 'ArcanistCppcheckLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCpplintLinter' => 'ArcanistExternalLinter', 'ArcanistCpplintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCurlyBraceArrayIndexXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCurlyBraceArrayIndexXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDeclarationParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDefaultsConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistDeprecationXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDictionaryConfigurationSource' => 'ArcanistConfigurationSource', 'ArcanistDiffByteSizeException' => 'Exception', 'ArcanistDiffChange' => 'Phobject', 'ArcanistDiffChangeType' => 'Phobject', 'ArcanistDiffHunk' => 'Phobject', 'ArcanistDiffParser' => 'Phobject', 'ArcanistDiffParserTestCase' => 'PhutilTestCase', 'ArcanistDiffUtils' => 'Phobject', 'ArcanistDiffUtilsTestCase' => 'PhutilTestCase', 'ArcanistDiffVectorNode' => 'Phobject', 'ArcanistDiffVectorTree' => 'Phobject', 'ArcanistDiffWorkflow' => 'ArcanistWorkflow', 'ArcanistDifferentialCommitMessage' => 'Phobject', 'ArcanistDifferentialCommitMessageParserException' => 'Exception', 'ArcanistDifferentialDependencyGraph' => 'AbstractDirectedGraph', 'ArcanistDifferentialRevisionHash' => 'Phobject', 'ArcanistDifferentialRevisionStatus' => 'Phobject', 'ArcanistDisplayRef' => array( 'Phobject', 'ArcanistTerminalStringInterface', ), 'ArcanistDoubleQuoteXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDoubleQuoteXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDownloadWorkflow' => 'ArcanistArcWorkflow', 'ArcanistDuplicateKeysInArrayXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDuplicateKeysInArrayXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDuplicateSwitchCaseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDuplicateSwitchCaseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDynamicDefineXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDynamicDefineXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistElseIfUsageXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistElseIfUsageXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistEmptyFileXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistEmptyStatementXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistEmptyStatementXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistEventType' => 'PhutilEventType', 'ArcanistExitExpressionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistExitExpressionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistExportWorkflow' => 'ArcanistWorkflow', 'ArcanistExternalLinter' => 'ArcanistFutureLinter', 'ArcanistExternalLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistFeatureBaseWorkflow' => 'ArcanistArcWorkflow', 'ArcanistFeatureWorkflow' => 'ArcanistFeatureBaseWorkflow', 'ArcanistFileConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistFileDataRef' => 'Phobject', 'ArcanistFileRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', ), 'ArcanistFileSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistFileUploader' => 'Phobject', 'ArcanistFilenameLinter' => 'ArcanistLinter', 'ArcanistFilenameLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistFilesystemAPI' => 'ArcanistRepositoryAPI', 'ArcanistFilesystemConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistFilesystemWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistFlake8Linter' => 'ArcanistExternalLinter', 'ArcanistFlake8LinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistFormattedStringXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistFormattedStringXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistFunctionCallShouldBeTypeCastXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistFunctionCallShouldBeTypeCastXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistFutureLinter' => 'ArcanistLinter', 'ArcanistGeneratedLinter' => 'ArcanistLinter', 'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistGetConfigWorkflow' => 'ArcanistWorkflow', 'ArcanistGitAPI' => 'ArcanistRepositoryAPI', 'ArcanistGitCommitMessageHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGitLandEngine' => 'ArcanistLandEngine', 'ArcanistGitLocalState' => 'ArcanistRepositoryLocalState', 'ArcanistGitUpstreamPath' => 'Phobject', 'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGlobalVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistGlobalVariableXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistGoLintLinter' => 'ArcanistExternalLinter', 'ArcanistGoLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistGoTestResultParser' => 'ArcanistTestResultParser', 'ArcanistGoTestResultParserTestCase' => 'PhutilTestCase', 'ArcanistHLintLinter' => 'ArcanistExternalLinter', 'ArcanistHLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistHardpoint' => 'Phobject', 'ArcanistHardpointEngine' => 'Phobject', 'ArcanistHardpointFutureList' => 'Phobject', 'ArcanistHardpointList' => 'Phobject', 'ArcanistHardpointObject' => 'Phobject', 'ArcanistHardpointQuery' => 'Phobject', 'ArcanistHardpointRequest' => 'Phobject', 'ArcanistHardpointRequestList' => 'Phobject', 'ArcanistHardpointTask' => 'Phobject', 'ArcanistHardpointTaskResult' => 'Phobject', 'ArcanistHelpWorkflow' => 'ArcanistWorkflow', 'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistHgClientChannel' => 'PhutilProtocolChannel', 'ArcanistHgProxyClient' => 'Phobject', 'ArcanistHgProxyServer' => 'Phobject', 'ArcanistHgServerChannel' => 'PhutilProtocolChannel', 'ArcanistImplicitConstructorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistImplicitConstructorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistImplicitFallthroughXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistImplicitFallthroughXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistImplicitVisibilityXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistImplicitVisibilityXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistImplodeArgumentOrderXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistImplodeArgumentOrderXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistInlineHTMLXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInlineHTMLXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistInnerFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInnerFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistInspectWorkflow' => 'ArcanistArcWorkflow', 'ArcanistInstallCertificateWorkflow' => 'ArcanistWorkflow', 'ArcanistInstanceOfOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInstanceofOperatorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistInterfaceAbstractMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInterfaceAbstractMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistInterfaceMethodBodyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInterfaceMethodBodyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistInvalidDefaultParameterXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInvalidDefaultParameterXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistInvalidModifiersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInvalidModifiersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistInvalidOctalNumericScalarXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistInvalidOctalNumericScalarXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistIsAShouldBeInstanceOfXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistIsAShouldBeInstanceOfXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistJSHintLinter' => 'ArcanistExternalLinter', 'ArcanistJSHintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistJSONLintLinter' => 'ArcanistExternalLinter', 'ArcanistJSONLintRenderer' => 'ArcanistLintRenderer', 'ArcanistJSONLinter' => 'ArcanistLinter', 'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistJscsLinter' => 'ArcanistExternalLinter', 'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistKeywordCasingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistLandCommit' => 'Phobject', + 'ArcanistLandCommitSet' => 'Phobject', 'ArcanistLandEngine' => 'Phobject', - 'ArcanistLandWorkflow' => 'ArcanistWorkflow', + 'ArcanistLandSymbol' => 'Phobject', + 'ArcanistLandTarget' => 'Phobject', + 'ArcanistLandWorkflow' => 'ArcanistArcWorkflow', 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLesscLinter' => 'ArcanistExternalLinter', 'ArcanistLesscLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistLiberateWorkflow' => 'ArcanistArcWorkflow', 'ArcanistLintEngine' => 'Phobject', 'ArcanistLintMessage' => 'Phobject', 'ArcanistLintMessageTestCase' => 'PhutilTestCase', 'ArcanistLintPatcher' => 'Phobject', 'ArcanistLintRenderer' => 'Phobject', 'ArcanistLintResult' => 'Phobject', 'ArcanistLintSeverity' => 'Phobject', 'ArcanistLintWorkflow' => 'ArcanistWorkflow', 'ArcanistLinter' => 'Phobject', 'ArcanistLinterStandard' => 'Phobject', 'ArcanistLinterStandardTestCase' => 'PhutilTestCase', 'ArcanistLinterTestCase' => 'PhutilTestCase', 'ArcanistLintersWorkflow' => 'ArcanistWorkflow', 'ArcanistListAssignmentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistListConfigOption' => 'ArcanistSingleSourceConfigOption', 'ArcanistListWorkflow' => 'ArcanistWorkflow', 'ArcanistLocalConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource', 'ArcanistLogEngine' => 'Phobject', 'ArcanistLogMessage' => 'Phobject', 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', + 'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', + 'ArcanistMercurialLandEngine' => 'ArcanistLandEngine', + 'ArcanistMercurialLocalState' => 'ArcanistRepositoryLocalState', 'ArcanistMercurialParser' => 'Phobject', 'ArcanistMercurialParserTestCase' => 'PhutilTestCase', 'ArcanistMercurialWorkingCopy' => 'ArcanistWorkingCopy', + 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', 'ArcanistMergeConflictLinter' => 'ArcanistLinter', 'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistMessageRevisionHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistMissingArgumentTerminatorException' => 'Exception', 'ArcanistMissingLinterException' => 'Exception', 'ArcanistModifierOrderingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistMultiSourceConfigOption' => 'ArcanistConfigOption', 'ArcanistNamespaceFirstStatementXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNamespaceFirstStatementXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistNamingConventionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNamingConventionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistNestedNamespacesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNestedNamespacesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistNewlineAfterOpenTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNewlineAfterOpenTagXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistNoEffectException' => 'ArcanistUsageException', 'ArcanistNoEngineException' => 'ArcanistUsageException', 'ArcanistNoLintLinter' => 'ArcanistLinter', 'ArcanistNoLintLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistNoParentScopeXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNoParentScopeXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistNoURIConduitException' => 'ArcanistConduitException', 'ArcanistNoneLintRenderer' => 'ArcanistLintRenderer', 'ArcanistObjectListHardpoint' => 'ArcanistHardpoint', 'ArcanistObjectOperatorSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistObjectOperatorSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPEP8Linter' => 'ArcanistExternalLinter', 'ArcanistPEP8LinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPHPCloseTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPHPCloseTagXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPHPCompatibilityXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPHPCompatibilityXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPHPEchoTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPHPEchoTagXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPHPOpenTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPHPOpenTagXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPHPShortTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPHPShortTagXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPaamayimNekudotayimSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPaamayimNekudotayimSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistParentMemberReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistParentMemberReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistParenthesesSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistParenthesesSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistParseStrUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistParseStrUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPasteRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', ), 'ArcanistPasteSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistPasteWorkflow' => 'ArcanistArcWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistWorkflow', 'ArcanistPhpLinter' => 'ArcanistExternalLinter', 'ArcanistPhpLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPhpcsLinter' => 'ArcanistExternalLinter', 'ArcanistPhpcsLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPhpunitTestResultParser' => 'ArcanistTestResultParser', 'ArcanistPhutilLibraryLinter' => 'ArcanistLinter', 'ArcanistPhutilWorkflow' => 'PhutilArgumentWorkflow', 'ArcanistPhutilXHPASTLinterStandard' => 'ArcanistLinterStandard', 'ArcanistPlusOperatorOnStringsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistProjectConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource', 'ArcanistPrompt' => 'Phobject', 'ArcanistPromptsWorkflow' => 'ArcanistWorkflow', 'ArcanistPublicPropertyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPuppetLintLinter' => 'ArcanistExternalLinter', 'ArcanistPuppetLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPyFlakesLinter' => 'ArcanistExternalLinter', 'ArcanistPyFlakesLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPyLintLinter' => 'ArcanistExternalLinter', 'ArcanistPyLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRaggedClassTreeEdgeXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistRef' => 'ArcanistHardpointObject', 'ArcanistRefInspector' => 'Phobject', 'ArcanistRepositoryAPI' => 'Phobject', 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase', 'ArcanistRepositoryLocalState' => 'Phobject', 'ArcanistRepositoryRef' => 'ArcanistRef', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedIteratorReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistReusedIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistRevisionAuthorHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionBuildableHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionCommitMessageHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionParentRevisionsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', ), 'ArcanistRevisionRefSource' => 'Phobject', 'ArcanistRevisionSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistRuboCopLinter' => 'ArcanistExternalLinter', 'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRubyLinter' => 'ArcanistExternalLinter', 'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRuntimeConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistRuntimeHardpointQuery' => 'ArcanistHardpointQuery', 'ArcanistScalarHardpoint' => 'ArcanistHardpoint', 'ArcanistScriptAndRegexLinter' => 'ArcanistLinter', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistSelfMemberReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSelfMemberReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistSemicolonSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSemicolonSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistSetConfigWorkflow' => 'ArcanistWorkflow', 'ArcanistSetting' => 'Phobject', 'ArcanistSettings' => 'Phobject', 'ArcanistShellCompleteWorkflow' => 'ArcanistWorkflow', 'ArcanistSimpleSymbolHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistSimpleSymbolRef' => 'ArcanistSymbolRef', 'ArcanistSimpleSymbolRefInspector' => 'ArcanistRefInspector', 'ArcanistSingleLintEngine' => 'ArcanistLintEngine', 'ArcanistSingleSourceConfigOption' => 'ArcanistConfigOption', 'ArcanistSlownessXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSlownessXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistSpellingLinter' => 'ArcanistLinter', 'ArcanistSpellingLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistStaticThisXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistStringConfigOption' => 'ArcanistSingleSourceConfigOption', 'ArcanistStringListConfigOption' => 'ArcanistListConfigOption', 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', 'ArcanistSubversionWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistSummaryLintRenderer' => 'ArcanistLintRenderer', 'ArcanistSymbolEngine' => 'Phobject', 'ArcanistSymbolRef' => 'ArcanistRef', 'ArcanistSyntaxErrorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSystemConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistTaskRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', ), 'ArcanistTaskSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistTasksWorkflow' => 'ArcanistWorkflow', 'ArcanistTautologicalExpressionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistTautologicalExpressionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistTestResultParser' => 'Phobject', 'ArcanistTestXHPASTLintSwitchHook' => 'ArcanistXHPASTLintSwitchHook', 'ArcanistTextLinter' => 'ArcanistLinter', 'ArcanistTextLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistThisReassignmentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistThisReassignmentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistToStringExceptionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistToStringExceptionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistTodoCommentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistTodoCommentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistTodoWorkflow' => 'ArcanistWorkflow', 'ArcanistToolset' => 'Phobject', 'ArcanistUSEnglishTranslation' => 'PhutilTranslation', 'ArcanistUnableToParseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnaryPostfixExpressionSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnaryPostfixExpressionSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUnaryPrefixExpressionSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnaryPrefixExpressionSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUndeclaredVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUndeclaredVariableXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUnexpectedReturnValueXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer', 'ArcanistUnitRenderer' => 'Phobject', 'ArcanistUnitTestEngine' => 'Phobject', 'ArcanistUnitTestResult' => 'Phobject', 'ArcanistUnitTestResultTestCase' => 'PhutilTestCase', 'ArcanistUnitTestableLintEngine' => 'ArcanistLintEngine', 'ArcanistUnitWorkflow' => 'ArcanistWorkflow', 'ArcanistUnnecessaryFinalModifierXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnnecessaryFinalModifierXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUnnecessarySemicolonXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnnecessarySymbolAliasXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnnecessarySymbolAliasXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUnsafeDynamicStringXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnsafeDynamicStringXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUpgradeWorkflow' => 'ArcanistArcWorkflow', 'ArcanistUploadWorkflow' => 'ArcanistArcWorkflow', 'ArcanistUsageException' => 'Exception', 'ArcanistUseStatementNamespacePrefixXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUseStatementNamespacePrefixXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUselessOverridingMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUselessOverridingMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUserAbortException' => 'ArcanistUsageException', 'ArcanistUserConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistUserRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', ), 'ArcanistUserSymbolHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistUserSymbolRef' => 'ArcanistSymbolRef', 'ArcanistUserSymbolRefInspector' => 'ArcanistRefInspector', 'ArcanistVariableReferenceSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistVariableVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistVariableVariableXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistVectorHardpoint' => 'ArcanistHardpoint', 'ArcanistVersionWorkflow' => 'ArcanistWorkflow', 'ArcanistWeldWorkflow' => 'ArcanistArcWorkflow', 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', 'ArcanistWildConfigOption' => 'ArcanistConfigOption', 'ArcanistWorkflow' => 'Phobject', 'ArcanistWorkflowArgument' => 'Phobject', 'ArcanistWorkflowGitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkflowInformation' => 'Phobject', + 'ArcanistWorkflowMercurialHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkingCopy' => 'Phobject', 'ArcanistWorkingCopyCommitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistWorkingCopyIdentity' => 'Phobject', 'ArcanistWorkingCopyPath' => 'Phobject', 'ArcanistWorkingCopyStateRef' => 'ArcanistRef', 'ArcanistWorkingCopyStateRefInspector' => 'ArcanistRefInspector', 'ArcanistXHPASTLintNamingHook' => 'Phobject', 'ArcanistXHPASTLintNamingHookTestCase' => 'PhutilTestCase', 'ArcanistXHPASTLintSwitchHook' => 'Phobject', 'ArcanistXHPASTLinter' => 'ArcanistBaseXHPASTLinter', 'ArcanistXHPASTLinterRule' => 'Phobject', 'ArcanistXHPASTLinterRuleTestCase' => 'ArcanistLinterTestCase', 'ArcanistXHPASTLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistXMLLinter' => 'ArcanistLinter', 'ArcanistXMLLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistXUnitTestResultParser' => 'Phobject', 'BaseHTTPFuture' => 'Future', 'CSharpToolsTestEngine' => 'XUnitTestEngine', 'CaseInsensitiveArray' => 'PhutilArray', 'CaseInsensitiveArrayTestCase' => 'PhutilTestCase', 'CommandException' => 'Exception', 'ConduitClient' => 'Phobject', 'ConduitClientException' => 'Exception', 'ConduitClientTestCase' => 'PhutilTestCase', 'ConduitFuture' => 'FutureProxy', 'ConduitSearchFuture' => 'FutureAgent', 'ExecFuture' => 'PhutilExecutableFuture', 'ExecFutureTestCase' => 'PhutilTestCase', 'ExecPassthruTestCase' => 'PhutilTestCase', 'FileFinder' => 'Phobject', 'FileFinderTestCase' => 'PhutilTestCase', 'FileList' => 'Phobject', 'Filesystem' => 'Phobject', 'FilesystemException' => 'Exception', 'FilesystemTestCase' => 'PhutilTestCase', 'Future' => 'Phobject', 'FutureAgent' => 'Future', 'FutureIterator' => array( 'Phobject', 'Iterator', ), 'FutureIteratorTestCase' => 'PhutilTestCase', 'FuturePool' => 'Phobject', 'FutureProxy' => 'Future', 'HTTPFuture' => 'BaseHTTPFuture', 'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatus' => 'Exception', 'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPSFuture' => 'BaseHTTPFuture', 'ImmediateFuture' => 'Future', 'LibphutilUSEnglishTranslation' => 'PhutilTranslation', 'LinesOfALarge' => array( 'Phobject', 'Iterator', ), 'LinesOfALargeExecFuture' => 'LinesOfALarge', 'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase', 'LinesOfALargeFile' => 'LinesOfALarge', 'LinesOfALargeFileTestCase' => 'PhutilTestCase', 'MFilterTestHelper' => 'Phobject', 'NoseTestEngine' => 'ArcanistUnitTestEngine', 'PHPASTParserTestCase' => 'PhutilTestCase', 'PhageAction' => 'Phobject', 'PhageAgentAction' => 'PhageAction', 'PhageAgentBootloader' => 'Phobject', 'PhageAgentTestCase' => 'PhutilTestCase', 'PhageExecWorkflow' => 'PhageWorkflow', 'PhageExecuteAction' => 'PhageAction', 'PhageLocalAction' => 'PhageAgentAction', 'PhagePHPAgent' => 'Phobject', 'PhagePHPAgentBootloader' => 'PhageAgentBootloader', 'PhagePlanAction' => 'PhageAction', 'PhageToolset' => 'ArcanistToolset', 'PhageWorkflow' => 'ArcanistWorkflow', 'Phobject' => 'Iterator', 'PhobjectTestCase' => 'PhutilTestCase', 'PhpunitTestEngine' => 'ArcanistUnitTestEngine', 'PhpunitTestEngineTestCase' => 'PhutilTestCase', 'PhutilAWSCloudFormationFuture' => 'PhutilAWSFuture', 'PhutilAWSCloudWatchFuture' => 'PhutilAWSFuture', 'PhutilAWSEC2Future' => 'PhutilAWSFuture', 'PhutilAWSException' => 'Exception', 'PhutilAWSFuture' => 'FutureProxy', 'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSS3Future' => 'PhutilAWSFuture', 'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow', 'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSv4Signature' => 'Phobject', 'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase', 'PhutilAggregateException' => 'Exception', 'PhutilAllCapsEnglishLocale' => 'PhutilLocale', 'PhutilArgumentParser' => 'Phobject', 'PhutilArgumentParserException' => 'Exception', 'PhutilArgumentParserTestCase' => 'PhutilTestCase', 'PhutilArgumentSpecification' => 'Phobject', 'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException', 'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase', 'PhutilArgumentSpellingCorrector' => 'Phobject', 'PhutilArgumentSpellingCorrectorTestCase' => 'PhutilTestCase', 'PhutilArgumentUsageException' => 'PhutilArgumentParserException', 'PhutilArgumentWorkflow' => 'Phobject', 'PhutilArray' => array( 'Phobject', 'Countable', 'ArrayAccess', 'Iterator', ), 'PhutilArrayCheck' => 'Phobject', 'PhutilArrayTestCase' => 'PhutilTestCase', 'PhutilArrayWithDefaultValue' => 'PhutilArray', 'PhutilAsanaFuture' => 'FutureProxy', 'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler', 'PhutilBallOfPHP' => 'Phobject', 'PhutilBinaryAnalyzer' => 'Phobject', 'PhutilBinaryAnalyzerTestCase' => 'PhutilTestCase', 'PhutilBootloaderException' => 'Exception', 'PhutilBritishEnglishLocale' => 'PhutilLocale', 'PhutilBufferedIterator' => array( 'Phobject', 'Iterator', ), 'PhutilBufferedIteratorTestCase' => 'PhutilTestCase', 'PhutilBugtraqParser' => 'Phobject', 'PhutilBugtraqParserTestCase' => 'PhutilTestCase', 'PhutilCIDRBlock' => 'Phobject', 'PhutilCIDRList' => 'Phobject', 'PhutilCallbackFilterIterator' => 'FilterIterator', 'PhutilCallbackSignalHandler' => 'PhutilSignalHandler', 'PhutilChannel' => 'Phobject', 'PhutilChannelChannel' => 'PhutilChannel', 'PhutilChannelTestCase' => 'PhutilTestCase', 'PhutilChunkedIterator' => array( 'Phobject', 'Iterator', ), 'PhutilChunkedIteratorTestCase' => 'PhutilTestCase', 'PhutilClassMapQuery' => 'Phobject', 'PhutilCloudWatchMetric' => 'Phobject', 'PhutilCommandString' => 'Phobject', 'PhutilConsole' => 'Phobject', 'PhutilConsoleBlock' => 'PhutilConsoleView', 'PhutilConsoleError' => 'PhutilConsoleLogLine', 'PhutilConsoleFormatter' => 'Phobject', 'PhutilConsoleInfo' => 'PhutilConsoleLogLine', 'PhutilConsoleList' => 'PhutilConsoleView', 'PhutilConsoleLogLine' => 'PhutilConsoleView', 'PhutilConsoleMessage' => 'Phobject', 'PhutilConsoleMetrics' => 'Phobject', 'PhutilConsoleMetricsSignalHandler' => 'PhutilSignalHandler', 'PhutilConsoleProgressBar' => 'Phobject', 'PhutilConsoleProgressSink' => 'PhutilProgressSink', 'PhutilConsoleServer' => 'Phobject', 'PhutilConsoleServerChannel' => 'PhutilChannelChannel', 'PhutilConsoleSkip' => 'PhutilConsoleLogLine', 'PhutilConsoleStdinNotInteractiveException' => 'Exception', 'PhutilConsoleTable' => 'PhutilConsoleView', 'PhutilConsoleView' => 'Phobject', 'PhutilConsoleWarning' => 'PhutilConsoleLogLine', 'PhutilConsoleWrapTestCase' => 'PhutilTestCase', 'PhutilCowsay' => 'Phobject', 'PhutilCowsayTestCase' => 'PhutilTestCase', 'PhutilCsprintfTestCase' => 'PhutilTestCase', 'PhutilCzechLocale' => 'PhutilLocale', 'PhutilDOMNode' => 'Phobject', 'PhutilDeferredLog' => 'Phobject', 'PhutilDeferredLogTestCase' => 'PhutilTestCase', 'PhutilDiffBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph', 'PhutilDirectoryFixture' => 'Phobject', 'PhutilDocblockParser' => 'Phobject', 'PhutilDocblockParserTestCase' => 'PhutilTestCase', 'PhutilEditDistanceMatrix' => 'Phobject', 'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase', 'PhutilEditorConfig' => 'Phobject', 'PhutilEditorConfigTestCase' => 'PhutilTestCase', 'PhutilEmailAddress' => 'Phobject', 'PhutilEmailAddressTestCase' => 'PhutilTestCase', 'PhutilEmojiLocale' => 'PhutilLocale', 'PhutilEnglishCanadaLocale' => 'PhutilLocale', 'PhutilErrorHandler' => 'Phobject', 'PhutilErrorHandlerTestCase' => 'PhutilTestCase', 'PhutilErrorTrap' => 'Phobject', 'PhutilEvent' => 'Phobject', 'PhutilEventConstants' => 'Phobject', 'PhutilEventEngine' => 'Phobject', 'PhutilEventListener' => 'Phobject', 'PhutilEventType' => 'PhutilEventConstants', 'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator', 'PhutilExecChannel' => 'PhutilChannel', 'PhutilExecPassthru' => 'PhutilExecutableFuture', 'PhutilExecutableFuture' => 'Future', 'PhutilExecutionEnvironment' => 'Phobject', 'PhutilFileLock' => 'PhutilLock', 'PhutilFileLockTestCase' => 'PhutilTestCase', 'PhutilFrenchLocale' => 'PhutilLocale', 'PhutilGermanLocale' => 'PhutilLocale', 'PhutilGitBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilGitHubFuture' => 'FutureProxy', 'PhutilGitHubResponse' => 'Phobject', 'PhutilGitURI' => 'Phobject', 'PhutilGitURITestCase' => 'PhutilTestCase', 'PhutilHTMLParser' => 'Phobject', 'PhutilHTMLParserTestCase' => 'PhutilTestCase', 'PhutilHTTPEngineExtension' => 'Phobject', 'PhutilHTTPResponse' => 'Phobject', 'PhutilHTTPResponseParser' => 'Phobject', 'PhutilHTTPResponseParserTestCase' => 'PhutilTestCase', 'PhutilHashingIterator' => array( 'PhutilProxyIterator', 'Iterator', ), 'PhutilHashingIteratorTestCase' => 'PhutilTestCase', 'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow', 'PhutilHgsprintfTestCase' => 'PhutilTestCase', 'PhutilINIParserException' => 'Exception', 'PhutilIPAddress' => 'Phobject', 'PhutilIPAddressTestCase' => 'PhutilTestCase', 'PhutilIPv4Address' => 'PhutilIPAddress', 'PhutilIPv6Address' => 'PhutilIPAddress', 'PhutilInteractiveEditor' => 'Phobject', 'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilInvalidStateException' => 'Exception', 'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase', 'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilJSON' => 'Phobject', 'PhutilJSONFragmentLexer' => 'PhutilLexer', 'PhutilJSONParser' => 'Phobject', 'PhutilJSONParserException' => 'Exception', 'PhutilJSONParserTestCase' => 'PhutilTestCase', 'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel', 'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase', 'PhutilJSONTestCase' => 'PhutilTestCase', 'PhutilJavaFragmentLexer' => 'PhutilLexer', 'PhutilKoreanLocale' => 'PhutilLocale', 'PhutilLanguageGuesser' => 'Phobject', 'PhutilLanguageGuesserTestCase' => 'PhutilTestCase', 'PhutilLexer' => 'Phobject', 'PhutilLibraryConflictException' => 'Exception', 'PhutilLibraryMapBuilder' => 'Phobject', 'PhutilLibraryTestCase' => 'PhutilTestCase', 'PhutilLocale' => 'Phobject', 'PhutilLocaleTestCase' => 'PhutilTestCase', 'PhutilLock' => 'Phobject', 'PhutilLockException' => 'Exception', 'PhutilLogFileChannel' => 'PhutilChannelChannel', 'PhutilLunarPhase' => 'Phobject', 'PhutilLunarPhaseTestCase' => 'PhutilTestCase', 'PhutilMercurialBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilMethodNotImplementedException' => 'Exception', 'PhutilMetricsChannel' => 'PhutilChannelChannel', 'PhutilMissingSymbolException' => 'Exception', 'PhutilModuleUtilsTestCase' => 'PhutilTestCase', 'PhutilNumber' => 'Phobject', 'PhutilOAuth1Future' => 'FutureProxy', 'PhutilOAuth1FutureTestCase' => 'PhutilTestCase', 'PhutilOpaqueEnvelope' => 'Phobject', 'PhutilOpaqueEnvelopeKey' => 'Phobject', 'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase', 'PhutilPHPFragmentLexer' => 'PhutilLexer', 'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase', 'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel', 'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase', 'PhutilParserGenerator' => 'Phobject', 'PhutilParserGeneratorException' => 'Exception', 'PhutilParserGeneratorTestCase' => 'PhutilTestCase', 'PhutilPayPalAPIFuture' => 'FutureProxy', 'PhutilPersonTest' => array( 'Phobject', 'PhutilPerson', ), 'PhutilPhtTestCase' => 'PhutilTestCase', 'PhutilPirateEnglishLocale' => 'PhutilLocale', 'PhutilPortugueseBrazilLocale' => 'PhutilLocale', 'PhutilPortuguesePortugalLocale' => 'PhutilLocale', 'PhutilPostmarkFuture' => 'FutureProxy', 'PhutilPregsprintfTestCase' => 'PhutilTestCase', 'PhutilProcessQuery' => 'Phobject', 'PhutilProcessRef' => 'Phobject', 'PhutilProcessRefTestCase' => 'PhutilTestCase', 'PhutilProgressSink' => 'Phobject', 'PhutilProtocolChannel' => 'PhutilChannelChannel', 'PhutilProxyException' => 'Exception', 'PhutilProxyIterator' => array( 'Phobject', 'Iterator', ), 'PhutilPygmentizeBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilPythonFragmentLexer' => 'PhutilLexer', 'PhutilQueryStringParser' => 'Phobject', 'PhutilQueryStringParserTestCase' => 'PhutilTestCase', 'PhutilRawEnglishLocale' => 'PhutilLocale', 'PhutilReadableSerializer' => 'Phobject', 'PhutilReadableSerializerTestCase' => 'PhutilTestCase', 'PhutilRope' => 'Phobject', 'PhutilRopeTestCase' => 'PhutilTestCase', 'PhutilServiceProfiler' => 'Phobject', 'PhutilShellLexer' => 'PhutilLexer', 'PhutilShellLexerTestCase' => 'PhutilTestCase', 'PhutilSignalHandler' => 'Phobject', 'PhutilSignalRouter' => 'Phobject', 'PhutilSimpleOptions' => 'Phobject', 'PhutilSimpleOptionsLexer' => 'PhutilLexer', 'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase', 'PhutilSimpleOptionsTestCase' => 'PhutilTestCase', 'PhutilSimplifiedChineseLocale' => 'PhutilLocale', 'PhutilSlackFuture' => 'FutureProxy', 'PhutilSocketChannel' => 'PhutilChannel', 'PhutilSortVector' => 'Phobject', 'PhutilSpanishSpainLocale' => 'PhutilLocale', 'PhutilStreamIterator' => array( 'Phobject', 'Iterator', ), 'PhutilSubversionBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilSystem' => 'Phobject', 'PhutilSystemTestCase' => 'PhutilTestCase', 'PhutilTerminalString' => 'Phobject', 'PhutilTestCase' => 'Phobject', 'PhutilTestCaseTestCase' => 'PhutilTestCase', 'PhutilTestPhobject' => 'Phobject', 'PhutilTestSkippedException' => 'Exception', 'PhutilTestTerminatedException' => 'Exception', 'PhutilTraditionalChineseLocale' => 'PhutilLocale', 'PhutilTranslation' => 'Phobject', 'PhutilTranslationTestCase' => 'PhutilTestCase', 'PhutilTranslator' => 'Phobject', 'PhutilTranslatorTestCase' => 'PhutilTestCase', 'PhutilTsprintfTestCase' => 'PhutilTestCase', 'PhutilTwitchFuture' => 'FutureProxy', 'PhutilTypeCheckException' => 'Exception', 'PhutilTypeExtraParametersException' => 'Exception', 'PhutilTypeLexer' => 'PhutilLexer', 'PhutilTypeMissingParametersException' => 'Exception', 'PhutilTypeSpec' => 'Phobject', 'PhutilTypeSpecTestCase' => 'PhutilTestCase', 'PhutilURI' => 'Phobject', 'PhutilURITestCase' => 'PhutilTestCase', 'PhutilUSEnglishLocale' => 'PhutilLocale', 'PhutilUTF8StringTruncator' => 'Phobject', 'PhutilUTF8TestCase' => 'PhutilTestCase', 'PhutilUnitTestEngine' => 'ArcanistUnitTestEngine', 'PhutilUnitTestEngineTestCase' => 'PhutilTestCase', 'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUrisprintfTestCase' => 'PhutilTestCase', 'PhutilUtilsTestCase' => 'PhutilTestCase', 'PhutilVeryWowEnglishLocale' => 'PhutilLocale', 'PhutilWordPressFuture' => 'FutureProxy', 'PhutilXHPASTBinary' => 'Phobject', 'PytestTestEngine' => 'ArcanistUnitTestEngine', 'TempFile' => 'Phobject', 'TestAbstractDirectedGraph' => 'AbstractDirectedGraph', 'XHPASTNode' => 'AASTNode', 'XHPASTNodeTestCase' => 'PhutilTestCase', 'XHPASTSyntaxErrorException' => 'Exception', 'XHPASTToken' => 'AASTToken', 'XHPASTTree' => 'AASTTree', 'XHPASTTreeTestCase' => 'PhutilTestCase', 'XUnitTestEngine' => 'ArcanistUnitTestEngine', 'XUnitTestResultParserTestCase' => 'PhutilTestCase', 'XsprintfUnknownConversionException' => 'InvalidArgumentException', ), )); diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php index e7f6d379..62e88c12 100644 --- a/src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -1,155 +1,151 @@ 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"', ), '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', ), '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, Arcanist can figure this association '. 'out on its own, but if your setup is unusual you can use '. 'this option to tell it what the desired value is.')) ->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.')), id(new ArcanistStringListConfigOption()) ->setKey('arc.land.onto') ->setDefaultValue(array()) ->setSummary(pht('Default list of "onto" refs for "arc land".')) ->setHelp( pht( 'Specifies the default behavior when "arc land" is run with '. 'no "--onto" flag.')) ->setExamples( array( '["master"]', )), id(new ArcanistStringConfigOption()) ->setKey('arc.land.onto-remote') ->setSummary(pht('Default list of "onto" remote for "arc land".')) ->setHelp( pht( 'Specifies the default behavior when "arc land" is run with '. 'no "--onto-remote" flag.')) ->setExamples( array( 'origin', )), - id(new ArcanistBoolConfigOption()) - ->setKey('history.immutable') + id(new ArcanistStringConfigOption()) + ->setKey('arc.land.strategy') ->setSummary( pht( - 'Configure use of history mutation operations like amends '. - 'and rebases.')) + 'Configure a default merge strategy for "arc land".')) ->setHelp( pht( - 'If this option is set to "true", Arcanist will treat the '. - 'repository history as immutable and will never issue '. - 'commands which rewrite repository history (like amends or '. - 'rebases). This option defaults to "true" in Mercurial, '. - '"false" in Git, and has no effect in Subversion.')), + 'Specifies the default behavior when "arc land" is run with '. + 'no "--strategy" flag.')), ); } } diff --git a/src/land/ArcanistGitLandEngine.php b/src/land/ArcanistGitLandEngine.php deleted file mode 100644 index c0a7a870..00000000 --- a/src/land/ArcanistGitLandEngine.php +++ /dev/null @@ -1,913 +0,0 @@ -isGitPerforce = $is_git_perforce; - return $this; - } - - private function getIsGitPerforce() { - return $this->isGitPerforce; - } - - public function parseArguments() { - $api = $this->getRepositoryAPI(); - - $onto = $this->getEngineOnto(); - $this->setTargetOnto($onto); - - $remote = $this->getEngineRemote(); - - $is_pushable = $api->isPushableRemote($remote); - $is_perforce = $api->isPerforceRemote($remote); - - if (!$is_pushable && !$is_perforce) { - throw new PhutilArgumentUsageException( - pht( - 'No pushable remote "%s" exists. Use the "--remote" flag to choose '. - 'a valid, pushable remote to land changes onto.', - $remote)); - } - - if ($is_perforce) { - $this->setIsGitPerforce(true); - $this->writeWarn( - pht('P4 MODE'), - pht( - 'Operating in Git/Perforce mode after selecting a Perforce '. - 'remote.')); - - if (!$this->getShouldSquash()) { - throw new PhutilArgumentUsageException( - pht( - 'Perforce mode does not support the "merge" land strategy. '. - 'Use the "squash" land strategy when landing to a Perforce '. - 'remote (you can use "--squash" to select this strategy).')); - } - } - - $this->setTargetRemote($remote); - } - - public function execute() { - $this->verifySourceAndTargetExist(); - $this->fetchTarget(); - - $this->printLandingCommits(); - - if ($this->getShouldPreview()) { - $this->writeInfo( - pht('PREVIEW'), - pht('Completed preview of operation.')); - return; - } - - $this->saveLocalState(); - - try { - $this->identifyRevision(); - $this->updateWorkingCopy(); - - if ($this->getShouldHold()) { - $this->didHoldChanges(); - } else { - $this->pushChange(); - $this->reconcileLocalState(); - - $api = $this->getRepositoryAPI(); - $api->execxLocal('submodule update --init --recursive'); - - if ($this->getShouldKeep()) { - echo tsprintf( - "%s\n", - pht('Keeping local branch.')); - } else { - $this->destroyLocalBranch(); - } - - $this->writeOkay( - pht('DONE'), - pht('Landed changes.')); - } - - $this->restoreWhenDestroyed = false; - } catch (Exception $ex) { - $this->restoreLocalState(); - throw $ex; - } - } - - public function __destruct() { - if ($this->restoreWhenDestroyed) { - $this->writeWarn( - pht('INTERRUPTED!'), - pht('Restoring working copy to its original state.')); - - $this->restoreLocalState(); - } - } - - protected function getLandingCommits() { - $api = $this->getRepositoryAPI(); - - list($out) = $api->execxLocal( - 'log --oneline %s..%s --', - $this->getTargetFullRef(), - $this->sourceCommit); - - $out = trim($out); - - if (!strlen($out)) { - return array(); - } else { - return phutil_split_lines($out, false); - } - } - - private function identifyRevision() { - $api = $this->getRepositoryAPI(); - $api->execxLocal('checkout %s --', $this->getSourceRef()); - call_user_func($this->getBuildMessageCallback(), $this); - } - - private function verifySourceAndTargetExist() { - $api = $this->getRepositoryAPI(); - - list($err) = $api->execManualLocal( - 'rev-parse --verify %s', - $this->getTargetFullRef()); - - if ($err) { - $this->writeWarn( - pht('TARGET'), - pht( - 'No local ref exists for branch "%s" in remote "%s", attempting '. - 'fetch...', - $this->getTargetOnto(), - $this->getTargetRemote())); - - $api->execManualLocal( - 'fetch %s %s --', - $this->getTargetRemote(), - $this->getTargetOnto()); - - list($err) = $api->execManualLocal( - 'rev-parse --verify %s', - $this->getTargetFullRef()); - if ($err) { - throw new Exception( - pht( - 'Branch "%s" does not exist in remote "%s".', - $this->getTargetOnto(), - $this->getTargetRemote())); - } - - $this->writeInfo( - pht('FETCHED'), - pht( - 'Fetched branch "%s" from remote "%s".', - $this->getTargetOnto(), - $this->getTargetRemote())); - } - - list($err, $stdout) = $api->execManualLocal( - 'rev-parse --verify %s', - $this->getSourceRef()); - - if ($err) { - throw new Exception( - pht( - 'Branch "%s" does not exist in the local working copy.', - $this->getSourceRef())); - } - - $this->sourceCommit = trim($stdout); - } - - private function fetchTarget() { - $api = $this->getRepositoryAPI(); - - $ref = $this->getTargetFullRef(); - - // NOTE: Although this output isn't hugely useful, we need to passthru - // instead of using a subprocess here because `git fetch` may prompt the - // user to enter a password if they're fetching over HTTP with basic - // authentication. See T10314. - - if ($this->getIsGitPerforce()) { - $this->writeInfo( - pht('P4 SYNC'), - pht('Synchronizing "%s" from Perforce...', $ref)); - - $sync_ref = sprintf( - 'refs/remotes/%s/%s', - $this->getTargetRemote(), - $this->getTargetOnto()); - - $err = $api->execPassthru( - 'p4 sync --silent --branch %R --', - $sync_ref); - - if ($err) { - throw new ArcanistUsageException( - pht( - 'Perforce sync failed! Fix the error and run "arc land" again.')); - } - } else { - $this->writeInfo( - pht('FETCH'), - pht('Fetching "%s"...', $ref)); - - $err = $api->execPassthru( - 'fetch --quiet -- %s %s', - $this->getTargetRemote(), - $this->getTargetOnto()); - - if ($err) { - throw new ArcanistUsageException( - pht( - 'Fetch failed! Fix the error and run "arc land" again.')); - } - } - } - - private function updateWorkingCopy() { - $api = $this->getRepositoryAPI(); - $source = $this->sourceCommit; - - $api->execxLocal( - 'checkout %s --', - $this->getTargetFullRef()); - - list($original_author, $original_date) = $this->getAuthorAndDate($source); - - try { - if ($this->getShouldSquash()) { - // NOTE: We're explicitly specifying "--ff" to override the presence - // of "merge.ff" options in user configuration. - - $api->execxLocal( - 'merge --no-stat --no-commit --ff --squash -- %s', - $source); - } else { - $api->execxLocal( - 'merge --no-stat --no-commit --no-ff -- %s', - $source); - } - } catch (Exception $ex) { - $api->execManualLocal('merge --abort'); - $api->execManualLocal('reset --hard HEAD --'); - - throw new Exception( - pht( - 'Local "%s" does not merge cleanly into "%s". Merge or rebase '. - 'local changes so they can merge cleanly.', - $this->getSourceRef(), - $this->getTargetFullRef())); - } - - // TODO: This could probably be cleaner by asking the API a question - // about working copy status instead of running a raw diff command. See - // discussion in T11435. - list($changes) = $api->execxLocal('diff --no-ext-diff HEAD --'); - $changes = trim($changes); - if (!strlen($changes)) { - throw new Exception( - pht( - 'Merging local "%s" into "%s" produces an empty diff. '. - 'This usually means these changes have already landed.', - $this->getSourceRef(), - $this->getTargetFullRef())); - } - - $api->execxLocal( - 'commit --author %s --date %s -F %s --', - $original_author, - $original_date, - $this->getCommitMessageFile()); - - $this->getWorkflow()->didCommitMerge(); - - list($stdout) = $api->execxLocal( - 'rev-parse --verify %s', - 'HEAD'); - $this->mergedRef = trim($stdout); - } - - private function pushChange() { - $api = $this->getRepositoryAPI(); - - if ($this->getIsGitPerforce()) { - $this->writeInfo( - pht('SUBMITTING'), - pht('Submitting changes to "%s".', $this->getTargetFullRef())); - - $config_argv = array(); - - // Skip the "git p4 submit" interactive editor workflow. We expect - // the commit message that "arc land" has built to be satisfactory. - $config_argv[] = '-c'; - $config_argv[] = 'git-p4.skipSubmitEdit=true'; - - // Skip the "git p4 submit" confirmation prompt if the user does not edit - // the submit message. - $config_argv[] = '-c'; - $config_argv[] = 'git-p4.skipSubmitEditCheck=true'; - - $flags_argv = array(); - - // Disable implicit "git p4 rebase" as part of submit. We're allowing - // the implicit "git p4 sync" to go through since this puts us in a - // state which is generally similar to the state after "git push", with - // updated remotes. - - // We could do a manual "git p4 sync" with a more narrow "--branch" - // instead, but it's not clear that this is beneficial. - $flags_argv[] = '--disable-rebase'; - - // Detect moves and submit them to Perforce as move operations. - $flags_argv[] = '-M'; - - // If we run into a conflict, abort the operation. We expect users to - // fix conflicts and run "arc land" again. - $flags_argv[] = '--conflict=quit'; - - $err = $api->execPassthru( - '%LR p4 submit %LR --commit %R --', - $config_argv, - $flags_argv, - $this->mergedRef); - - if ($err) { - throw new ArcanistUsageException( - pht( - 'Submit failed! Fix the error and run "arc land" again.')); - } - } else { - $this->writeInfo( - pht('PUSHING'), - pht('Pushing changes to "%s".', $this->getTargetFullRef())); - - $err = $api->execPassthru( - 'push -- %s %s:%s', - $this->getTargetRemote(), - $this->mergedRef, - $this->getTargetOnto()); - - if ($err) { - throw new ArcanistUsageException( - pht( - 'Push failed! Fix the error and run "arc land" again.')); - } - } - } - - private function reconcileLocalState() { - $api = $this->getRepositoryAPI(); - - // Try to put the user into the best final state we can. This is very - // complicated because users are incredibly creative and their local - // branches may have the same names as branches in the remote but no - // relationship to them. - - if ($this->localRef != $this->getSourceRef()) { - // The user ran `arc land X` but was on a different branch, so just put - // them back wherever they were before. - $this->writeInfo( - pht('RESTORE'), - pht('Switching back to "%s".', $this->localRef)); - $this->restoreLocalState(); - return; - } - - // We're going to try to find a path to the upstream target branch. We - // try in two different ways: - // - // - follow the source branch directly along tracking branches until - // we reach the upstream; or - // - follow a local branch with the same name as the target branch until - // we reach the upstream. - - // First, get the path from whatever we landed to wherever it goes. - $local_branch = $this->getSourceRef(); - - $path = $api->getPathToUpstream($local_branch); - if ($path->getLength()) { - // We may want to discard the thing we landed from the path, if we're - // going to delete it. In this case, we don't want to update it or worry - // if it's dirty. - if ($this->getSourceRef() == $this->getTargetOnto()) { - // In this case, we've done something like land "master" onto itself, - // so we do want to update the actual branch. We're going to use the - // entire path. - } else { - // Otherwise, we're going to delete the branch at the end of the - // workflow, so throw it away the most-local branch that isn't long - // for this world. - $path->removeUpstream($local_branch); - - if (!$path->getLength()) { - // The local branch tracked upstream directly; however, it - // may not be the only one to do so. If there's a local - // branch of the same name that tracks the remote, try - // switching to that. - $local_branch = $this->getTargetOnto(); - list($err) = $api->execManualLocal( - 'rev-parse --verify %s', - $local_branch); - if (!$err) { - $path = $api->getPathToUpstream($local_branch); - } - if (!$path->isConnectedToRemote()) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local branch "%s" directly tracks remote, staying on '. - 'detached HEAD.', - $local_branch)); - return; - } - } - - $local_branch = head($path->getLocalBranches()); - } - } else { - // The source branch has no upstream, so look for a local branch with - // the same name as the target branch. This corresponds to the common - // case where you have "master" and checkout local branches from it - // with "git checkout -b feature", then land onto "master". - - $local_branch = $this->getTargetOnto(); - - list($err) = $api->execManualLocal( - 'rev-parse --verify %s', - $local_branch); - if ($err) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local branch "%s" does not exist, staying on detached HEAD.', - $local_branch)); - return; - } - - $path = $api->getPathToUpstream($local_branch); - } - - if ($path->getCycle()) { - $this->writeWarn( - pht('LOCAL CYCLE'), - pht( - 'Local branch "%s" tracks an upstream but following it leads to '. - 'a local cycle, staying on detached HEAD.', - $local_branch)); - return; - } - - $is_perforce = $this->getIsGitPerforce(); - - if ($is_perforce) { - // If we're in Perforce mode, we don't expect to have a meaningful - // path to the remote: the "p4" remote is not a real remote, and - // "git p4" commands do not configure branch upstreams to provide - // a path. - - // Just pretend the target branch is connected directly to the remote, - // since this is effectively the behavior of Perforce and appears to - // do the right thing. - $cascade_branches = array($local_branch); - } else { - if (!$path->isConnectedToRemote()) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local branch "%s" is not connected to a remote, staying on '. - 'detached HEAD.', - $local_branch)); - return; - } - - $remote_remote = $path->getRemoteRemoteName(); - $remote_branch = $path->getRemoteBranchName(); - - $remote_actual = $remote_remote.'/'.$remote_branch; - $remote_expect = $this->getTargetFullRef(); - if ($remote_actual != $remote_expect) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local branch "%s" is connected to a remote ("%s") other than '. - 'the target remote ("%s"), staying on detached HEAD.', - $local_branch, - $remote_actual, - $remote_expect)); - return; - } - - // If we get this far, we have a sequence of branches which ultimately - // connect to the remote. We're going to try to update them all in reverse - // order, from most-upstream to most-local. - - $cascade_branches = $path->getLocalBranches(); - $cascade_branches = array_reverse($cascade_branches); - } - - // First, check if any of them are ahead of the remote. - - $ahead_of_remote = array(); - foreach ($cascade_branches as $cascade_branch) { - list($stdout) = $api->execxLocal( - 'log %s..%s --', - $this->mergedRef, - $cascade_branch); - $stdout = trim($stdout); - - if (strlen($stdout)) { - $ahead_of_remote[$cascade_branch] = $cascade_branch; - } - } - - // We're going to handle the last branch (the thing we ultimately intend - // to check out) differently. It's OK if it's ahead of the remote, as long - // as we just landed it. - - $local_ahead = isset($ahead_of_remote[$local_branch]); - unset($ahead_of_remote[$local_branch]); - $land_self = ($this->getTargetOnto() === $this->getSourceRef()); - - // We aren't going to pull anything if anything upstream from us is ahead - // of the remote, or the local is ahead of the remote and we didn't land - // it onto itself. - $skip_pull = ($ahead_of_remote || ($local_ahead && !$land_self)); - - if ($skip_pull) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local "%s" is ahead of remote "%s". Checking out "%s" but '. - 'not pulling changes.', - nonempty(head($ahead_of_remote), $local_branch), - $this->getTargetFullRef(), - $local_branch)); - - $this->writeInfo( - pht('CHECKOUT'), - pht( - 'Checking out "%s".', - $local_branch)); - - $api->execxLocal('checkout %s --', $local_branch); - - return; - } - - // If nothing upstream from our nearest branch is ahead of the remote, - // pull it all. - - $cascade_targets = array(); - if (!$ahead_of_remote) { - foreach ($cascade_branches as $cascade_branch) { - if ($local_ahead && ($local_branch == $cascade_branch)) { - continue; - } - $cascade_targets[] = $cascade_branch; - } - } - - if ($is_perforce) { - // In Perforce, we've already set the remote to the right state with an - // implicit "git p4 sync" during "git p4 submit", and "git pull" isn't a - // meaningful operation. We're going to skip this step and jump down to - // the "git reset --hard" below to get everything into the right state. - } else if ($cascade_targets) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local "%s" tracks target remote "%s", checking out and '. - 'pulling changes.', - $local_branch, - $this->getTargetFullRef())); - - foreach ($cascade_targets as $cascade_branch) { - $this->writeInfo( - pht('PULL'), - pht( - 'Checking out and pulling "%s".', - $cascade_branch)); - - $api->execxLocal('checkout %s --', $cascade_branch); - $api->execxLocal( - 'pull %s %s --', - $this->getTargetRemote(), - $cascade_branch); - } - - if (!$local_ahead) { - return; - } - } - - // In this case, the user did something like land a branch onto itself, - // and the branch is tracking the correct remote. We're going to discard - // the local state and reset it to the state we just pushed. - - $this->writeInfo( - pht('RESET'), - pht( - 'Local "%s" landed into remote "%s", resetting local branch to '. - 'remote state.', - $this->getTargetOnto(), - $this->getTargetFullRef())); - - $api->execxLocal('checkout %s --', $local_branch); - $api->execxLocal('reset --hard %s --', $this->getTargetFullRef()); - - return; - } - - private function destroyLocalBranch() { - $api = $this->getRepositoryAPI(); - $source_ref = $this->getSourceRef(); - - if ($source_ref == $this->getTargetOnto()) { - // If we landed a branch into a branch with the same name, so don't - // destroy it. This prevents us from cleaning up "master" if you're - // landing master into itself. - return; - } - - // TODO: Maybe this should also recover the proper upstream? - - // See T10321. If we were not landing a branch, don't try to clean it up. - // This happens most often when landing from a detached HEAD. - $is_branch = $this->isBranch($source_ref); - if (!$is_branch) { - echo tsprintf( - "%s\n", - pht( - '(Source "%s" is not a branch, leaving working copy as-is.)', - $source_ref)); - return; - } - - $recovery_command = csprintf( - 'git checkout -b %R %R', - $source_ref, - $this->sourceCommit); - - echo tsprintf( - "%s\n", - pht('Cleaning up branch "%s"...', $source_ref)); - - echo tsprintf( - "%s\n", - pht('(Use `%s` if you want it back.)', $recovery_command)); - - $api->execxLocal('branch -D -- %s', $source_ref); - } - - /** - * Save the local working copy state so we can restore it later. - */ - private function saveLocalState() { - $api = $this->getRepositoryAPI(); - - $this->localCommit = $api->getWorkingCopyRevision(); - - list($ref) = $api->execxLocal('rev-parse --abbrev-ref HEAD'); - $ref = trim($ref); - if ($ref === 'HEAD') { - $ref = $this->localCommit; - } - - $this->localRef = $ref; - - $this->restoreWhenDestroyed = true; - } - - /** - * Restore the working copy to the state it was in before we started - * performing writes. - */ - private function restoreLocalState() { - $api = $this->getRepositoryAPI(); - - $api->execxLocal('checkout %s --', $this->localRef); - $api->execxLocal('reset --hard %s --', $this->localCommit); - $api->execxLocal('submodule update --init --recursive'); - - $this->restoreWhenDestroyed = false; - } - - private function getTargetFullRef() { - return $this->getTargetRemote().'/'.$this->getTargetOnto(); - } - - private function getAuthorAndDate($commit) { - $api = $this->getRepositoryAPI(); - - // TODO: This is working around Windows escaping problems, see T8298. - - list($info) = $api->execxLocal( - 'log -n1 --format=%C %s --', - '%aD%n%an%n%ae', - $commit); - - $info = trim($info); - list($date, $author, $email) = explode("\n", $info, 3); - - return array( - "$author <{$email}>", - $date, - ); - } - - private function didHoldChanges() { - if ($this->getIsGitPerforce()) { - $this->writeInfo( - pht('HOLD'), - pht( - 'Holding change locally, it has not been submitted.')); - - $push_command = csprintf( - '$ git p4 submit -M --commit %R --', - $this->mergedRef); - } else { - $this->writeInfo( - pht('HOLD'), - pht( - 'Holding change locally, it has not been pushed.')); - - $push_command = csprintf( - '$ git push -- %R %R:%R', - $this->getTargetRemote(), - $this->mergedRef, - $this->getTargetOnto()); - } - - $restore_command = csprintf( - '$ git checkout %R --', - $this->localRef); - - echo tsprintf( - "\n%s\n\n". - "%s\n\n". - " **%s**\n\n". - "%s\n\n". - " **%s**\n\n". - "%s\n", - pht( - 'This local working copy now contains the merged changes in a '. - 'detached state.'), - pht('You can push the changes manually with this command:'), - $push_command, - pht( - 'You can go back to how things were before you ran "arc land" with '. - 'this command:'), - $restore_command, - pht( - 'Local branches have not been changed, and are still in exactly the '. - 'same state as before.')); - } - - private function isBranch($ref) { - $api = $this->getRepositoryAPI(); - - list($err) = $api->execManualLocal( - 'show-ref --verify --quiet -- %R', - 'refs/heads/'.$ref); - - return !$err; - } - - private function getEngineOnto() { - $source_ref = $this->getSourceRef(); - - $onto = $this->getOntoArgument(); - if ($onto !== null) { - $this->writeInfo( - pht('TARGET'), - pht( - 'Landing onto "%s", selected with the "--onto" flag.', - $onto)); - return $onto; - } - - $api = $this->getRepositoryAPI(); - $path = $api->getPathToUpstream($source_ref); - - if ($path->getLength()) { - $cycle = $path->getCycle(); - if ($cycle) { - $this->writeWarn( - pht('LOCAL CYCLE'), - pht( - 'Local branch tracks an upstream, but following it leads to a '. - 'local cycle; ignoring branch upstream.')); - - echo tsprintf( - "\n %s\n\n", - implode(' -> ', $cycle)); - - } else { - if ($path->isConnectedToRemote()) { - $onto = $path->getRemoteBranchName(); - $this->writeInfo( - pht('TARGET'), - pht( - 'Landing onto "%s", selected by following tracking branches '. - 'upstream to the closest remote.', - $onto)); - return $onto; - } else { - $this->writeInfo( - pht('NO PATH TO UPSTREAM'), - pht( - 'Local branch tracks an upstream, but there is no path '. - 'to a remote; ignoring branch upstream.')); - } - } - } - - $workflow = $this->getWorkflow(); - - $config_key = 'arc.land.onto.default'; - $onto = $workflow->getConfigFromAnySource($config_key); - if ($onto !== null) { - $this->writeInfo( - pht('TARGET'), - pht( - 'Landing onto "%s", selected by "%s" configuration.', - $onto, - $config_key)); - return $onto; - } - - $onto = 'master'; - $this->writeInfo( - pht('TARGET'), - pht( - 'Landing onto "%s", the default target under git.', - $onto)); - - return $onto; - } - - private function getEngineRemote() { - $source_ref = $this->getSourceRef(); - - $remote = $this->getRemoteArgument(); - if ($remote !== null) { - $this->writeInfo( - pht('REMOTE'), - pht( - 'Using remote "%s", selected with the "--remote" flag.', - $remote)); - return $remote; - } - - $api = $this->getRepositoryAPI(); - $path = $api->getPathToUpstream($source_ref); - - $remote = $path->getRemoteRemoteName(); - if ($remote !== null) { - $this->writeInfo( - pht('REMOTE'), - pht( - 'Using remote "%s", selected by following tracking branches '. - 'upstream to the closest remote.', - $remote)); - return $remote; - } - - $remote = 'p4'; - if ($api->isPerforceRemote($remote)) { - $this->writeInfo( - pht('REMOTE'), - pht( - 'Using Perforce remote "%s". The existence of this remote implies '. - 'this working copy was synchronized from a Perforce repository.', - $remote)); - return $remote; - } - - $remote = 'origin'; - $this->writeInfo( - pht('REMOTE'), - pht( - 'Using remote "%s", the default remote under Git.', - $remote)); - - return $remote; - } - -} diff --git a/src/land/ArcanistLandCommit.php b/src/land/ArcanistLandCommit.php new file mode 100644 index 00000000..527611ce --- /dev/null +++ b/src/land/ArcanistLandCommit.php @@ -0,0 +1,149 @@ +hash = $hash; + return $this; + } + + public function getHash() { + return $this->hash; + } + + public function setSummary($summary) { + $this->summary = $summary; + return $this; + } + + public function getSummary() { + return $this->summary; + } + + public function getDisplaySummary() { + if ($this->displaySummary === null) { + $this->displaySummary = id(new PhutilUTF8StringTruncator()) + ->setMaximumGlyphs(64) + ->truncateString($this->getSummary()); + } + return $this->displaySummary; + } + + public function setParents(array $parents) { + $this->parents = $parents; + return $this; + } + + public function getParents() { + return $this->parents; + } + + public function addSymbol(ArcanistLandSymbol $symbol) { + $this->symbols[] = $symbol; + return $this; + } + + public function getSymbols() { + return $this->symbols; + } + + public function setExplicitRevisionref(ArcanistRevisionRef $ref) { + $this->explicitRevisionRef = $ref; + return $this; + } + + public function getExplicitRevisionref() { + return $this->explicitRevisionRef; + } + + public function setParentCommits(array $parent_commits) { + $this->parentCommits = $parent_commits; + return $this; + } + + public function getParentCommits() { + return $this->parentCommits; + } + + public function setIsHeadCommit($is_head_commit) { + $this->isHeadCommit = $is_head_commit; + return $this; + } + + public function getIsHeadCommit() { + return $this->isHeadCommit; + } + + public function setIsImplicitCommit($is_implicit_commit) { + $this->isImplicitCommit = $is_implicit_commit; + return $this; + } + + public function getIsImplicitCommit() { + return $this->isImplicitCommit; + } + + public function getAncestorRevisionPHIDs() { + $phids = array(); + + foreach ($this->getParentCommits() as $parent_commit) { + $phids += $parent_commit->getAncestorRevisionPHIDs(); + } + + $revision_ref = $this->getRevisionRef(); + if ($revision_ref) { + $phids[$revision_ref->getPHID()] = $revision_ref->getPHID(); + } + + return $phids; + } + + public function getRevisionRef() { + if ($this->revisionRef === false) { + $this->revisionRef = $this->newRevisionRef(); + } + + return $this->revisionRef; + } + + private function newRevisionRef() { + $revision_ref = $this->getExplicitRevisionRef(); + if ($revision_ref) { + return $revision_ref; + } + + $parent_refs = array(); + foreach ($this->getParentCommits() as $parent_commit) { + $parent_ref = $parent_commit->getRevisionRef(); + if ($parent_ref) { + $parent_refs[$parent_ref->getPHID()] = $parent_ref; + } + } + + if (count($parent_refs) > 1) { + throw new Exception( + pht( + 'Too many distinct parent refs!')); + } + + if ($parent_refs) { + return head($parent_refs); + } + + return null; + } + + +} diff --git a/src/land/ArcanistLandCommitSet.php b/src/land/ArcanistLandCommitSet.php new file mode 100644 index 00000000..bdf34ce0 --- /dev/null +++ b/src/land/ArcanistLandCommitSet.php @@ -0,0 +1,52 @@ +revisionRef = $revision_ref; + return $this; + } + + public function getRevisionRef() { + return $this->revisionRef; + } + + public function setCommits(array $commits) { + assert_instances_of($commits, 'ArcanistLandCommit'); + $this->commits = $commits; + + $revision_phid = $this->getRevisionRef()->getPHID(); + foreach ($commits as $commit) { + $revision_ref = $commit->getExplicitRevisionRef(); + + if ($revision_ref) { + if ($revision_ref->getPHID() === $revision_phid) { + continue; + } + } + + $commit->setIsImplicitCommit(true); + } + + return $this; + } + + public function getCommits() { + return $this->commits; + } + + public function hasImplicitCommits() { + foreach ($this->commits as $commit) { + if ($commit->getIsImplicitCommit()) { + return true; + } + } + + return false; + } + +} diff --git a/src/land/ArcanistLandEngine.php b/src/land/ArcanistLandEngine.php deleted file mode 100644 index e81349f8..00000000 --- a/src/land/ArcanistLandEngine.php +++ /dev/null @@ -1,182 +0,0 @@ -workflow = $workflow; - return $this; - } - - final public function getWorkflow() { - return $this->workflow; - } - - final public function setRepositoryAPI( - ArcanistRepositoryAPI $repository_api) { - $this->repositoryAPI = $repository_api; - return $this; - } - - final public function getRepositoryAPI() { - return $this->repositoryAPI; - } - - final public function setShouldHold($should_hold) { - $this->shouldHold = $should_hold; - return $this; - } - - final public function getShouldHold() { - return $this->shouldHold; - } - - final public function setShouldKeep($should_keep) { - $this->shouldKeep = $should_keep; - return $this; - } - - final public function getShouldKeep() { - return $this->shouldKeep; - } - - final public function setShouldSquash($should_squash) { - $this->shouldSquash = $should_squash; - return $this; - } - - final public function getShouldSquash() { - return $this->shouldSquash; - } - - final public function setShouldPreview($should_preview) { - $this->shouldPreview = $should_preview; - return $this; - } - - final public function getShouldPreview() { - return $this->shouldPreview; - } - - final public function setTargetRemote($target_remote) { - $this->targetRemote = $target_remote; - return $this; - } - - final public function getTargetRemote() { - return $this->targetRemote; - } - - final public function setTargetOnto($target_onto) { - $this->targetOnto = $target_onto; - return $this; - } - - final public function getTargetOnto() { - return $this->targetOnto; - } - - final public function setSourceRef($source_ref) { - $this->sourceRef = $source_ref; - return $this; - } - - final public function getSourceRef() { - return $this->sourceRef; - } - - final public function setBuildMessageCallback($build_message_callback) { - $this->buildMessageCallback = $build_message_callback; - return $this; - } - - final public function getBuildMessageCallback() { - return $this->buildMessageCallback; - } - - final public function setCommitMessageFile($commit_message_file) { - $this->commitMessageFile = $commit_message_file; - return $this; - } - - final public function getCommitMessageFile() { - return $this->commitMessageFile; - } - - final public function setRemoteArgument($remote_argument) { - $this->remoteArgument = $remote_argument; - return $this; - } - - final public function getRemoteArgument() { - return $this->remoteArgument; - } - - final public function setOntoArgument($onto_argument) { - $this->ontoArgument = $onto_argument; - return $this; - } - - final public function getOntoArgument() { - return $this->ontoArgument; - } - - abstract public function parseArguments(); - abstract public function execute(); - - abstract protected function getLandingCommits(); - - protected function printLandingCommits() { - $logs = $this->getLandingCommits(); - - if (!$logs) { - throw new ArcanistUsageException( - pht( - 'There are no commits on "%s" which are not already present on '. - 'the target.', - $this->getSourceRef())); - } - - $list = id(new PhutilConsoleList()) - ->setWrap(false) - ->addItems($logs); - - id(new PhutilConsoleBlock()) - ->addParagraph( - pht( - 'These %s commit(s) will be landed:', - new PhutilNumber(count($logs)))) - ->addList($list) - ->draw(); - } - - protected function writeWarn($title, $message) { - return $this->getWorkflow()->writeWarn($title, $message); - } - - protected function writeInfo($title, $message) { - return $this->getWorkflow()->writeInfo($title, $message); - } - - protected function writeOkay($title, $message) { - return $this->getWorkflow()->writeOkay($title, $message); - } - - -} diff --git a/src/land/ArcanistLandSymbol.php b/src/land/ArcanistLandSymbol.php new file mode 100644 index 00000000..672f792e --- /dev/null +++ b/src/land/ArcanistLandSymbol.php @@ -0,0 +1,27 @@ +symbol = $symbol; + return $this; + } + + public function getSymbol() { + return $this->symbol; + } + + public function setCommit($commit) { + $this->commit = $commit; + return $this; + } + + public function getCommit() { + return $this->commit; + } + +} diff --git a/src/land/ArcanistLandTarget.php b/src/land/ArcanistLandTarget.php new file mode 100644 index 00000000..6a258e6b --- /dev/null +++ b/src/land/ArcanistLandTarget.php @@ -0,0 +1,41 @@ +remote = $remote; + return $this; + } + + public function getRemote() { + return $this->remote; + } + + public function setRef($ref) { + $this->ref = $ref; + return $this; + } + + public function getRef() { + return $this->ref; + } + + public function getLandTargetKey() { + return sprintf('%s/%s', $this->getRemote(), $this->getRef()); + } + + public function setLandTargetCommit($commit) { + $this->commit = $commit; + return $this; + } + + public function getLandTargetCommit() { + return $this->commit; + } + +} diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php new file mode 100644 index 00000000..48b2d95a --- /dev/null +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -0,0 +1,1362 @@ +isGitPerforce = $is_git_perforce; + return $this; + } + + private function getIsGitPerforce() { + return $this->isGitPerforce; + } + + protected function pruneBranches(array $sets) { + $old_commits = array(); + foreach ($sets as $set) { + $hash = last($set->getCommits())->getHash(); + $old_commits[] = $hash; + } + + $branch_map = $this->getBranchesForCommits( + $old_commits, + $is_contains = false); + + $api = $this->getRepositoryAPI(); + foreach ($branch_map as $branch_name => $branch_hash) { + $recovery_command = csprintf( + 'git checkout -b %s %s', + $branch_name, + $this->getDisplayHash($branch_hash)); + + echo tsprintf( + "%s\n", + pht('Cleaning up branch "%s"...', $branch_name)); + + echo tsprintf( + "%s\n", + pht('(Use `%s` if you want it back.)', $recovery_command)); + + $api->execxLocal('branch -D -- %s', $branch_name); + $this->deletedBranches[$branch_name] = true; + } + } + + private function getBranchesForCommits(array $hashes, $is_contains) { + $api = $this->getRepositoryAPI(); + + $format = '%(refname) %(objectname)'; + + $result = array(); + foreach ($hashes as $hash) { + if ($is_contains) { + $command = csprintf( + 'for-each-ref --contains %s --format %s --', + $hash, + $format); + } else { + $command = csprintf( + 'for-each-ref --points-at %s --format %s --', + $hash, + $format); + } + + list($foreach_lines) = $api->execxLocal('%C', $command); + $foreach_lines = phutil_split_lines($foreach_lines, false); + + foreach ($foreach_lines as $line) { + if (!strlen($line)) { + continue; + } + + $expect_parts = 2; + $parts = explode(' ', $line, $expect_parts); + if (count($parts) !== $expect_parts) { + throw new Exception( + pht( + 'Failed to explode line "%s".', + $line)); + } + + $ref_name = $parts[0]; + $ref_hash = $parts[1]; + + $matches = null; + $ok = preg_match('(^refs/heads/(.*)\z)', $ref_name, $matches); + if ($ok === false) { + throw new Exception( + pht( + 'Failed to match against branch pattern "%s".', + $line)); + } + + if (!$ok) { + continue; + } + + $result[$matches[1]] = $ref_hash; + } + } + + return $result; + } + + protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // This has no effect when we're executing a merge strategy. + if (!$this->isSquashStrategy()) { + return; + } + + $old_commit = last($set->getCommits())->getHash(); + $new_commit = $into_commit; + + $branch_map = $this->getBranchesForCommits( + array($old_commit), + $is_contains = true); + + $log = $this->getLogEngine(); + foreach ($branch_map as $branch_name => $branch_head) { + // If this branch just points at the old state, don't bother rebasing + // it. We'll update or delete it later. + if ($branch_head === $old_commit) { + continue; + } + + $log->writeStatus( + pht('CASCADE'), + pht( + 'Rebasing "%s" onto landed state...', + $branch_name)); + + try { + $api->execxLocal( + 'rebase --onto %s -- %s %s', + $new_commit, + $old_commit, + $branch_name); + } catch (CommandException $ex) { + // TODO: If we have a stashed state or are not running in incremental + // mode: abort the rebase, restore the local state, and pop the stash. + // Otherwise, drop the user out here. + throw $ex; + } + } + } + + private function fetchTarget(ArcanistLandTarget $target) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // NOTE: Although this output isn't hugely useful, we need to passthru + // instead of using a subprocess here because `git fetch` may prompt the + // user to enter a password if they're fetching over HTTP with basic + // authentication. See T10314. + + if ($this->getIsGitPerforce()) { + $log->writeStatus( + pht('P4 SYNC'), + pht( + 'Synchronizing "%s" from Perforce...', + $target->getRef())); + + $err = $api->execPassthru( + 'p4 sync --silent --branch %s --', + $target->getRemote().'/'.$target->getRef()); + + if ($err) { + throw new ArcanistUsageException( + pht( + 'Perforce sync failed! Fix the error and run "arc land" again.')); + } + + return $this->getLandTargetLocalCommit($target); + } + + $exists = $this->getLandTargetLocalExists($target); + if (!$exists) { + $log->writeWarning( + pht('TARGET'), + pht( + 'No local copy of ref "%s" in remote "%s" exists, attempting '. + 'fetch...', + $target->getRef(), + $target->getRemote())); + + $this->fetchLandTarget($target, $ignore_failure = true); + + $exists = $this->getLandTargetLocalExists($target); + if (!$exists) { + return null; + } + + $log->writeStatus( + pht('FETCHED'), + pht( + 'Fetched ref "%s" from remote "%s".', + $target->getRef(), + $target->getRemote())); + + return $this->getLandTargetLocalCommit($target); + } + + $log->writeStatus( + pht('FETCH'), + pht( + 'Fetching "%s" from remote "%s"...', + $target->getRef(), + $target->getRemote())); + + $this->fetchLandTarget($target, $ignore_failure = false); + + return $this->getLandTargetLocalCommit($target); + } + + private function updateWorkingCopy($into_commit) { + $api = $this->getRepositoryAPI(); + if ($into_commit === null) { + throw new Exception('TODO: Author a new empty state.'); + } else { + $api->execxLocal('checkout %s --', $into_commit); + } + } + + protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { + $api = $this->getRepositoryAPI(); + + $this->updateWorkingCopy($into_commit); + + $commits = $set->getCommits(); + $source_commit = last($commits)->getHash(); + + // NOTE: See T11435 for some history. See PHI1727 for a case where a user + // modified their working copy while running "arc land". This attempts to + // resist incorrectly detecting simultaneous working copy modifications + // as changes. + + list($changes) = $api->execxLocal( + 'diff --no-ext-diff HEAD..%s --', + $source_commit); + $changes = trim($changes); + if (!strlen($changes)) { + + // TODO: We could make a more significant effort to identify the + // human-readable symbol which led us to try to land this ref. + + throw new PhutilArgumentUsageException( + pht( + 'Merging local "%s" into "%s" produces an empty diff. '. + 'This usually means these changes have already landed.', + $this->getDisplayHash($source_commit), + $this->getDisplayHash($into_commit))); + } + + list($original_author, $original_date) = $this->getAuthorAndDate( + $source_commit); + + try { + if ($this->isSquashStrategy()) { + // NOTE: We're explicitly specifying "--ff" to override the presence + // of "merge.ff" options in user configuration. + + $api->execxLocal( + 'merge --no-stat --no-commit --ff --squash -- %s', + $source_commit); + } else { + $api->execxLocal( + 'merge --no-stat --no-commit --no-ff -- %s', + $source_commit); + } + } catch (CommandException $ex) { + $api->execManualLocal('merge --abort'); + $api->execManualLocal('reset --hard HEAD --'); + + throw new PhutilArgumentUsageException( + pht( + 'Local "%s" does not merge cleanly into "%s". Merge or rebase '. + 'local changes so they can merge cleanly.', + $source_commit, + $into_commit)); + } + + $revision_ref = $set->getRevisionRef(); + $commit_message = $revision_ref->getCommitMessage(); + + $future = $api->execFutureLocal( + 'commit --author %s --date %s -F - --', + $original_author, + $original_date); + $future->write($commit_message); + $future->resolvex(); + + list($stdout) = $api->execxLocal('rev-parse --verify %s', 'HEAD'); + $new_cursor = trim($stdout); + + if ($into_commit === null) { + if ($this->isSquashStrategy()) { + throw new Exception( + pht('TODO: Rewrite HEAD to have no parents.')); + } else { + throw new Exception( + pht('TODO: Rewrite HEAD to have only source as a parent.')); + } + } + + return $new_cursor; + } + + protected function pushChange($into_commit) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + if ($this->getIsGitPerforce()) { + + // TODO: Specifying "--onto" more than once is almost certainly an error + // in Perforce. + + $log->writeStatus( + pht('SUBMITTING'), + pht( + 'Submitting changes to "%s".', + $this->getOntoRemote())); + + $config_argv = array(); + + // Skip the "git p4 submit" interactive editor workflow. We expect + // the commit message that "arc land" has built to be satisfactory. + $config_argv[] = '-c'; + $config_argv[] = 'git-p4.skipSubmitEdit=true'; + + // Skip the "git p4 submit" confirmation prompt if the user does not edit + // the submit message. + $config_argv[] = '-c'; + $config_argv[] = 'git-p4.skipSubmitEditCheck=true'; + + $flags_argv = array(); + + // Disable implicit "git p4 rebase" as part of submit. We're allowing + // the implicit "git p4 sync" to go through since this puts us in a + // state which is generally similar to the state after "git push", with + // updated remotes. + + // We could do a manual "git p4 sync" with a more narrow "--branch" + // instead, but it's not clear that this is beneficial. + $flags_argv[] = '--disable-rebase'; + + // Detect moves and submit them to Perforce as move operations. + $flags_argv[] = '-M'; + + // If we run into a conflict, abort the operation. We expect users to + // fix conflicts and run "arc land" again. + $flags_argv[] = '--conflict=quit'; + + $err = $api->execPassthru( + '%LR p4 submit %LR --commit %R --', + $config_argv, + $flags_argv, + $into_commit); + + if ($err) { + throw new ArcanistUsageException( + pht( + 'Submit failed! Fix the error and run "arc land" again.')); + } + + return; + } + + $log->writeStatus( + pht('PUSHING'), + pht('Pushing changes to "%s".', $this->getOntoRemote())); + + $refspecs = array(); + foreach ($this->getOntoRefs() as $onto_ref) { + $refspecs[] = sprintf( + '%s:%s', + $into_commit, + $onto_ref); + } + + $err = $api->execPassthru( + 'push -- %s %Ls', + $this->getOntoRemote(), + $refspecs); + + if ($err) { + throw new ArcanistUsageException( + pht( + 'Push failed! Fix the error and run "arc land" again.')); + } + + // TODO + // if ($this->isGitSvn) { + // $err = phutil_passthru('git svn dcommit'); + // $cmd = 'git svn dcommit'; + + } + + protected function reconcileLocalState( + $into_commit, + ArcanistRepositoryLocalState $state) { + + $api = $this->getRepositoryAPI(); + $log = $this->getWorkflow()->getLogEngine(); + + // Try to put the user into the best final state we can. This is very + // complicated because users are incredibly creative and their local + // branches may, for example, have the same names as branches in the + // remote but no relationship to them. + + // First, we're going to try to update these local branches: + // + // - the branch we started on originally; and + // - the local upstreams of the branch we started on originally; and + // - the local branch with the same name as the "into" ref; and + // - the local branch with the same name as the "onto" ref. + // + // These branches may not all exist and may not all be unique. + // + // To be updated, these branches must: + // + // - exist; + // - have not been deleted; and + // - be connected to the remote we pushed into. + + $update_branches = array(); + + $local_ref = $state->getLocalRef(); + if ($local_ref !== null) { + $update_branches[] = $local_ref; + } + + $local_path = $state->getLocalPath(); + if ($local_path) { + foreach ($local_path->getLocalBranches() as $local_branch) { + $update_branches[] = $local_branch; + } + } + + if (!$this->getIntoEmpty() && !$this->getIntoLocal()) { + $update_branches[] = $this->getIntoRef(); + } + + foreach ($this->getOntoRefs() as $onto_ref) { + $update_branches[] = $onto_ref; + } + + $update_branches = array_fuse($update_branches); + + // Remove any branches we know we deleted. + foreach ($update_branches as $key => $update_branch) { + if (isset($this->deletedBranches[$update_branch])) { + unset($update_branches[$key]); + } + } + + // Now, remove any branches which don't actually exist. + foreach ($update_branches as $key => $update_branch) { + list($err) = $api->execManualLocal( + 'rev-parse --verify %s', + $update_branch); + if ($err) { + unset($update_branches[$key]); + } + } + + $is_perforce = $this->getIsGitPerforce(); + if ($is_perforce) { + // If we're in Perforce mode, we don't expect to have a meaningful + // path to the remote: the "p4" remote is not a real remote, and + // "git p4" commands do not configure branch upstreams to provide + // a path. + + // Additionally, we've already set the remote to the right state with an + // implicit "git p4 sync" during "git p4 submit", and "git pull" isn't a + // meaningful operation. + + // We're going to skip everything here and just switch to the most + // desirable branch (if we can find one), then reset the state (if that + // operation is safe). + + if (!$update_branches) { + $log->writeStatus( + pht('DETACHED HEAD'), + pht( + 'Unable to find any local branches to update, staying on '. + 'detached head.')); + $state->discardLocalState(); + return; + } + + $dst_branch = head($update_branches); + if (!$this->isAncestorOf($dst_branch, $into_commit)) { + $log->writeStatus( + pht('CHECKOUT'), + pht( + 'Local branch "%s" has unpublished changes, checking it out '. + 'but leaving them in place.', + $dst_branch)); + $do_reset = false; + } else { + $log->writeStatus( + pht('UPDATE'), + pht( + 'Switching to local branch "%s".', + $dst_branch)); + $do_reset = true; + } + + $api->execxLocal('checkout %s --', $dst_branch); + + if ($do_reset) { + $api->execxLocal('reset --hard %s --', $into_commit); + } + + $state->discardLocalState(); + return; + } + + $onto_refs = array_fuse($this->getOntoRefs()); + + $pull_branches = array(); + foreach ($update_branches as $update_branch) { + $update_path = $api->getPathToUpstream($update_branch); + + // Remove any branches which contain upstream cycles. + if ($update_path->getCycle()) { + $log->writeWarning( + pht('LOCAL CYCLE'), + pht( + 'Local branch "%s" tracks an upstream but following it leads to '. + 'a local cycle, ignoring branch.', + $update_branch)); + continue; + } + + // Remove any branches not connected to a remote. + if (!$update_path->isConnectedToRemote()) { + continue; + } + + // Remove any branches connected to a remote other than the remote + // we actually pushed to. + $remote_name = $update_path->getRemoteRemoteName(); + if ($remote_name !== $this->getOntoRemote()) { + continue; + } + + // Remove any branches not connected to a branch we pushed to. + $remote_branch = $update_path->getRemoteBranchName(); + if (!isset($onto_refs[$remote_branch])) { + continue; + } + + // This is the most-desirable path between some local branch and + // an impacted upstream. Select it and continue. + $pull_branches = $update_path->getLocalBranches(); + break; + } + + // When we update these branches later, we want to start with the branch + // closest to the upstream and work our way down. + $pull_branches = array_reverse($pull_branches); + $pull_branches = array_fuse($pull_branches); + + // If we started on a branch and it still exists but is not impacted + // by the changes we made to the remote (i.e., we aren't actually going + // to pull or update it if we continue), just switch back to it now. It's + // okay if this branch is completely unrelated to the changes we just + // landed. + + if ($local_ref !== null) { + if (isset($update_branches[$local_ref])) { + if (!isset($pull_branches[$local_ref])) { + + $log->writeStatus( + pht('RETURN'), + pht( + 'Returning to original branch "%s" in original state.', + $local_ref)); + + $state->restoreLocalState(); + return; + } + } + } + + // Otherwise, if we don't have any path from the upstream to any local + // branch, we don't want to switch to some unrelated branch which happens + // to have the same name as a branch we interacted with. Just stay where + // we ended up. + + $dst_branch = null; + if ($pull_branches) { + $dst_branch = null; + foreach ($pull_branches as $pull_branch) { + if (!$this->isAncestorOf($pull_branch, $into_commit)) { + + $log->writeStatus( + pht('LOCAL CHANGES'), + pht( + 'Local branch "%s" has unpublished changes, ending updates.', + $pull_branch)); + + break; + } + + $log->writeStatus( + pht('UPDATE'), + pht( + 'Updating local branch "%s"...', + $pull_branch)); + + $api->execxLocal( + 'branch -f %s %s --', + $pull_branch, + $into_commit); + + $dst_branch = $pull_branch; + } + } + + if ($dst_branch) { + $log->writeStatus( + pht('CHECKOUT'), + pht( + 'Checking out "%s".', + $dst_branch)); + + $api->execxLocal('checkout %s --', $dst_branch); + } else { + $log->writeStatus( + pht('DETACHED HEAD'), + pht( + 'Unable to find any local branches to update, staying on '. + 'detached head.')); + } + + $state->discardLocalState(); + } + + private function isAncestorOf($branch, $commit) { + $api = $this->getRepositoryAPI(); + + list($stdout) = $api->execxLocal( + 'merge-base %s %s', + $branch, + $commit); + $merge_base = trim($stdout); + + list($stdout) = $api->execxLocal( + 'rev-parse --verify %s', + $branch); + $branch_hash = trim($stdout); + + return ($merge_base === $branch_hash); + } + + private function getAuthorAndDate($commit) { + $api = $this->getRepositoryAPI(); + + list($info) = $api->execxLocal( + 'log -n1 --format=%s %s --', + '%aD%n%an%n%ae', + $commit); + + $info = trim($info); + list($date, $author, $email) = explode("\n", $info, 3); + + return array( + "$author <{$email}>", + $date, + ); + } + + private function didHoldChanges() { + $log = $this->getLogEngine(); + + if ($this->getIsGitPerforce()) { + $this->writeInfo( + pht('HOLD'), + pht( + 'Holding change locally, it has not been submitted.')); + + $push_command = csprintf( + '$ git p4 submit -M --commit %R --', + $this->mergedRef); + } else { + $log->writeStatus( + pht('HOLD'), + pht( + 'Holding change locally, it has not been pushed.')); + + $push_command = csprintf( + '$ git push -- %R %R:%R', + $this->getTargetRemote(), + $this->mergedRef, + $this->getTargetOnto()); + } + + $restore_command = csprintf( + '$ git checkout %R --', + $this->localRef); + + echo tsprintf( + "\n%s\n\n". + "%s\n\n". + " **%s**\n\n". + "%s\n\n". + " **%s**\n\n". + "%s\n", + pht( + 'This local working copy now contains the merged changes in a '. + 'detached state.'), + pht('You can push the changes manually with this command:'), + $push_command, + pht( + 'You can go back to how things were before you ran "arc land" with '. + 'this command:'), + $restore_command, + pht( + 'Local branches have not been changed, and are still in exactly the '. + 'same state as before.')); + } + + protected function resolveSymbols(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + + foreach ($symbols as $symbol) { + $raw_symbol = $symbol->getSymbol(); + + list($err, $stdout) = $api->execManualLocal( + 'rev-parse --verify %s', + $raw_symbol); + + if ($err) { + throw new PhutilArgumentUsageException( + pht( + 'Branch "%s" does not exist in the local working copy.', + $raw_symbol)); + } + + $commit = trim($stdout); + $symbol->setCommit($commit); + } + } + + protected function confirmOntoRefs(array $onto_refs) { + foreach ($onto_refs as $onto_ref) { + if (!strlen($onto_ref)) { + throw new PhutilArgumentUsageException( + pht( + 'Selected "onto" ref "%s" is invalid: the empty string is not '. + 'a valid ref.', + $onto_ref)); + } + } + } + + protected function selectOntoRefs(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $log = $this->getLogEngine(); + + $onto = $this->getOntoArguments(); + if ($onto) { + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Refs were selected with the "--onto" flag: %s.', + implode(', ', $onto))); + + return $onto; + } + + $onto = $this->getOntoFromConfiguration(); + if ($onto) { + $onto_key = $this->getOntoConfigurationKey(); + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Refs were selected by reading "%s" configuration: %s.', + $onto_key, + implode(', ', $onto))); + + return $onto; + } + + $api = $this->getRepositoryAPI(); + + $remote_onto = array(); + foreach ($symbols as $symbol) { + $raw_symbol = $symbol->getSymbol(); + $path = $api->getPathToUpstream($raw_symbol); + + if (!$path->getLength()) { + continue; + } + + $cycle = $path->getCycle(); + if ($cycle) { + $log->writeWarning( + pht('LOCAL CYCLE'), + pht( + 'Local branch "%s" tracks an upstream, but following it leads '. + 'to a local cycle; ignoring branch upstream.', + $raw_symbol)); + + $log->writeWarning( + pht('LOCAL CYCLE'), + implode(' -> ', $cycle)); + + continue; + } + + if (!$path->isConnectedToRemote()) { + $log->writeWarning( + pht('NO PATH TO REMOTE'), + pht( + 'Local branch "%s" tracks an upstream, but there is no path '. + 'to a remote; ignoring branch upstream.', + $raw_symbol)); + + continue; + } + + $onto = $path->getRemoteBranchName(); + + $remote_onto[$onto] = $onto; + } + + if (count($remote_onto) > 1) { + throw new PhutilArgumentUsageException( + pht( + 'The branches you are landing are connected to multiple different '. + 'remote branches via Git branch upstreams. Use "--onto" to select '. + 'the refs you want to push to.')); + } + + if ($remote_onto) { + $remote_onto = array_values($remote_onto); + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Landing onto target "%s", selected by following tracking branches '. + 'upstream to the closest remote branch.', + head($remote_onto))); + + return $remote_onto; + } + + $default_onto = 'master'; + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Landing onto target "%s", the default target under Git.', + $default_onto)); + + return array($default_onto); + } + + protected function selectOntoRemote(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $remote = $this->newOntoRemote($symbols); + + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + $is_pushable = $api->isPushableRemote($remote); + $is_perforce = $api->isPerforceRemote($remote); + + if (!$is_pushable && !$is_perforce) { + throw new PhutilArgumentUsageException( + pht( + 'No pushable remote "%s" exists. Use the "--onto-remote" flag to '. + 'choose a valid, pushable remote to land changes onto.', + $remote)); + } + + if ($is_perforce) { + $this->setIsGitPerforce(true); + + $log->writeWarning( + pht('P4 MODE'), + pht( + 'Operating in Git/Perforce mode after selecting a Perforce '. + 'remote.')); + + if (!$this->isSquashStrategy()) { + throw new PhutilArgumentUsageException( + pht( + 'Perforce mode does not support the "merge" land strategy. '. + 'Use the "squash" land strategy when landing to a Perforce '. + 'remote (you can use "--squash" to select this strategy).')); + } + } + + return $remote; + } + + private function newOntoRemote(array $onto_symbols) { + assert_instances_of($onto_symbols, 'ArcanistLandSymbol'); + $log = $this->getLogEngine(); + + $remote = $this->getOntoRemoteArgument(); + if ($remote !== null) { + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected with the "--onto-remote" flag.', + $remote)); + + return $remote; + } + + $remote = $this->getOntoRemoteFromConfiguration(); + if ($remote !== null) { + $remote_key = $this->getOntoRemoteConfigurationKey(); + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected by reading "%s" configuration.', + $remote, + $remote_key)); + + return $remote; + } + + $api = $this->getRepositoryAPI(); + + $upstream_remotes = array(); + foreach ($onto_symbols as $onto_symbol) { + $path = $api->getPathToUpstream($onto_symbol->getSymbol()); + + $remote = $path->getRemoteRemoteName(); + if ($remote !== null) { + $upstream_remotes[$remote][] = $onto_symbol; + } + } + + if (count($upstream_remotes) > 1) { + throw new PhutilArgumentUsageException( + pht( + 'The "onto" refs you have selected are connected to multiple '. + 'different remotes via Git branch upstreams. Use "--onto-remote" '. + 'to select a single remote.')); + } + + if ($upstream_remotes) { + $upstream_remote = head_key($upstream_remotes); + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected by following tracking branches '. + 'upstream to the closest remote.', + $remote)); + + return $upstream_remote; + } + + $perforce_remote = 'p4'; + if ($api->isPerforceRemote($remote)) { + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Peforce remote "%s" was selected because the existence of '. + 'this remote implies this working copy was synchronized '. + 'from a Perforce repository.', + $remote)); + + return $remote; + } + + $default_remote = 'origin'; + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Landing onto remote "%s", the default remote under Git.', + $default_remote)); + + return $default_remote; + } + + protected function selectIntoRemote() { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + if ($this->getIntoEmptyArgument()) { + $this->setIntoEmpty(true); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into empty state, selected with the "--into-empty" '. + 'flag.')); + + return; + } + + if ($this->getIntoLocalArgument()) { + $this->setIntoLocal(true); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into local state, selected with the "--into-local" '. + 'flag.')); + + return; + } + + $into = $this->getIntoRemoteArgument(); + if ($into !== null) { + + // TODO: We could allow users to pass a URI argument instead, but + // this also requires some updates to the fetch logic elsewhere. + + if (!$api->isFetchableRemote($into)) { + throw new PhutilArgumentUsageException( + pht( + 'Remote "%s", specified with "--into", is not a valid fetchable '. + 'remote.', + $into)); + } + + $this->setIntoRemote($into); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into remote "%s", selected with the "--into" flag.', + $into)); + + return; + } + + $onto = $this->getOntoRemote(); + $this->setIntoRemote($onto); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into remote "%s" by default, because this is the remote '. + 'the change is landing onto.', + $onto)); + } + + protected function selectIntoRef() { + $log = $this->getLogEngine(); + + if ($this->getIntoEmptyArgument()) { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into empty state, selected with the "--into-empty" '. + 'flag.')); + + return; + } + + $into = $this->getIntoArgument(); + if ($into !== null) { + $this->setIntoRef($into); + + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s", selected with the "--into" flag.', + $into)); + + return; + } + + $ontos = $this->getOntoRefs(); + $onto = head($ontos); + + $this->setIntoRef($onto); + if (count($ontos) > 1) { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s" by default, because this is the first '. + '"onto" target.', + $onto)); + } else { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s" by default, because this is the "onto" '. + 'target.', + $onto)); + } + } + + protected function selectIntoCommit() { + // Make sure that our "into" target is valid. + $log = $this->getLogEngine(); + + if ($this->getIntoEmpty()) { + // If we're running under "--into-empty", we don't have to do anything. + + $log->writeStatus( + pht('INTO COMMIT'), + pht('Preparing merge into the empty state.')); + + return null; + } + + if ($this->getIntoLocal()) { + // If we're running under "--into-local", just make sure that the + // target identifies some actual commit. + $api = $this->getRepositoryAPI(); + $local_ref = $this->getIntoRef(); + + list($err, $stdout) = $api->execManualLocal( + 'rev-parse --verify %s', + $local_ref); + + if ($err) { + throw new PhutilArgumentUsageException( + pht( + 'Local ref "%s" does not exist.', + $local_ref)); + } + + $into_commit = trim($stdout); + + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into local target "%s", at commit "%s".', + $local_ref, + $this->getDisplayHash($into_commit))); + + return $into_commit; + } + + $target = id(new ArcanistLandTarget()) + ->setRemote($this->getIntoRemote()) + ->setRef($this->getIntoRef()); + + $commit = $this->fetchTarget($target); + if ($commit !== null) { + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into "%s" from remote "%s", at commit "%s".', + $target->getRef(), + $target->getRemote(), + $this->getDisplayHash($commit))); + return $commit; + } + + // If we have no valid target and the user passed "--into" explicitly, + // treat this as an error. For example, "arc land --into Q --onto Q", + // where "Q" does not exist, is an error. + if ($this->getIntoArgument()) { + throw new PhutilArgumentUsageException( + pht( + 'Ref "%s" does not exist in remote "%s".', + $target->getRef(), + $target->getRemote())); + } + + // Otherwise, treat this as implying "--into-empty". For example, + // "arc land --onto Q", where "Q" does not exist, is equivalent to + // "arc land --into-empty --onto Q". + $this->setIntoEmpty(true); + + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into the empty state to create target "%s" '. + 'in remote "%s".', + $target->getRef(), + $target->getRemote())); + + return null; + } + + private function getLandTargetLocalCommit(ArcanistLandTarget $target) { + $commit = $this->resolveLandTargetLocalCommit($target); + + if ($commit === null) { + throw new Exception( + pht( + 'No ref "%s" exists in remote "%s".', + $target->getRef(), + $target->getRemote())); + } + + return $commit; + } + + private function getLandTargetLocalExists(ArcanistLandTarget $target) { + $commit = $this->resolveLandTargetLocalCommit($target); + return ($commit !== null); + } + + private function resolveLandTargetLocalCommit(ArcanistLandTarget $target) { + $target_key = $target->getLandTargetKey(); + + if (!array_key_exists($target_key, $this->landTargetCommitMap)) { + $full_ref = sprintf( + 'refs/remotes/%s/%s', + $target->getRemote(), + $target->getRef()); + + $api = $this->getRepositoryAPI(); + + list($err, $stdout) = $api->execManualLocal( + 'rev-parse --verify %s', + $full_ref); + + if ($err) { + $result = null; + } else { + $result = trim($stdout); + } + + $this->landTargetCommitMap[$target_key] = $result; + } + + return $this->landTargetCommitMap[$target_key]; + } + + private function fetchLandTarget( + ArcanistLandTarget $target, + $ignore_failure = false) { + $api = $this->getRepositoryAPI(); + + // TODO: Format this fetch nicely as a workflow command. + + $err = $api->execPassthru( + 'fetch --no-tags --quiet -- %s %s', + $target->getRemote(), + $target->getRef()); + if ($err && !$ignore_failure) { + throw new ArcanistUsageException( + pht( + 'Fetch of "%s" from remote "%s" failed! Fix the error and '. + 'run "arc land" again.', + $target->getRef(), + $target->getRemote())); + } + + // TODO: If the remote is a bare URI, we could read ".git/FETCH_HEAD" + // here and write the commit into the map. For now, settle for clearing + // the cache. + + // We could also fetch into some named "refs/arc-land-temporary" named + // ref, then read that. + + if (!$err) { + $target_key = $target->getLandTargetKey(); + unset($this->landTargetCommitMap[$target_key]); + } + } + + protected function selectCommits($into_commit, array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + + $commit_map = array(); + foreach ($symbols as $symbol) { + $symbol_commit = $symbol->getCommit(); + $format = '%H%x00%P%x00%s%x00'; + + if ($into_commit === null) { + list($commits) = $api->execxLocal( + 'log %s --format=%s', + $symbol_commit, + $format); + } else { + list($commits) = $api->execxLocal( + 'log %s --not %s --format=%s', + $symbol_commit, + $into_commit, + $format); + } + + $commits = phutil_split_lines($commits, false); + foreach ($commits as $line) { + if (!strlen($line)) { + continue; + } + + $parts = explode("\0", $line, 4); + if (count($parts) < 3) { + throw new Exception( + pht( + 'Unexpected output from "git log ...": %s', + $line)); + } + + $hash = $parts[0]; + if (!isset($commit_map[$hash])) { + $parents = $parts[1]; + $parents = trim($parents); + if (strlen($parents)) { + $parents = explode(' ', $parents); + } else { + $parents = array(); + } + + $summary = $parts[2]; + + $commit_map[$hash] = id(new ArcanistLandCommit()) + ->setHash($hash) + ->setParents($parents) + ->setSummary($summary); + } + + $commit = $commit_map[$hash]; + $commit->addSymbol($symbol); + } + } + + return $this->confirmCommits($into_commit, $symbols, $commit_map); + } + + protected function getDefaultSymbols() { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $branch = $api->getBranchName(); + if ($branch === null) { + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the current branch, "%s".', + $branch)); + + return array($branch); + } + + $commit = $api->getCurrentCommitRef(); + + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the current HEAD, "%s".', + $commit->getCommitHash())); + + return array($branch); + } + +} diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php new file mode 100644 index 00000000..051a21ae --- /dev/null +++ b/src/land/engine/ArcanistLandEngine.php @@ -0,0 +1,1430 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setOntoRemote($onto_remote) { + $this->ontoRemote = $onto_remote; + return $this; + } + + final public function getOntoRemote() { + return $this->ontoRemote; + } + + final public function setOntoRefs($onto_refs) { + $this->ontoRefs = $onto_refs; + return $this; + } + + final public function getOntoRefs() { + return $this->ontoRefs; + } + + final public function setIntoRemote($into_remote) { + $this->intoRemote = $into_remote; + return $this; + } + + final public function getIntoRemote() { + return $this->intoRemote; + } + + final public function setIntoRef($into_ref) { + $this->intoRef = $into_ref; + return $this; + } + + final public function getIntoRef() { + return $this->intoRef; + } + + final public function setIntoEmpty($into_empty) { + $this->intoEmpty = $into_empty; + return $this; + } + + final public function getIntoEmpty() { + return $this->intoEmpty; + } + + final public function setIntoLocal($into_local) { + $this->intoLocal = $into_local; + return $this; + } + + final public function getIntoLocal() { + return $this->intoLocal; + } + + final public function setWorkflow($workflow) { + $this->workflow = $workflow; + return $this; + } + + final public function getWorkflow() { + return $this->workflow; + } + + final public function setRepositoryAPI( + ArcanistRepositoryAPI $repository_api) { + $this->repositoryAPI = $repository_api; + return $this; + } + + final public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + final public function setLogEngine(ArcanistLogEngine $log_engine) { + $this->logEngine = $log_engine; + return $this; + } + + final public function getLogEngine() { + return $this->logEngine; + } + + final public function setShouldHold($should_hold) { + $this->shouldHold = $should_hold; + return $this; + } + + final public function getShouldHold() { + return $this->shouldHold; + } + + final public function setShouldKeep($should_keep) { + $this->shouldKeep = $should_keep; + return $this; + } + + final public function getShouldKeep() { + return $this->shouldKeep; + } + + final public function setStrategy($strategy) { + $this->strategy = $strategy; + return $this; + } + + final public function getStrategy() { + return $this->strategy; + } + + final public function setRevisionSymbol($revision_symbol) { + $this->revisionSymbol = $revision_symbol; + return $this; + } + + final public function getRevisionSymbol() { + return $this->revisionSymbol; + } + + final public function setRevisionSymbolRef( + ArcanistRevisionSymbolRef $revision_ref) { + $this->revisionSymbolRef = $revision_ref; + return $this; + } + + final public function getRevisionSymbolRef() { + return $this->revisionSymbolRef; + } + + final public function setShouldPreview($should_preview) { + $this->shouldPreview = $should_preview; + return $this; + } + + final public function getShouldPreview() { + return $this->shouldPreview; + } + + final public function setSourceRefs(array $source_refs) { + $this->sourceRefs = $source_refs; + return $this; + } + + final public function getSourceRefs() { + return $this->sourceRefs; + } + + final public function setOntoRemoteArgument($remote_argument) { + $this->ontoRemoteArgument = $remote_argument; + return $this; + } + + final public function getOntoRemoteArgument() { + return $this->ontoRemoteArgument; + } + + final public function setOntoArguments(array $onto_arguments) { + $this->ontoArguments = $onto_arguments; + return $this; + } + + final public function getOntoArguments() { + return $this->ontoArguments; + } + + final public function setIsIncremental($is_incremental) { + $this->isIncremental = $is_incremental; + return $this; + } + + final public function getIsIncremental() { + return $this->isIncremental; + } + + final public function setIntoEmptyArgument($into_empty_argument) { + $this->intoEmptyArgument = $into_empty_argument; + return $this; + } + + final public function getIntoEmptyArgument() { + return $this->intoEmptyArgument; + } + + final public function setIntoLocalArgument($into_local_argument) { + $this->intoLocalArgument = $into_local_argument; + return $this; + } + + final public function getIntoLocalArgument() { + return $this->intoLocalArgument; + } + + final public function setIntoRemoteArgument($into_remote_argument) { + $this->intoRemoteArgument = $into_remote_argument; + return $this; + } + + final public function getIntoRemoteArgument() { + return $this->intoRemoteArgument; + } + + final public function setIntoArgument($into_argument) { + $this->intoArgument = $into_argument; + return $this; + } + + final public function getIntoArgument() { + return $this->intoArgument; + } + + final protected function getOntoFromConfiguration() { + $config_key = $this->getOntoConfigurationKey(); + return $this->getWorkflow()->getConfig($config_key); + } + + final protected function getOntoConfigurationKey() { + return 'arc.land.onto'; + } + + final protected function getOntoRemoteFromConfiguration() { + $config_key = $this->getOntoRemoteConfigurationKey(); + return $this->getWorkflow()->getConfig($config_key); + } + + final protected function getOntoRemoteConfigurationKey() { + return 'arc.land.onto-remote'; + } + + final protected function confirmRevisions(array $sets) { + assert_instances_of($sets, 'ArcanistLandCommitSet'); + + $revision_refs = mpull($sets, 'getRevisionRef'); + $viewer = $this->getViewer(); + $viewer_phid = $viewer->getPHID(); + + $unauthored = array(); + foreach ($revision_refs as $revision_ref) { + $author_phid = $revision_ref->getAuthorPHID(); + if ($author_phid !== $viewer_phid) { + $unauthored[] = $revision_ref; + } + } + + if ($unauthored) { + $this->getWorkflow()->loadHardpoints( + $unauthored, + array( + ArcanistRevisionRef::HARDPOINT_AUTHORREF, + )); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('NOT REVISION AUTHOR'), + pht( + 'You are landing revisions which you ("%s") are not the author of:', + $viewer->getMonogram())); + + foreach ($unauthored as $revision_ref) { + $display_ref = $revision_ref->newDisplayRef(); + + $author_ref = $revision_ref->getAuthorRef(); + if ($author_ref) { + $display_ref->appendLine( + pht( + 'Author: %s', + $author_ref->getMonogram())); + } + + echo tsprintf('%s', $display_ref); + } + + echo tsprintf( + "\n%?\n", + pht( + 'Use "Commandeer" in the web interface to become the author of '. + 'a revision.')); + + $query = pht('Land revisions you are not the author of?'); + + $this->getWorkflow() + ->getPrompt('arc.land.unauthored') + ->setQuery($query) + ->execute(); + } + + $planned = array(); + $closed = array(); + $not_accepted = array(); + foreach ($revision_refs as $revision_ref) { + if ($revision_ref->isStatusChangesPlanned()) { + $planned[] = $revision_ref; + } else if ($revision_ref->isStatusClosed()) { + $closed[] = $revision_ref; + } else if (!$revision_ref->isStatusAccepted()) { + $not_accepted[] = $revision_ref; + } + } + + // See T10233. Previously, this prompt was bundled with the generic "not + // accepted" prompt, but users found it confusing and interpreted the + // prompt as a bug. + + if ($planned) { + $example_ref = head($planned); + + echo tsprintf( + "\n%!\n%W\n\n%W\n\n%W\n\n", + pht('%s REVISION(S) HAVE CHANGES PLANNED', phutil_count($planned)), + pht( + 'You are landing %s revision(s) which are currently in the state '. + '"%s", indicating that you expect to revise them before moving '. + 'forward.', + phutil_count($planned), + $example_ref->getStatusDisplayName()), + pht( + 'Normally, you should update these %s revision(s), submit them '. + 'for review, and wait for reviewers to accept them before '. + 'you continue. To resubmit a revision for review, either: '. + 'update the revision with revised changes; or use '. + '"Request Review" from the web interface.', + phutil_count($planned)), + pht( + 'These %s revision(s) have changes planned:', + phutil_count($planned))); + + foreach ($planned as $revision_ref) { + echo tsprintf('%s', $revision_ref->newDisplayRef()); + } + + $query = pht( + 'Land %s revision(s) with changes planned?', + phutil_count($planned)); + + $this->getWorkflow() + ->getPrompt('arc.land.changes-planned') + ->setQuery($query) + ->execute(); + } + + // See PHI1727. Previously, this prompt was bundled with the generic + // "not accepted" prompt, but at least one user found it confusing. + + if ($closed) { + $example_ref = head($closed); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('%s REVISION(S) ARE ALREADY CLOSED', phutil_count($closed)), + pht( + 'You are landing %s revision(s) which are already in the state '. + '"%s", indicating that they have previously landed:', + phutil_count($closed), + $example_ref->getStatusDisplayName())); + + foreach ($closed as $revision_ref) { + echo tsprintf('%s', $revision_ref->newDisplayRef()); + } + + $query = pht( + 'Land %s revision(s) that are already closed?', + phutil_count($closed)); + + $this->getWorkflow() + ->getPrompt('arc.land.closed') + ->setQuery($query) + ->execute(); + } + + if ($not_accepted) { + $example_ref = head($not_accepted); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('%s REVISION(S) ARE NOT ACCEPTED', phutil_count($not_accepted)), + pht( + 'You are landing %s revision(s) which are not in state "Accepted", '. + 'indicating that they have not been accepted by reviewers. '. + 'Normally, you should land changes only once they have been '. + 'accepted. These revisions are in the wrong state:', + phutil_count($not_accepted))); + + foreach ($not_accepted as $revision_ref) { + $display_ref = $revision_ref->newDisplayRef(); + $display_ref->appendLine( + pht( + 'Status: %s', + $revision_ref->getStatusDisplayName())); + echo tsprintf('%s', $display_ref); + } + + $query = pht( + 'Land %s revision(s) in the wrong state?', + phutil_count($not_accepted)); + + $this->getWorkflow() + ->getPrompt('arc.land.not-accepted') + ->setQuery($query) + ->execute(); + } + + $this->getWorkflow()->loadHardpoints( + $revision_refs, + array( + ArcanistRevisionRef::HARDPOINT_PARENTREVISIONREFS, + )); + + $open_parents = array(); + foreach ($revision_refs as $revision_phid => $revision_ref) { + $parent_refs = $revision_ref->getParentRevisionRefs(); + foreach ($parent_refs as $parent_ref) { + $parent_phid = $parent_ref->getPHID(); + + // If we're landing a parent revision in this operation, we don't need + // to complain that it hasn't been closed yet. + if (isset($revision_refs[$parent_phid])) { + continue; + } + + if ($parent_ref->isClosed()) { + continue; + } + + if (!isset($open_parents[$parent_phid])) { + $open_parents[$parent_phid] = array( + 'ref' => $parent_ref, + 'children' => array(), + ); + } + + $open_parents[$parent_phid]['children'][] = $revision_ref; + } + } + + if ($open_parents) { + echo tsprintf( + "\n%!\n%W\n\n", + pht('%s OPEN PARENT REVISION(S) ', phutil_count($open_parents)), + pht( + 'The changes you are landing depend on %s open parent revision(s). '. + 'Usually, you should land parent revisions before landing the '. + 'changes which depend on them. These parent revisions are open:', + phutil_count($open_parents))); + + foreach ($open_parents as $parent_phid => $spec) { + $parent_ref = $spec['ref']; + + $display_ref = $parent_ref->newDisplayRef(); + + $display_ref->appendLine( + pht( + 'Status: %s', + $parent_ref->getStatusDisplayName())); + + foreach ($spec['children'] as $child_ref) { + $display_ref->appendLine( + pht( + 'Parent of: %s %s', + $child_ref->getMonogram(), + $child_ref->getName())); + } + + echo tsprintf('%s', $display_ref); + } + + $query = pht( + 'Land changes that depend on %s open revision(s)?', + phutil_count($open_parents)); + + $this->getWorkflow() + ->getPrompt('arc.land.open-parents') + ->setQuery($query) + ->execute(); + } + + $this->confirmBuilds($revision_refs); + + // This is a reasonable place to bulk-load the commit messages, which + // we'll need soon. + + $this->getWorkflow()->loadHardpoints( + $revision_refs, + array( + ArcanistRevisionRef::HARDPOINT_COMMITMESSAGE, + )); + } + + private function confirmBuilds(array $revision_refs) { + assert_instances_of($revision_refs, 'ArcanistRevisionRef'); + + $this->getWorkflow()->loadHardpoints( + $revision_refs, + array( + ArcanistRevisionRef::HARDPOINT_BUILDABLEREF, + )); + + $buildable_refs = array(); + foreach ($revision_refs as $revision_ref) { + $ref = $revision_ref->getBuildableRef(); + if ($ref) { + $buildable_refs[] = $ref; + } + } + + $this->getWorkflow()->loadHardpoints( + $buildable_refs, + array( + ArcanistBuildableRef::HARDPOINT_BUILDREFS, + )); + + $build_refs = array(); + foreach ($buildable_refs as $buildable_ref) { + foreach ($buildable_ref->getBuildRefs() as $build_ref) { + $build_refs[] = $build_ref; + } + } + + $this->getWorkflow()->loadHardpoints( + $build_refs, + array( + ArcanistBuildRef::HARDPOINT_BUILDPLANREF, + )); + + $problem_builds = array(); + $has_failures = false; + $has_ongoing = false; + + $build_refs = msortv($build_refs, 'getStatusSortVector'); + foreach ($build_refs as $build_ref) { + $plan_ref = $build_ref->getBuildPlanRef(); + if (!$plan_ref) { + continue; + } + + $plan_behavior = $plan_ref->getBehavior('arc-land', 'always'); + $if_building = ($plan_behavior == 'building'); + $if_complete = ($plan_behavior == 'complete'); + $if_never = ($plan_behavior == 'never'); + + // If the build plan "Never" warns when landing, skip it. + if ($if_never) { + continue; + } + + // If the build plan warns when landing "If Complete" but the build is + // not complete, skip it. + if ($if_complete && !$build_ref->isComplete()) { + continue; + } + + // If the build plan warns when landing "If Building" but the build is + // complete, skip it. + if ($if_building && $build_ref->isComplete()) { + continue; + } + + // Ignore passing builds. + if ($build_ref->isPassed()) { + continue; + } + + if ($build_ref->isComplete()) { + $has_failures = true; + } else { + $has_ongoing = true; + } + + $problem_builds[] = $build_ref; + } + + if (!$problem_builds) { + return; + } + + $build_map = array(); + $failure_map = array(); + $buildable_map = mpull($buildable_refs, null, 'getPHID'); + $revision_map = mpull($revision_refs, null, 'getDiffPHID'); + foreach ($problem_builds as $build_ref) { + $buildable_phid = $build_ref->getBuildablePHID(); + $buildable_ref = $buildable_map[$buildable_phid]; + + $object_phid = $buildable_ref->getObjectPHID(); + $revision_ref = $revision_map[$object_phid]; + + $revision_phid = $revision_ref->getPHID(); + + if (!isset($build_map[$revision_phid])) { + $build_map[$revision_phid] = array( + 'revisionRef' => $revision_phid, + 'buildRefs' => array(), + ); + } + + $build_map[$revision_phid]['buildRefs'][] = $build_ref; + } + + $log = $this->getLogEngine(); + + if ($has_failures) { + if ($has_ongoing) { + $message = pht( + '%s revision(s) have build failures or ongoing builds:', + phutil_count($build_map)); + + $query = pht( + 'Land %s revision(s) anyway, despite ongoing and failed builds?', + phutil_count($build_map)); + + } else { + $message = pht( + '%s revision(s) have build failures:', + phutil_count($build_map)); + + $query = pht( + 'Land %s revision(s) anyway, despite failed builds?', + phutil_count($build_map)); + } + + echo tsprintf( + "%!\n%s\n\n", + pht('BUILD FAILURES'), + $message); + + $prompt_key = 'arc.land.failed-builds'; + } else if ($has_ongoing) { + echo tsprintf( + "%!\n%s\n\n", + pht('ONGOING BUILDS'), + pht( + '%s revision(s) have ongoing builds:', + phutil_count($build_map))); + + $query = pht( + 'Land %s revision(s) anyway, despite ongoing builds?', + phutil_count($build_map)); + + $prompt_key = 'arc.land.ongoing-builds'; + } + + echo tsprintf("\n"); + foreach ($build_map as $build_item) { + $revision_ref = $build_item['revisionRef']; + + echo tsprintf('%s', $revision_ref->newDisplayRef()); + + foreach ($build_item['buildRefs'] as $build_ref) { + echo tsprintf('%s', $build_ref->newDisplayRef()); + } + + echo tsprintf("\n"); + } + + echo tsprintf( + "\n%s\n\n", + pht('You can review build details here:')); + + // TODO: Only show buildables with problem builds. + + foreach ($buildable_refs as $buildable) { + $display_ref = $buildable->newDisplayRef(); + + // TODO: Include URI here. + + echo tsprintf('%s', $display_ref); + } + + $this->getWorkflow() + ->getPrompt($prompt_key) + ->setQuery($query) + ->execute(); + } + + final protected function confirmImplicitCommits(array $sets, array $symbols) { + assert_instances_of($sets, 'ArcanistLandCommitSet'); + assert_instances_of($symbols, 'ArcanistLandSymbol'); + + $implicit = array(); + foreach ($sets as $set) { + if ($set->hasImplicitCommits()) { + $implicit[] = $set; + } + } + + if (!$implicit) { + return; + } + + echo tsprintf( + "\n%!\n%W\n", + pht('IMPLICIT COMMITS'), + pht( + 'Some commits reachable from the specified sources (%s) are not '. + 'associated with revisions, and may not have been reviewed. These '. + 'commits will be landed as though they belong to the nearest '. + 'ancestor revision:', + $this->getDisplaySymbols($symbols))); + + foreach ($implicit as $set) { + $this->printCommitSet($set); + } + + $query = pht( + 'Continue with this mapping between commits and revisions?'); + + $this->getWorkflow() + ->getPrompt('arc.land.implicit') + ->setQuery($query) + ->execute(); + } + + final protected function getDisplaySymbols(array $symbols) { + $display = array(); + + foreach ($symbols as $symbol) { + $display[] = sprintf('"%s"', addcslashes($symbol->getSymbol(), '\\"')); + } + + return implode(', ', $display); + } + + final protected function printCommitSet(ArcanistLandCommitSet $set) { + $revision_ref = $set->getRevisionRef(); + + echo tsprintf( + "\n%s", + $revision_ref->newDisplayRef()); + + foreach ($set->getCommits() as $commit) { + $is_implicit = $commit->getIsImplicitCommit(); + + $display_hash = $this->getDisplayHash($commit->getHash()); + $display_summary = $commit->getDisplaySummary(); + + if ($is_implicit) { + echo tsprintf( + " %s %s\n", + $display_hash, + $display_summary); + } else { + echo tsprintf( + " %s %s\n", + $display_hash, + $display_summary); + } + } + } + + final protected function loadRevisionRefs(array $commit_map) { + assert_instances_of($commit_map, 'ArcanistLandCommit'); + $workflow = $this->getWorkflow(); + + $state_refs = array(); + foreach ($commit_map as $commit) { + $hash = $commit->getHash(); + + $commit_ref = id(new ArcanistCommitRef()) + ->setCommitHash($hash); + + $state_ref = id(new ArcanistWorkingCopyStateRef()) + ->setCommitRef($commit_ref); + + $state_refs[$hash] = $state_ref; + } + + $force_symbol_ref = $this->getRevisionSymbolRef(); + $force_ref = null; + if ($force_symbol_ref) { + $workflow->loadHardpoints( + $force_symbol_ref, + ArcanistSymbolRef::HARDPOINT_OBJECT); + + $force_ref = $force_symbol_ref->getObject(); + if (!$force_ref) { + throw new PhutilArgumentUsageException( + pht( + 'Symbol "%s" does not identify a valid revision.', + $force_symbol_ref->getSymbol())); + } + } + + $workflow->loadHardpoints( + $state_refs, + ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); + + foreach ($commit_map as $commit) { + $hash = $commit->getHash(); + $state_ref = $state_refs[$hash]; + + $revision_refs = $state_ref->getRevisionRefs(); + + // If we have several possible revisions but one of them matches the + // "--revision" argument, just select it. This is relatively safe and + // reasonable and doesn't need a warning. + + if ($force_ref) { + if (count($revision_refs) > 1) { + foreach ($revision_refs as $revision_ref) { + if ($revision_ref->getPHID() === $force_ref->getPHID()) { + $revision_refs = array($revision_ref); + break; + } + } + } + } + + if (count($revision_refs) === 1) { + $revision_ref = head($revision_refs); + $commit->setExplicitRevisionRef($revision_ref); + continue; + } + + if (!$revision_refs) { + continue; + } + + // TODO: If we have several refs but all but one are abandoned or closed + // or authored by someone else, we could guess what you mean. + + $symbols = $commit->getSymbols(); + $raw_symbols = mpull($symbols, 'getSymbol'); + $symbol_list = implode(', ', $raw_symbols); + $display_hash = $this->getDisplayHash($hash); + + // TODO: Include "use 'arc look --type commit abc' to figure out why" + // once that works? + + echo tsprintf( + "\n%!\n%W\n\n", + pht('AMBIGUOUS REVISION'), + pht( + 'The revision associated with commit "%s" (an ancestor of: %s) '. + 'is ambiguous. These %s revision(s) are associated with the commit:', + $display_hash, + implode(', ', $raw_symbols), + phutil_count($revision_refs))); + + foreach ($revision_refs as $revision_ref) { + echo tsprintf( + '%s', + $revision_ref->newDisplayRef()); + } + + echo tsprintf("\n"); + + throw new PhutilArgumentUsageException( + pht( + 'Revision for commit "%s" is ambiguous. Use "--revision" to force '. + 'selection of a particular revision.', + $display_hash)); + } + + // TODO: Some of the revisions we've identified may be mapped to an + // outdated set of commits. We should look in local branches for a better + // set of commits, and try to confirm that the state we're about to land + // is the current state in Differential. + + if ($force_ref) { + $phid_map = array(); + foreach ($commit_map as $commit) { + $explicit_ref = $commit->getExplicitRevisionRef(); + if ($explicit_ref) { + $revision_phid = $explicit_ref->getPHID(); + $phid_map[$revision_phid] = $revision_phid; + } + } + + $force_phid = $force_ref->getPHID(); + + // If none of the commits have a revision, forcing the revision is + // reasonable and we don't need to confirm it. + + // If some of the commits have a revision, but it's the same as the + // revision we're forcing, forcing the revision is also reasonable. + + // Discard the revision we're trying to force, then check if there's + // anything left. If some of the commits have a different revision, + // make sure the user is really doing what they expect. + + unset($phid_map[$force_phid]); + + if ($phid_map) { + // TODO: Make this more clear. + + throw new PhutilArgumentUsageException( + pht( + 'TODO: You are forcing a revision, but commits are associated '. + 'with some other revision. Are you REALLY sure you want to land '. + 'ALL these commits wiht a different unrelated revision???')); + } + + foreach ($commit_map as $commit) { + $commit->setExplicitRevisionRef($force_ref); + } + } + } + + final protected function getDisplayHash($hash) { + // TODO: This should be on the API object. + return substr($hash, 0, 12); + } + + final protected function confirmCommits( + $into_commit, + array $symbols, + array $commit_map) { + + $commit_count = count($commit_map); + + if (!$commit_count) { + $message = pht( + 'There are no commits reachable from the specified sources (%s) '. + 'which are not already present in the state you are merging '. + 'into ("%s"), so nothing can land.', + $this->getDisplaySymbols($symbols), + $this->getDisplayHash($into_commit)); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('NOTHING TO LAND'), + $message); + + throw new PhutilArgumentUsageException( + pht('There are no commits to land.')); + } + + // Reverse the commit list so that it's oldest-first, since this is the + // order we'll use to show revisions. + $commit_map = array_reverse($commit_map, true); + + $warn_limit = $this->getWorkflow()->getLargeWorkingSetLimit(); + $show_limit = 5; + if ($commit_count > $warn_limit) { + if ($into_commit === null) { + $message = pht( + 'There are %s commit(s) reachable from the specified sources (%s). '. + 'You are landing into the empty state, so all of these commits '. + 'will land:', + new PhutilNumber($commit_count), + $this->getDisplaySymbols($symbols)); + } else { + $message = pht( + 'There are %s commit(s) reachable from the specified sources (%s) '. + 'that are not present in the repository state you are merging '. + 'into ("%s"). All of these commits will land:', + new PhutilNumber($commit_count), + $this->getDisplaySymbols($symbols), + $this->getDisplayHash($into_commit)); + } + + echo tsprintf( + "\n%!\n%W\n", + pht('LARGE WORKING SET'), + $message); + + $display_commits = array_merge( + array_slice($commit_map, 0, $show_limit), + array(null), + array_slice($commit_map, -$show_limit)); + + echo tsprintf("\n"); + + foreach ($display_commits as $commit) { + if ($commit === null) { + echo tsprintf( + " %s\n", + pht( + '< ... %s more commits ... >', + new PhutilNumber($commit_count - ($show_limit * 2)))); + } else { + echo tsprintf( + " %s %s\n", + $this->getDisplayHash($commit->getHash()), + $commit->getDisplaySummary()); + } + } + + $query = pht( + 'Land %s commit(s)?', + new PhutilNumber($commit_count)); + + $this->getWorkflow() + ->getPrompt('arc.land.large-working-set') + ->setQuery($query) + ->execute(); + } + + // Build the commit objects into a tree. + foreach ($commit_map as $commit_hash => $commit) { + $parent_map = array(); + foreach ($commit->getParents() as $parent) { + if (isset($commit_map[$parent])) { + $parent_map[$parent] = $commit_map[$parent]; + } + } + $commit->setParentCommits($parent_map); + } + + // Identify the commits which are heads (have no children). + $child_map = array(); + foreach ($commit_map as $commit_hash => $commit) { + foreach ($commit->getParents() as $parent) { + $child_map[$parent][$commit_hash] = $commit; + } + } + + foreach ($commit_map as $commit_hash => $commit) { + if (isset($child_map[$commit_hash])) { + continue; + } + $commit->setIsHeadCommit(true); + } + + return $commit_map; + } + + public function execute() { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $this->validateArguments(); + + $raw_symbols = $this->getSourceRefs(); + if (!$raw_symbols) { + $raw_symbols = $this->getDefaultSymbols(); + } + + $symbols = array(); + foreach ($raw_symbols as $raw_symbol) { + $symbols[] = id(new ArcanistLandSymbol()) + ->setSymbol($raw_symbol); + } + + $this->resolveSymbols($symbols); + + $onto_remote = $this->selectOntoRemote($symbols); + $this->setOntoRemote($onto_remote); + + $onto_refs = $this->selectOntoRefs($symbols); + $this->confirmOntoRefs($onto_refs); + $this->setOntoRefs($onto_refs); + + $this->selectIntoRemote(); + $this->selectIntoRef(); + + $into_commit = $this->selectIntoCommit(); + $commit_map = $this->selectCommits($into_commit, $symbols); + + $this->loadRevisionRefs($commit_map); + + // TODO: It's possible we have a list of commits which includes disjoint + // groups of commits associated with the same revision, or groups of + // commits which do not form a range. We should test that here, since we + // can't land commit groups which are not a single contiguous range. + + $revision_groups = array(); + foreach ($commit_map as $commit_hash => $commit) { + $revision_ref = $commit->getRevisionRef(); + + if (!$revision_ref) { + echo tsprintf( + "\n%!\n%W\n\n", + pht('UNKNOWN REVISION'), + pht( + 'Unable to determine which revision is associated with commit '. + '"%s". Use "arc diff" to create or update a revision with this '. + 'commit, or "--revision" to force selection of a particular '. + 'revision.', + $this->getDisplayHash($commit_hash))); + + throw new PhutilArgumentUsageException( + pht( + 'Unable to determine revision for commit "%s".', + $this->getDisplayHash($commit_hash))); + } + + $revision_groups[$revision_ref->getPHID()][] = $commit; + } + + $commit_heads = array(); + foreach ($commit_map as $commit) { + if ($commit->getIsHeadCommit()) { + $commit_heads[] = $commit; + } + } + + $revision_order = array(); + foreach ($commit_heads as $head) { + foreach ($head->getAncestorRevisionPHIDs() as $phid) { + $revision_order[$phid] = true; + } + } + + $revision_groups = array_select_keys( + $revision_groups, + array_keys($revision_order)); + + $sets = array(); + foreach ($revision_groups as $revision_phid => $group) { + $revision_ref = head($group)->getRevisionRef(); + + $set = id(new ArcanistLandCommitSet()) + ->setRevisionRef($revision_ref) + ->setCommits($group); + + $sets[$revision_phid] = $set; + } + + if (!$this->getShouldPreview()) { + $this->confirmImplicitCommits($sets, $symbols); + } + + $log->writeStatus( + pht('LANDING'), + pht('These changes will land:')); + + foreach ($sets as $set) { + $this->printCommitSet($set); + } + + if ($this->getShouldPreview()) { + $log->writeStatus( + pht('PREVIEW'), + pht('Completed preview of land operation.')); + return; + } + + $query = pht('Land these changes?'); + $this->getWorkflow() + ->getPrompt('arc.land.confirm') + ->setQuery($query) + ->execute(); + + $this->confirmRevisions($sets); + + $workflow = $this->getWorkflow(); + + $is_incremental = $this->getIsIncremental(); + $is_hold = $this->getShouldHold(); + $is_keep = $this->getShouldKeep(); + + $local_state = $api->newLocalState() + ->setWorkflow($workflow) + ->saveLocalState(); + + $seen_into = array(); + try { + $last_key = last_key($sets); + + $need_cascade = array(); + $need_prune = array(); + + foreach ($sets as $set_key => $set) { + // Add these first, so we don't add them multiple times if we need + // to retry a push. + $need_prune[] = $set; + $need_cascade[] = $set; + + while (true) { + $into_commit = $this->executeMerge($set, $into_commit); + + if ($is_hold) { + $should_push = false; + } else if ($is_incremental) { + $should_push = true; + } else { + $is_last = ($set_key === $last_key); + $should_push = $is_last; + } + + if ($should_push) { + try { + $this->pushChange($into_commit); + } catch (Exception $ex) { + + // TODO: If the push fails, fetch and retry if the remote ref + // has moved ahead of us. + + if ($this->getIntoLocal()) { + $can_retry = false; + } else if ($this->getIntoEmpty()) { + $can_retry = false; + } else if ($this->getIntoRemote() !== $this->getOntoRemote()) { + $can_retry = false; + } else { + $can_retry = false; + } + + if ($can_retry) { + // New commit state here + $into_commit = '..'; + continue; + } + + throw $ex; + } + + if ($need_cascade) { + + // NOTE: We cascade each set we've pushed, but we're going to + // cascade them from most recent to least recent. This way, + // branches which descend from more recent changes only cascade + // once, directly in to the correct state. + + $need_cascade = array_reverse($need_cascade); + foreach ($need_cascade as $cascade_set) { + $this->cascadeState($set, $into_commit); + } + $need_cascade = array(); + } + + if (!$is_keep) { + $this->pruneBranches($need_prune); + $need_prune = array(); + } + } + + break; + } + } + + if ($is_hold) { + $this->didHoldChanges(); + $this->discardLocalState(); + } else { + $this->reconcileLocalState($into_commit, $local_state); + } + + // TODO: Restore this. + // $this->getWorkflow()->askForRepositoryUpdate(); + + $log->writeSuccess( + pht('DONE'), + pht('Landed changes.')); + } catch (Exception $ex) { + $local_state->restoreLocalState(); + throw $ex; + } catch (Throwable $ex) { + $local_state->restoreLocalState(); + throw $ex; + } + } + + + protected function validateArguments() { + $log = $this->getLogEngine(); + + $into_local = $this->getIntoLocalArgument(); + $into_empty = $this->getIntoEmptyArgument(); + $into_remote = $this->getIntoRemoteArgument(); + + $into_count = 0; + if ($into_remote !== null) { + $into_count++; + } + + if ($into_local) { + $into_count++; + } + + if ($into_empty) { + $into_count++; + } + + if ($into_count > 1) { + throw new PhutilArgumentUsageException( + pht( + 'Arguments "--into-local", "--into-remote", and "--into-empty" '. + 'are mutually exclusive.')); + } + + $into = $this->getIntoArgument(); + if ($into && ($into_empty !== null)) { + throw new PhutilArgumentUsageException( + pht( + 'Arguments "--into" and "--into-empty" are mutually exclusive.')); + } + + $strategy = $this->selectMergeStrategy(); + $this->setStrategy($strategy); + + // Build the symbol ref here (which validates the format of the symbol), + // but don't load the object until later on when we're sure we actually + // need it, since loading it requires a relatively expensive Conduit call. + $revision_symbol = $this->getRevisionSymbol(); + if ($revision_symbol) { + $symbol_ref = id(new ArcanistRevisionSymbolRef()) + ->setSymbol($revision_symbol); + $this->setRevisionSymbolRef($symbol_ref); + } + + // NOTE: When a user provides: "--hold" or "--preview"; and "--incremental" + // or various combinations of remote flags, the flags affecting push/remote + // behavior have no effect. + + // These combinations are allowed to support adding "--preview" or "--hold" + // to any command to run the same command with fewer side effects. + } + + abstract protected function getDefaultSymbols(); + abstract protected function resolveSymbols(array $symbols); + abstract protected function selectOntoRemote(array $symbols); + abstract protected function selectOntoRefs(array $symbols); + abstract protected function confirmOntoRefs(array $onto_refs); + abstract protected function selectIntoRemote(); + abstract protected function selectIntoRef(); + abstract protected function selectIntoCommit(); + abstract protected function selectCommits($into_commit, array $symbols); + abstract protected function executeMerge( + ArcanistLandCommitSet $set, + $into_commit); + abstract protected function pushChange($into_commit); + abstract protected function cascadeState( + ArcanistLandCommitSet $set, + $into_commit); + + protected function isSquashStrategy() { + return ($this->getStrategy() === 'squash'); + } + + abstract protected function pruneBranches(array $sets); + + abstract protected function reconcileLocalState( + $into_commit, + ArcanistRepositoryLocalState $state); + + private function selectMergeStrategy() { + $log = $this->getLogEngine(); + + $supported_strategies = array( + 'merge', + 'squash', + ); + $supported_strategies = array_fuse($supported_strategies); + $strategy_list = implode(', ', $supported_strategies); + + $strategy = $this->getStrategyArgument(); + if ($strategy !== null) { + if (!isset($supported_strategies[$strategy])) { + throw new PhutilArgumentUsageException( + pht( + 'Merge strategy "%s" specified with "--strategy" is unknown. '. + 'Supported merge strategies are: %s.', + $strategy, + $strategy_list)); + } + + $log->writeStatus( + pht('STRATEGY'), + pht( + 'Merging with "%s" strategy, selected with "--strategy".', + $strategy)); + + return $strategy; + } + + $strategy_key = 'arc.land.strategy'; + $strategy = $this->getWorkflow()->getConfig($strategy_key); + if ($strategy !== null) { + if (!isset($supported_strategies[$strategy])) { + throw new PhutilArgumentUsageException( + pht( + 'Merge strategy "%s" specified in "%s" configuration is '. + 'unknown. Supported merge strategies are: %s.', + $strategy, + $strategy_list)); + } + + $log->writeStatus( + pht('STRATEGY'), + pht( + 'Merging with "%s" strategy, configured with "%s".', + $strategy, + $strategy_key)); + + return $strategy; + } + + $strategy = 'squash'; + + $log->writeStatus( + pht('STRATEGY'), + pht( + 'Merging with "%s" strategy, the default strategy.', + $strategy)); + + return $strategy; + } + +} diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php new file mode 100644 index 00000000..cc48de66 --- /dev/null +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -0,0 +1,603 @@ +getRepositoryAPI(); + $log = $this->getLogEngine(); + + $bookmark = $api->getActiveBookmark(); + if ($bookmark !== null) { + + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the active bookmark, "%s".', + $bookmark)); + + return array($bookmark); + } + + $branch = $api->getBranchName(); + if ($branch !== null) { + + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the current branch, "%s".', + $branch)); + + return array($branch); + } + + throw new Exception(pht('TODO: Operate on raw revision.')); + } + + protected function resolveSymbols(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + + foreach ($symbols as $symbol) { + $raw_symbol = $symbol->getSymbol(); + + if ($api->isBookmark($raw_symbol)) { + $hash = $api->getBookmarkCommitHash($raw_symbol); + $symbol->setCommit($hash); + + // TODO: Set that this is a bookmark? + + continue; + } + + if ($api->isBranch($raw_symbol)) { + $hash = $api->getBranchCommitHash($raw_symbol); + $symbol->setCommit($hash); + + // TODO: Set that this is a branch? + + continue; + } + + throw new PhutilArgumentUsageException( + pht( + 'Symbol "%s" is not a bookmark or branch name.', + $raw_symbol)); + } + } + + protected function selectOntoRemote(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $remote = $this->newOntoRemote($symbols); + + // TODO: Verify this remote actually exists. + + return $remote; + } + + private function newOntoRemote(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $remote = $this->getOntoRemoteArgument(); + if ($remote !== null) { + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected with the "--onto-remote" flag.', + $remote)); + + return $remote; + } + + $remote = $this->getOntoRemoteFromConfiguration(); + if ($remote !== null) { + $remote_key = $this->getOntoRemoteConfigurationKey(); + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected by reading "%s" configuration.', + $remote, + $remote_key)); + + return $remote; + } + + $api = $this->getRepositoryAPI(); + + $default_remote = 'default'; + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Landing onto remote "%s", the default remote under Mercurial.', + $default_remote)); + + return $default_remote; + } + + protected function selectOntoRefs(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $log = $this->getLogEngine(); + + $onto = $this->getOntoArguments(); + if ($onto) { + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Refs were selected with the "--onto" flag: %s.', + implode(', ', $onto))); + + return $onto; + } + + $onto = $this->getOntoFromConfiguration(); + if ($onto) { + $onto_key = $this->getOntoConfigurationKey(); + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Refs were selected by reading "%s" configuration: %s.', + $onto_key, + implode(', ', $onto))); + + return $onto; + } + + $api = $this->getRepositoryAPI(); + + $default_onto = 'default'; + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Landing onto target "%s", the default target under Mercurial.', + $default_onto)); + + return array($default_onto); + } + + protected function confirmOntoRefs(array $onto_refs) { + foreach ($onto_refs as $onto_ref) { + if (!strlen($onto_ref)) { + throw new PhutilArgumentUsageException( + pht( + 'Selected "onto" ref "%s" is invalid: the empty string is not '. + 'a valid ref.', + $onto_ref)); + } + } + } + + protected function selectIntoRemote() { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + if ($this->getIntoEmptyArgument()) { + $this->setIntoEmpty(true); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into empty state, selected with the "--into-empty" '. + 'flag.')); + + return; + } + + if ($this->getIntoLocalArgument()) { + $this->setIntoLocal(true); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into local state, selected with the "--into-local" '. + 'flag.')); + + return; + } + + $into = $this->getIntoRemoteArgument(); + if ($into !== null) { + + // TODO: Verify that this is a valid path. + // TODO: Allow a raw URI? + + $this->setIntoRemote($into); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into remote "%s", selected with the "--into" flag.', + $into)); + + return; + } + + $onto = $this->getOntoRemote(); + $this->setIntoRemote($onto); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into remote "%s" by default, because this is the remote '. + 'the change is landing onto.', + $onto)); + } + + protected function selectIntoRef() { + $log = $this->getLogEngine(); + + if ($this->getIntoEmptyArgument()) { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into empty state, selected with the "--into-empty" '. + 'flag.')); + + return; + } + + $into = $this->getIntoArgument(); + if ($into !== null) { + $this->setIntoRef($into); + + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s", selected with the "--into" flag.', + $into)); + + return; + } + + $ontos = $this->getOntoRefs(); + $onto = head($ontos); + + $this->setIntoRef($onto); + if (count($ontos) > 1) { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s" by default, because this is the first '. + '"onto" target.', + $onto)); + } else { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s" by default, because this is the "onto" '. + 'target.', + $onto)); + } + } + + protected function selectIntoCommit() { + // Make sure that our "into" target is valid. + $log = $this->getLogEngine(); + + if ($this->getIntoEmpty()) { + // If we're running under "--into-empty", we don't have to do anything. + + $log->writeStatus( + pht('INTO COMMIT'), + pht('Preparing merge into the empty state.')); + + return null; + } + + if ($this->getIntoLocal()) { + // If we're running under "--into-local", just make sure that the + // target identifies some actual commit. + $api = $this->getRepositoryAPI(); + $local_ref = $this->getIntoRef(); + + // TODO: This error handling could probably be cleaner. + + $into_commit = $api->getCanonicalRevisionName($local_ref); + + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into local target "%s", at commit "%s".', + $local_ref, + $this->getDisplayHash($into_commit))); + + return $into_commit; + } + + $target = id(new ArcanistLandTarget()) + ->setRemote($this->getIntoRemote()) + ->setRef($this->getIntoRef()); + + $commit = $this->fetchTarget($target); + if ($commit !== null) { + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into "%s" from remote "%s", at commit "%s".', + $target->getRef(), + $target->getRemote(), + $this->getDisplayHash($commit))); + return $commit; + } + + // If we have no valid target and the user passed "--into" explicitly, + // treat this as an error. For example, "arc land --into Q --onto Q", + // where "Q" does not exist, is an error. + if ($this->getIntoArgument()) { + throw new PhutilArgumentUsageException( + pht( + 'Ref "%s" does not exist in remote "%s".', + $target->getRef(), + $target->getRemote())); + } + + // Otherwise, treat this as implying "--into-empty". For example, + // "arc land --onto Q", where "Q" does not exist, is equivalent to + // "arc land --into-empty --onto Q". + $this->setIntoEmpty(true); + + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into the empty state to create target "%s" '. + 'in remote "%s".', + $target->getRef(), + $target->getRemote())); + + return null; + } + + private function fetchTarget(ArcanistLandTarget $target) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // TODO: Support bookmarks. + // TODO: Deal with bookmark save/restore behavior. + // TODO: Format this nicely with passthru. + // TODO: Raise a good error message when the ref does not exist. + + $api->execPassthru( + 'pull -b %s -- %s', + $target->getRef(), + $target->getRemote()); + + // TODO: Deal with multiple branch heads. + + list($stdout) = $api->execxLocal( + 'log --rev %s --template %s --', + hgsprintf( + 'last(ancestors(%s) and !outgoing(%s))', + $target->getRef(), + $target->getRemote()), + '{node}'); + + return trim($stdout); + } + + protected function selectCommits($into_commit, array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + + $commit_map = array(); + foreach ($symbols as $symbol) { + $symbol_commit = $symbol->getCommit(); + $template = '{node}-{parents}-'; + + if ($into_commit === null) { + list($commits) = $api->execxLocal( + 'log --rev %s --template %s --', + hgsprintf('reverse(ancestors(%s))', $into_commit), + $template); + } else { + list($commits) = $api->execxLocal( + 'log --rev %s --template %s --', + hgsprintf( + 'reverse(ancestors(%s) - ancestors(%s))', + $symbol_commit, + $into_commit), + $template); + } + + $commits = phutil_split_lines($commits, false); + foreach ($commits as $line) { + if (!strlen($line)) { + continue; + } + + $parts = explode('-', $line, 3); + if (count($parts) < 3) { + throw new Exception( + pht( + 'Unexpected output from "hg log ...": %s', + $line)); + } + + $hash = $parts[0]; + if (!isset($commit_map[$hash])) { + $parents = $parts[1]; + $parents = trim($parents); + if (strlen($parents)) { + $parents = explode(' ', $parents); + } else { + $parents = array(); + } + + $summary = $parts[2]; + + $commit_map[$hash] = id(new ArcanistLandCommit()) + ->setHash($hash) + ->setParents($parents) + ->setSummary($summary); + } + + $commit = $commit_map[$hash]; + $commit->addSymbol($symbol); + } + } + + return $this->confirmCommits($into_commit, $symbols, $commit_map); + } + + protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { + $api = $this->getRepositoryAPI(); + + if ($this->getStrategy() !== 'squash') { + throw new Exception(pht('TODO: Support merge strategies')); + } + + // TODO: Add a Mercurial version check requiring 2.1.1 or newer. + + $api->execxLocal( + 'update --rev %s', + hgsprintf('%s', $into_commit)); + + $commits = $set->getCommits(); + + $min_commit = last($commits)->getHash(); + $max_commit = head($commits)->getHash(); + + $revision_ref = $set->getRevisionRef(); + $commit_message = $revision_ref->getCommitMessage(); + + try { + $argv = array(); + $argv[] = '--dest'; + $argv[] = hgsprintf('%s', $into_commit); + + $argv[] = '--rev'; + $argv[] = hgsprintf('%s..%s', $min_commit, $max_commit); + + $argv[] = '--logfile'; + $argv[] = '-'; + + $argv[] = '--keep'; + $argv[] = '--collapse'; + + $future = $api->execFutureLocal('rebase %Ls', $argv); + $future->write($commit_message); + $future->resolvex(); + + } catch (CommandException $ex) { + // TODO + // $api->execManualLocal('rebase --abort'); + throw $ex; + } + + list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}'); + $new_cursor = trim($stdout); + + return $new_cursor; + } + + protected function pushChange($into_commit) { + $api = $this->getRepositoryAPI(); + + // TODO: This does not respect "--into" or "--onto" properly. + + $api->execxLocal( + 'push --rev %s -- %s', + $into_commit, + $this->getOntoRemote()); + } + + protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // This has no effect when we're executing a merge strategy. + if (!$this->isSquashStrategy()) { + return; + } + + $old_commit = last($set->getCommits())->getHash(); + $new_commit = $into_commit; + + list($output) = $api->execxLocal( + 'log --rev %s --template %s', + hgsprintf('children(%s)', $old_commit), + '{node}\n'); + $child_hashes = phutil_split_lines($output, false); + + foreach ($child_hashes as $child_hash) { + if (!strlen($child_hash)) { + continue; + } + + // TODO: If the only heads which are descendants of this child will + // be deleted, we can skip this rebase? + + try { + $api->execxLocal( + 'rebase --source %s --dest %s --keep --keepbranches', + $child_hash, + $new_commit); + } catch (CommandException $ex) { + // TODO: Recover state. + throw $ex; + } + } + } + + + protected function pruneBranches(array $sets) { + assert_instances_of($sets, 'ArcanistLandCommitSet'); + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // This has no effect when we're executing a merge strategy. + if (!$this->isSquashStrategy()) { + return; + } + + $strip = array(); + + // We've rebased all descendants already, so we can safely delete all + // of these commits. + + $sets = array_reverse($sets); + foreach ($sets as $set) { + $commits = $set->getCommits(); + + $min_commit = head($commits)->getHash(); + $max_commit = last($commits)->getHash(); + + $strip[] = hgsprintf('%s::%s', $min_commit, $max_commit); + } + + $rev_set = '('.implode(') or (', $strip).')'; + + // See PHI45. If we have "hg evolve", get rid of old commits using + // "hg prune" instead of "hg strip". + + // If we "hg strip" a commit which has an obsolete predecessor, it + // removes the obsolescence marker and revives the predecessor. This is + // not desirable: we want to destroy all predecessors of these commits. + + try { + $api->execxLocal( + '--config extensions.evolve= prune --rev %s', + $rev_set); + } catch (CommandException $ex) { + $api->execxLocal( + '--config extensions.strip= strip --rev %s', + $rev_set); + } + } + + protected function reconcileLocalState( + $into_commit, + ArcanistRepositoryLocalState $state) { + + // TODO: For now, just leave users wherever they ended up. + + $state->discardLocalState(); + } + +} diff --git a/src/query/ArcanistMercurialCommitMessageHardpointQuery.php b/src/query/ArcanistMercurialCommitMessageHardpointQuery.php new file mode 100644 index 00000000..45dc8ee3 --- /dev/null +++ b/src/query/ArcanistMercurialCommitMessageHardpointQuery.php @@ -0,0 +1,36 @@ +getRepositoryAPI(); + + $hashes = mpull($refs, 'getCommitHash'); + $unique_hashes = array_fuse($hashes); + + // TODO: Batch this properly and make it future oriented. + + $messages = array(); + foreach ($unique_hashes as $unique_hash) { + $messages[$unique_hash] = $api->getCommitMessage($unique_hash); + } + + foreach ($hashes as $ref_key => $hash) { + $hashes[$ref_key] = $messages[$hash]; + } + + yield $this->yieldMap($hashes); + } + +} diff --git a/src/query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php b/src/query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php new file mode 100644 index 00000000..c6c03cf3 --- /dev/null +++ b/src/query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php @@ -0,0 +1,76 @@ +yieldRequests( + $refs, + array( + ArcanistWorkingCopyStateRef::HARDPOINT_COMMITREF, + )); + + // TODO: This has a lot in common with the Git query in the same role. + + $hashes = array(); + $map = array(); + foreach ($refs as $ref_key => $ref) { + $commit = $ref->getCommitRef(); + + $commit_hashes = array(); + + $commit_hashes[] = array( + 'hgcm', + $commit->getCommitHash(), + ); + + foreach ($commit_hashes as $hash) { + $hashes[] = $hash; + $hash_key = $this->getHashKey($hash); + $map[$hash_key][$ref_key] = $ref; + } + } + + $results = array_fill_keys(array_keys($refs), array()); + if ($hashes) { + $revisions = (yield $this->yieldConduit( + 'differential.query', + array( + 'commitHashes' => $hashes, + ))); + + foreach ($revisions as $dict) { + $revision_hashes = idx($dict, 'hashes'); + if (!$revision_hashes) { + continue; + } + + $revision_ref = ArcanistRevisionRef::newFromConduitQuery($dict); + foreach ($revision_hashes as $revision_hash) { + $hash_key = $this->getHashKey($revision_hash); + $state_refs = idx($map, $hash_key, array()); + foreach ($state_refs as $ref_key => $state_ref) { + $results[$ref_key][] = $revision_ref; + } + } + } + } + + yield $this->yieldMap($results); + } + + private function getHashKey(array $hash) { + return $hash[0].':'.$hash[1]; + } + +} diff --git a/src/query/ArcanistWorkflowMercurialHardpointQuery.php b/src/query/ArcanistWorkflowMercurialHardpointQuery.php new file mode 100644 index 00000000..b1932ae6 --- /dev/null +++ b/src/query/ArcanistWorkflowMercurialHardpointQuery.php @@ -0,0 +1,11 @@ +getRepositoryAPI(); + return ($api instanceof ArcanistMercurialAPI); + } + +} diff --git a/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php b/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php index df23f01a..c0ff6e72 100644 --- a/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php +++ b/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php @@ -1,60 +1,60 @@ $revision_ref) { $diff_phid = $revision_ref->getDiffPHID(); if ($diff_phid) { $diff_map[$key] = $diff_phid; } } if (!$diff_map) { yield $this->yieldValue($refs, null); } $buildables = (yield $this->yieldConduitSearch( 'harbormaster.buildable.search', array( - 'objectPHIDs' => $diff_map, + 'objectPHIDs' => array_values($diff_map), 'manual' => false, ))); $buildable_refs = array(); foreach ($buildables as $buildable) { $buildable_ref = ArcanistBuildableRef::newFromConduit($buildable); $object_phid = $buildable_ref->getObjectPHID(); $buildable_refs[$object_phid] = $buildable_ref; } $results = array_fill_keys(array_keys($refs), null); foreach ($refs as $key => $revision_ref) { if (!isset($diff_map[$key])) { continue; } $diff_phid = $diff_map[$key]; if (!isset($buildable_refs[$diff_phid])) { continue; } $results[$key] = $buildable_refs[$diff_phid]; } yield $this->yieldMap($results); } } diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index d8299f1f..2a07f9dd 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1,1742 +1,1756 @@ setCWD($this->getPath()); return $future; } public function execPassthru($pattern /* , ... */) { $args = func_get_args(); static $git = null; if ($git === null) { if (phutil_is_windows()) { // NOTE: On Windows, phutil_passthru() uses 'bypass_shell' because // everything goes to hell if we don't. We must provide an absolute // path to Git for this to work properly. $git = Filesystem::resolveBinary('git'); $git = csprintf('%s', $git); } else { $git = 'git'; } } $args[0] = $git.' '.$args[0]; return call_user_func_array('phutil_passthru', $args); } public function getSourceControlSystemName() { return 'git'; } public function getGitVersion() { static $version = null; if ($version === null) { list($stdout) = $this->execxLocal('--version'); $version = rtrim(str_replace('git version ', '', $stdout)); } return $version; } public function getMetadataPath() { static $path = null; if ($path === null) { list($stdout) = $this->execxLocal('rev-parse --git-dir'); $path = rtrim($stdout, "\n"); // the output of git rev-parse --git-dir is an absolute path, unless // the cwd is the root of the repository, in which case it uses the // relative path of .git. If we get this relative path, turn it into // an absolute path. if ($path === '.git') { $path = $this->getPath('.git'); } } return $path; } public function getHasCommits() { return !$this->repositoryHasNoCommits; } /** * Tests if a child commit is descendant of a parent commit. * If child and parent are the same, it returns false. * @param Child commit SHA. * @param Parent commit SHA. * @return bool True if the child is a descendant of the parent. */ private function isDescendant($child, $parent) { list($common_ancestor) = $this->execxLocal( 'merge-base %s %s', $child, $parent); $common_ancestor = trim($common_ancestor); return ($common_ancestor == $parent) && ($common_ancestor != $child); } public function getLocalCommitInformation() { if ($this->repositoryHasNoCommits) { // Zero commits. throw new Exception( pht( "You can't get local commit information for a repository with no ". "commits.")); } else if ($this->getBaseCommit() == self::GIT_MAGIC_ROOT_COMMIT) { // One commit. $against = 'HEAD'; } else { // 2..N commits. We include commits reachable from HEAD which are // not reachable from the base commit; this is consistent with user // expectations even though it is not actually the diff range. // Particularly: // // | // D <----- master branch // | // C Y <- feature branch // | /| // B X // | / // A // | // // If "A, B, C, D" are master, and the user is at Y, when they run // "arc diff B" they want (and get) a diff of B vs Y, but they think about // this as being the commits X and Y. If we log "B..Y", we only show // Y. With "Y --not B", we show X and Y. if ($this->symbolicHeadCommit !== null) { $base_commit = $this->getBaseCommit(); $resolved_base = $this->resolveCommit($base_commit); $head_commit = $this->symbolicHeadCommit; $resolved_head = $this->getHeadCommit(); if (!$this->isDescendant($resolved_head, $resolved_base)) { // NOTE: Since the base commit will have been resolved as the // merge-base of the specified base and the specified HEAD, we can't // easily tell exactly what's wrong with the range. // For example, `arc diff HEAD --head HEAD^^^` is invalid because it // is reversed, but resolving the commit "HEAD" will compute its // merge-base with "HEAD^^^", which is "HEAD^^^", so the range will // appear empty. throw new ArcanistUsageException( pht( 'The specified commit range is empty, backward or invalid: the '. 'base (%s) is not an ancestor of the head (%s). You can not '. 'diff an empty or reversed commit range.', $base_commit, $head_commit)); } } $against = csprintf( '%s --not %s', $this->getHeadCommit(), $this->getBaseCommit()); } // NOTE: Windows escaping of "%" symbols apparently is inherently broken; // when passed through escapeshellarg() they are replaced with spaces. // TODO: Learn how cmd.exe works and find some clever workaround? // NOTE: If we use "%x00", output is truncated in Windows. list($info) = $this->execxLocal( phutil_is_windows() ? 'log %C --format=%C --' : 'log %C --format=%s --', $against, // NOTE: "%B" is somewhat new, use "%s%n%n%b" instead. '%H%x01%T%x01%P%x01%at%x01%an%x01%aE%x01%s%x01%s%n%n%b%x02'); $commits = array(); $info = trim($info, " \n\2"); if (!strlen($info)) { return array(); } $info = explode("\2", $info); foreach ($info as $line) { list($commit, $tree, $parents, $time, $author, $author_email, $title, $message) = explode("\1", trim($line), 8); $message = rtrim($message); $commits[$commit] = array( 'commit' => $commit, 'tree' => $tree, 'parents' => array_filter(explode(' ', $parents)), 'time' => $time, 'author' => $author, 'summary' => $title, 'message' => $message, 'authorEmail' => $author_email, ); } return $commits; } protected function buildBaseCommit($symbolic_commit) { if ($symbolic_commit !== null) { if ($symbolic_commit == self::GIT_MAGIC_ROOT_COMMIT) { $this->setBaseCommitExplanation( pht('you explicitly specified the empty tree.')); return $symbolic_commit; } list($err, $merge_base) = $this->execManualLocal( 'merge-base %s %s', $symbolic_commit, $this->getHeadCommit()); if ($err) { throw new ArcanistUsageException( pht( "Unable to find any git commit named '%s' in this repository.", $symbolic_commit)); } if ($this->symbolicHeadCommit === null) { $this->setBaseCommitExplanation( pht( "it is the merge-base of the explicitly specified base commit ". "'%s' and HEAD.", $symbolic_commit)); } else { $this->setBaseCommitExplanation( pht( "it is the merge-base of the explicitly specified base commit ". "'%s' and the explicitly specified head commit '%s'.", $symbolic_commit, $this->symbolicHeadCommit)); } return trim($merge_base); } // Detect zero-commit or one-commit repositories. There is only one // relative-commit value that makes any sense in these repositories: the // empty tree. list($err) = $this->execManualLocal('rev-parse --verify HEAD^'); if ($err) { list($err) = $this->execManualLocal('rev-parse --verify HEAD'); if ($err) { $this->repositoryHasNoCommits = true; } if ($this->repositoryHasNoCommits) { $this->setBaseCommitExplanation(pht('the repository has no commits.')); } else { $this->setBaseCommitExplanation( pht('the repository has only one commit.')); } return self::GIT_MAGIC_ROOT_COMMIT; } if ($this->getBaseCommitArgumentRules() || $this->getConfigurationManager()->getConfigFromAnySource('base')) { $base = $this->resolveBaseCommit(); if (!$base) { throw new ArcanistUsageException( pht( "None of the rules in your 'base' configuration matched a valid ". "commit. Adjust rules or specify which commit you want to use ". "explicitly.")); } return $base; } $do_write = false; $default_relative = null; $working_copy = $this->getWorkingCopyIdentity(); if ($working_copy) { $default_relative = $working_copy->getProjectConfig( 'git.default-relative-commit'); $this->setBaseCommitExplanation( pht( "it is the merge-base of '%s' and HEAD, as specified in '%s' in ". "'%s'. This setting overrides other settings.", $default_relative, 'git.default-relative-commit', '.arcconfig')); } if (!$default_relative) { list($err, $upstream) = $this->execManualLocal( 'rev-parse --abbrev-ref --symbolic-full-name %s', '@{upstream}'); if (!$err) { $default_relative = trim($upstream); $this->setBaseCommitExplanation( pht( "it is the merge-base of '%s' (the Git upstream ". "of the current branch) HEAD.", $default_relative)); } } if (!$default_relative) { $default_relative = $this->readScratchFile('default-relative-commit'); $default_relative = trim($default_relative); if ($default_relative) { $this->setBaseCommitExplanation( pht( "it is the merge-base of '%s' and HEAD, as specified in '%s'.", $default_relative, '.git/arc/default-relative-commit')); } } if (!$default_relative) { // TODO: Remove the history lesson soon. echo phutil_console_format( "** %s **\n\n", pht('Select a Default Commit Range')); echo phutil_console_wrap( pht( "You're running a command which operates on a range of revisions ". "(usually, from some revision to HEAD) but have not specified the ". "revision that should determine the start of the range.\n\n". "Previously, arc assumed you meant '%s' when you did not specify ". "a start revision, but this behavior does not make much sense in ". "most workflows outside of Facebook's historic %s workflow.\n\n". "arc no longer assumes '%s'. You must specify a relative commit ". "explicitly when you invoke a command (e.g., `%s`, not just `%s`) ". "or select a default for this working copy.\n\nIn most cases, the ". "best default is '%s'. You can also select '%s' to preserve the ". "old behavior, or some other remote or branch. But you almost ". "certainly want to select 'origin/master'.\n\n". "(Technically: the merge-base of the selected revision and HEAD is ". "used to determine the start of the commit range.)", 'HEAD^', 'git-svn', 'HEAD^', 'arc diff HEAD^', 'arc diff', 'origin/master', 'HEAD^')); $prompt = pht('What default do you want to use? [origin/master]'); $default = phutil_console_prompt($prompt); if (!strlen(trim($default))) { $default = 'origin/master'; } $default_relative = $default; $do_write = true; } list($object_type) = $this->execxLocal( 'cat-file -t %s', $default_relative); if (trim($object_type) !== 'commit') { throw new Exception( pht( "Relative commit '%s' is not the name of a commit!", $default_relative)); } if ($do_write) { // Don't perform this write until we've verified that the object is a // valid commit name. $this->writeScratchFile('default-relative-commit', $default_relative); $this->setBaseCommitExplanation( pht( "it is the merge-base of '%s' and HEAD, as you just specified.", $default_relative)); } list($merge_base) = $this->execxLocal( 'merge-base %s HEAD', $default_relative); return trim($merge_base); } public function getHeadCommit() { if ($this->resolvedHeadCommit === null) { $this->resolvedHeadCommit = $this->resolveCommit( coalesce($this->symbolicHeadCommit, 'HEAD')); } return $this->resolvedHeadCommit; } public function setHeadCommit($symbolic_commit) { $this->symbolicHeadCommit = $symbolic_commit; $this->reloadCommitRange(); return $this; } /** * Translates a symbolic commit (like "HEAD^") to a commit identifier. * @param string_symbol commit. * @return string the commit SHA. */ private function resolveCommit($symbolic_commit) { list($err, $commit_hash) = $this->execManualLocal( 'rev-parse %s', $symbolic_commit); if ($err) { throw new ArcanistUsageException( pht( "Unable to find any git commit named '%s' in this repository.", $symbolic_commit)); } return trim($commit_hash); } private function getDiffFullOptions($detect_moves_and_renames = true) { $options = array( self::getDiffBaseOptions(), '--no-color', '--src-prefix=a/', '--dst-prefix=b/', '-U'.$this->getDiffLinesOfContext(), ); if ($detect_moves_and_renames) { $options[] = '-M'; $options[] = '-C'; } return implode(' ', $options); } private function getDiffBaseOptions() { $options = array( // Disable external diff drivers, like graphical differs, since Arcanist // needs to capture the diff text. '--no-ext-diff', // Disable textconv so we treat binary files as binary, even if they have // an alternative textual representation. TODO: Ideally, Differential // would ship up the binaries for 'arc patch' but display the textconv // output in the visual diff. '--no-textconv', // Provide a standard view of submodule changes; the 'log' and 'diff' // values do not parse by the diff parser. '--submodule=short', ); return implode(' ', $options); } /** * @param the base revision * @param head revision. If this is null, the generated diff will include the * working copy */ public function getFullGitDiff($base, $head = null) { $options = $this->getDiffFullOptions(); $config_options = array(); // See T13432. Disable the rare "diff.suppressBlankEmpty" configuration // option, which discards the " " (space) change type prefix on unchanged // blank lines. At time of writing the parser does not handle these // properly, but generating a more-standard diff is generally desirable // even if a future parser handles this case more gracefully. $config_options[] = '-c'; $config_options[] = 'diff.suppressBlankEmpty=false'; if ($head !== null) { list($stdout) = $this->execxLocal( "%LR diff {$options} %s %s --", $config_options, $base, $head); } else { list($stdout) = $this->execxLocal( "%LR diff {$options} %s --", $config_options, $base); } return $stdout; } /** * @param string Path to generate a diff for. * @param bool If true, detect moves and renames. Otherwise, ignore * moves/renames; this is useful because it prompts git to * generate real diff text. */ public function getRawDiffText($path, $detect_moves_and_renames = true) { $options = $this->getDiffFullOptions($detect_moves_and_renames); list($stdout) = $this->execxLocal( "diff {$options} %s -- %s", $this->getBaseCommit(), $path); return $stdout; } private function getBranchNameFromRef($ref) { $count = 0; $branch = preg_replace('/^refs\/heads\//', '', $ref, 1, $count); if ($count !== 1) { return null; } if (!strlen($branch)) { return null; } return $branch; } public function getBranchName() { list($err, $stdout, $stderr) = $this->execManualLocal( 'symbolic-ref --quiet HEAD'); if ($err === 0) { // We expect the branch name to come qualified with a refs/heads/ prefix. // Verify this, and strip it. $ref = rtrim($stdout); $branch = $this->getBranchNameFromRef($ref); if ($branch === null) { throw new Exception( pht('Failed to parse %s output!', 'git symbolic-ref')); } return $branch; } else if ($err === 1) { // Exit status 1 with --quiet indicates that HEAD is detached. return null; } else { throw new Exception( pht('Command %s failed: %s', 'git symbolic-ref', $stderr)); } } public function getRemoteURI() { // Determine which remote to examine; default to 'origin' $remote = 'origin'; $branch = $this->getBranchName(); if ($branch) { $path = $this->getPathToUpstream($branch); if ($path->isConnectedToRemote()) { $remote = $path->getRemoteRemoteName(); } } return $this->getGitRemoteFetchURI($remote); } public function getSourceControlPath() { // TODO: Try to get something useful here. return null; } public function getGitCommitLog() { $relative = $this->getBaseCommit(); if ($this->repositoryHasNoCommits) { // No commits yet. return ''; } else if ($relative == self::GIT_MAGIC_ROOT_COMMIT) { // First commit. list($stdout) = $this->execxLocal( 'log --format=medium HEAD'); } else { // 2..N commits. list($stdout) = $this->execxLocal( 'log --first-parent --format=medium %s..%s', $this->getBaseCommit(), $this->getHeadCommit()); } return $stdout; } public function getGitHistoryLog() { list($stdout) = $this->execxLocal( 'log --format=medium -n%d %s', self::SEARCH_LENGTH_FOR_PARENT_REVISIONS, $this->getBaseCommit()); return $stdout; } public function getSourceControlBaseRevision() { list($stdout) = $this->execxLocal( 'rev-parse %s', $this->getBaseCommit()); return rtrim($stdout, "\n"); } public function getCanonicalRevisionName($string) { $match = null; if (preg_match('/@([0-9]+)$/', $string, $match)) { $stdout = $this->getHashFromFromSVNRevisionNumber($match[1]); } else { list($stdout) = $this->execxLocal( phutil_is_windows() ? 'show -s --format=%C %s --' : 'show -s --format=%s %s --', '%H', $string); } return rtrim($stdout); } private function executeSVNFindRev($input, $vcs) { $match = array(); list($stdout) = $this->execxLocal( 'svn find-rev %s', $input); if (!$stdout) { throw new ArcanistUsageException( pht( 'Cannot find the %s equivalent of %s.', $vcs, $input)); } // When git performs a partial-rebuild during svn // look-up, we need to parse the final line $lines = explode("\n", $stdout); $stdout = $lines[count($lines) - 2]; return rtrim($stdout); } // Convert svn revision number to git hash public function getHashFromFromSVNRevisionNumber($revision_id) { return $this->executeSVNFindRev('r'.$revision_id, 'Git'); } // Convert a git hash to svn revision number public function getSVNRevisionNumberFromHash($hash) { return $this->executeSVNFindRev($hash, 'SVN'); } private function buildUncommittedStatusViaStatus() { $status = $this->buildLocalFuture( array( 'status --porcelain=2 -z', )); list($stdout) = $status->resolvex(); $result = new PhutilArrayWithDefaultValue(); $parts = explode("\0", $stdout); while (count($parts) > 1) { $entry = array_shift($parts); $entry_parts = explode(' ', $entry, 2); if ($entry_parts[0] == '1') { $entry_parts = explode(' ', $entry, 9); $path = $entry_parts[8]; } else if ($entry_parts[0] == '2') { $entry_parts = explode(' ', $entry, 10); $path = $entry_parts[9]; } else if ($entry_parts[0] == 'u') { $entry_parts = explode(' ', $entry, 11); $path = $entry_parts[10]; } else if ($entry_parts[0] == '?') { $entry_parts = explode(' ', $entry, 2); $result[$entry_parts[1]] = self::FLAG_UNTRACKED; continue; } $result[$path] |= self::FLAG_UNCOMMITTED; $index_state = substr($entry_parts[1], 0, 1); $working_state = substr($entry_parts[1], 1, 1); if ($index_state == 'A') { $result[$path] |= self::FLAG_ADDED; } else if ($index_state == 'M') { $result[$path] |= self::FLAG_MODIFIED; } else if ($index_state == 'D') { $result[$path] |= self::FLAG_DELETED; } if ($working_state != '.') { $result[$path] |= self::FLAG_UNSTAGED; if ($index_state == '.') { if ($working_state == 'A') { $result[$path] |= self::FLAG_ADDED; } else if ($working_state == 'M') { $result[$path] |= self::FLAG_MODIFIED; } else if ($working_state == 'D') { $result[$path] |= self::FLAG_DELETED; } } } $submodule_tracked = substr($entry_parts[2], 2, 1); $submodule_untracked = substr($entry_parts[2], 3, 1); if ($submodule_tracked == 'M' || $submodule_untracked == 'U') { $result[$path] |= self::FLAG_EXTERNALS; } if ($entry_parts[0] == '2') { $result[array_shift($parts)] = $result[$path] | self::FLAG_DELETED; $result[$path] |= self::FLAG_ADDED; } } return $result->toArray(); } protected function buildUncommittedStatus() { if (version_compare($this->getGitVersion(), '2.11.0', '>=')) { return $this->buildUncommittedStatusViaStatus(); } $diff_options = $this->getDiffBaseOptions(); if ($this->repositoryHasNoCommits) { $diff_base = self::GIT_MAGIC_ROOT_COMMIT; } else { $diff_base = 'HEAD'; } // Find uncommitted changes. $uncommitted_future = $this->buildLocalFuture( array( 'diff %C --raw %s --', $diff_options, $diff_base, )); $untracked_future = $this->buildLocalFuture( array( 'ls-files --others --exclude-standard', )); // Unstaged changes $unstaged_future = $this->buildLocalFuture( array( 'diff-files --name-only', )); $futures = array( $uncommitted_future, $untracked_future, // NOTE: `git diff-files` races with each of these other commands // internally, and resolves with inconsistent results if executed // in parallel. To work around this, DO NOT run it at the same time. // After the other commands exit, we can start the `diff-files` command. ); id(new FutureIterator($futures))->resolveAll(); // We're clear to start the `git diff-files` now. $unstaged_future->start(); $result = new PhutilArrayWithDefaultValue(); list($stdout) = $uncommitted_future->resolvex(); $uncommitted_files = $this->parseGitRawDiff($stdout); foreach ($uncommitted_files as $path => $mask) { $result[$path] |= ($mask | self::FLAG_UNCOMMITTED); } list($stdout) = $untracked_future->resolvex(); $stdout = rtrim($stdout, "\n"); if (strlen($stdout)) { $stdout = explode("\n", $stdout); foreach ($stdout as $path) { $result[$path] |= self::FLAG_UNTRACKED; } } list($stdout, $stderr) = $unstaged_future->resolvex(); $stdout = rtrim($stdout, "\n"); if (strlen($stdout)) { $stdout = explode("\n", $stdout); foreach ($stdout as $path) { $result[$path] |= self::FLAG_UNSTAGED; } } return $result->toArray(); } protected function buildCommitRangeStatus() { list($stdout, $stderr) = $this->execxLocal( 'diff %C --raw %s HEAD --', $this->getDiffBaseOptions(), $this->getBaseCommit()); return $this->parseGitRawDiff($stdout); } public function getGitConfig($key, $default = null) { list($err, $stdout) = $this->execManualLocal('config %s', $key); if ($err) { return $default; } return rtrim($stdout); } public function getAuthor() { list($stdout) = $this->execxLocal('var GIT_AUTHOR_IDENT'); return preg_replace('/\s+<.*/', '', rtrim($stdout, "\n")); } public function addToCommit(array $paths) { $this->execxLocal( 'add -A -- %Ls', $paths); $this->reloadWorkingCopy(); return $this; } public function doCommit($message) { $tmp_file = new TempFile(); Filesystem::writeFile($tmp_file, $message); // NOTE: "--allow-empty-message" was introduced some time after 1.7.0.4, // so we do not provide it and thus require a message. $this->execxLocal( 'commit -F %s', $tmp_file); $this->reloadWorkingCopy(); return $this; } public function amendCommit($message = null) { if ($message === null) { $this->execxLocal('commit --amend --allow-empty -C HEAD'); } else { $tmp_file = new TempFile(); Filesystem::writeFile($tmp_file, $message); $this->execxLocal( 'commit --amend --allow-empty -F %s', $tmp_file); } $this->reloadWorkingCopy(); return $this; } private function parseGitRawDiff($status, $full = false) { static $flags = array( 'A' => self::FLAG_ADDED, 'M' => self::FLAG_MODIFIED, 'D' => self::FLAG_DELETED, ); $status = trim($status); $lines = array(); foreach (explode("\n", $status) as $line) { if ($line) { $lines[] = preg_split("/[ \t]/", $line, 6); } } $files = array(); foreach ($lines as $line) { $mask = 0; // "git diff --raw" lines begin with a ":" character. $old_mode = ltrim($line[0], ':'); $new_mode = $line[1]; // The hashes may be padded with "." characters for alignment. Discard // them. $old_hash = rtrim($line[2], '.'); $new_hash = rtrim($line[3], '.'); $flag = $line[4]; $file = $line[5]; $new_value = intval($new_mode, 8); $is_submodule = (($new_value & 0160000) === 0160000); if (($is_submodule) && ($flag == 'M') && ($old_hash === $new_hash) && ($old_mode === $new_mode)) { // See T9455. We see this submodule as "modified", but the old and new // hashes are the same and the old and new modes are the same, so we // don't directly see a modification. // We can end up here if we have a submodule which has uncommitted // changes inside of it (for example, the user has added untracked // files or made uncommitted changes to files in the submodule). In // this case, we set a different flag because we can't meaningfully // give users the same prompt. // Note that if the submodule has real changes from the parent // perspective (the base commit has changed) and also has uncommitted // changes, we'll only see the real changes and miss the uncommitted // changes. At the time of writing, there is no reasonable porcelain // for finding those changes, and the impact of this error seems small. $mask |= self::FLAG_EXTERNALS; } else if (isset($flags[$flag])) { $mask |= $flags[$flag]; } else if ($flag[0] == 'R') { $both = explode("\t", $file); if ($full) { $files[$both[0]] = array( 'mask' => $mask | self::FLAG_DELETED, 'ref' => str_repeat('0', 40), ); } else { $files[$both[0]] = $mask | self::FLAG_DELETED; } $file = $both[1]; $mask |= self::FLAG_ADDED; } else if ($flag[0] == 'C') { $both = explode("\t", $file); $file = $both[1]; $mask |= self::FLAG_ADDED; } if ($full) { $files[$file] = array( 'mask' => $mask, 'ref' => $new_hash, ); } else { $files[$file] = $mask; } } return $files; } public function getAllFiles() { $future = $this->buildLocalFuture(array('ls-files -z')); return id(new LinesOfALargeExecFuture($future)) ->setDelimiter("\0"); } public function getChangedFiles($since_commit) { list($stdout) = $this->execxLocal( 'diff --raw %s', $since_commit); return $this->parseGitRawDiff($stdout); } public function getBlame($path) { list($stdout) = $this->execxLocal( 'blame --porcelain -w -M %s -- %s', $this->getBaseCommit(), $path); // the --porcelain format prints at least one header line per source line, // then the source line prefixed by a tab character $blame_info = preg_split('/^\t.*\n/m', rtrim($stdout)); // commit info is not repeated in these headers, so cache it $revision_data = array(); $blame = array(); foreach ($blame_info as $line_info) { $revision = substr($line_info, 0, 40); $data = idx($revision_data, $revision, array()); if (empty($data)) { $matches = array(); if (!preg_match('/^author (.*)$/m', $line_info, $matches)) { throw new Exception( pht( 'Unexpected output from %s: no author for commit %s', 'git blame', $revision)); } $data['author'] = $matches[1]; $data['from_first_commit'] = preg_match('/^boundary$/m', $line_info); $revision_data[$revision] = $data; } // Ignore lines predating the git repository (on a boundary commit) // rather than blaming them on the oldest diff's unfortunate author if (!$data['from_first_commit']) { $blame[] = array($data['author'], $revision); } } return $blame; } public function getOriginalFileData($path) { return $this->getFileDataAtRevision($path, $this->getBaseCommit()); } public function getCurrentFileData($path) { return $this->getFileDataAtRevision($path, 'HEAD'); } private function parseGitTree($stdout) { $result = array(); $stdout = trim($stdout); if (!strlen($stdout)) { return $result; } $lines = explode("\n", $stdout); foreach ($lines as $line) { $matches = array(); $ok = preg_match( '/^(\d{6}) (blob|tree|commit) ([a-z0-9]{40})[\t](.*)$/', $line, $matches); if (!$ok) { throw new Exception(pht('Failed to parse %s output!', 'git ls-tree')); } $result[$matches[4]] = array( 'mode' => $matches[1], 'type' => $matches[2], 'ref' => $matches[3], ); } return $result; } private function getFileDataAtRevision($path, $revision) { // NOTE: We don't want to just "git show {$revision}:{$path}" since if the // path was a directory at the given revision we'll get a list of its files // and treat it as though it as a file containing a list of other files, // which is silly. if (!strlen($path)) { // No filename, so there's no content (Probably new/deleted file). return null; } list($stdout) = $this->execxLocal( 'ls-tree %s -- %s', $revision, $path); $info = $this->parseGitTree($stdout); if (empty($info[$path])) { // No such path, or the path is a directory and we executed 'ls-tree dir/' // and got a list of its contents back. return null; } if ($info[$path]['type'] != 'blob') { // Path is or was a directory, not a file. return null; } list($stdout) = $this->execxLocal( 'cat-file blob %s', $info[$path]['ref']); return $stdout; } /** * Returns names of all the branches in the current repository. * * @return list> Dictionary of branch information. */ public function getAllBranches() { $field_list = array( '%(refname)', '%(objectname)', '%(committerdate:raw)', '%(tree)', '%(subject)', '%(subject)%0a%0a%(body)', '%02', ); list($stdout) = $this->execxLocal( 'for-each-ref --format=%s -- refs/heads', implode('%01', $field_list)); $current = $this->getBranchName(); $result = array(); $lines = explode("\2", $stdout); foreach ($lines as $line) { $line = trim($line); if (!strlen($line)) { continue; } $fields = explode("\1", $line, 6); list($ref, $hash, $epoch, $tree, $desc, $text) = $fields; $branch = $this->getBranchNameFromRef($ref); if ($branch !== null) { $result[] = array( 'current' => ($branch === $current), 'name' => $branch, 'ref' => $ref, 'hash' => $hash, 'tree' => $tree, 'epoch' => (int)$epoch, 'desc' => $desc, 'text' => $text, ); } } return $result; } public function getAllBranchRefs() { $branches = $this->getAllBranches(); $refs = array(); foreach ($branches as $branch) { $commit_ref = $this->newCommitRef() ->setCommitHash($branch['hash']) ->setTreeHash($branch['tree']) ->setCommitEpoch($branch['epoch']) ->attachMessage($branch['text']); $refs[] = $this->newBranchRef() ->setBranchName($branch['name']) ->setRefName($branch['ref']) ->setIsCurrentBranch($branch['current']) ->attachCommitRef($commit_ref); } return $refs; } public function getBaseCommitRef() { $base_commit = $this->getBaseCommit(); if ($base_commit === self::GIT_MAGIC_ROOT_COMMIT) { return null; } $base_message = $this->getCommitMessage($base_commit); // TODO: We should also pull the tree hash. return $this->newCommitRef() ->setCommitHash($base_commit) ->attachMessage($base_message); } public function getWorkingCopyRevision() { list($stdout) = $this->execxLocal('rev-parse HEAD'); return rtrim($stdout, "\n"); } public function isHistoryDefaultImmutable() { return false; } public function supportsAmend() { return true; } public function supportsCommitRanges() { return true; } public function supportsLocalCommits() { return true; } public function hasLocalCommit($commit) { try { if (!$this->getCanonicalRevisionName($commit)) { return false; } } catch (CommandException $exception) { return false; } return true; } public function getAllLocalChanges() { $diff = $this->getFullGitDiff($this->getBaseCommit()); if (!strlen(trim($diff))) { return array(); } $parser = new ArcanistDiffParser(); return $parser->parseDiff($diff); } public function getFinalizedRevisionMessage() { return pht( "You may now push this commit upstream, as appropriate (e.g. with ". "'%s', or '%s', or by printing and faxing it).", 'git push', 'git svn dcommit'); } public function getCommitMessage($commit) { list($message) = $this->execxLocal( 'log -n1 --format=%C %s --', '%s%n%n%b', $commit); return $message; } public function loadWorkingCopyDifferentialRevisions( ConduitClient $conduit, array $query) { $messages = $this->getGitCommitLog(); if (!strlen($messages)) { return array(); } $parser = new ArcanistDiffParser(); $messages = $parser->parseDiff($messages); // First, try to find revisions by explicit revision IDs in commit messages. $reason_map = array(); $revision_ids = array(); foreach ($messages as $message) { $object = ArcanistDifferentialCommitMessage::newFromRawCorpus( $message->getMetadata('message')); if ($object->getRevisionID()) { $revision_ids[] = $object->getRevisionID(); $reason_map[$object->getRevisionID()] = $message->getCommitHash(); } } if ($revision_ids) { $results = $conduit->callMethodSynchronous( 'differential.query', $query + array( 'ids' => $revision_ids, )); foreach ($results as $key => $result) { $hash = substr($reason_map[$result['id']], 0, 16); $results[$key]['why'] = pht( "Commit message for '%s' has explicit 'Differential Revision'.", $hash); } return $results; } // If we didn't succeed, try to find revisions by hash. $hashes = array(); foreach ($this->getLocalCommitInformation() as $commit) { $hashes[] = array('gtcm', $commit['commit']); $hashes[] = array('gttr', $commit['tree']); } $results = $conduit->callMethodSynchronous( 'differential.query', $query + array( 'commitHashes' => $hashes, )); foreach ($results as $key => $result) { $results[$key]['why'] = pht( 'A git commit or tree hash in the commit range is already attached '. 'to the Differential revision.'); } return $results; } public function updateWorkingCopy() { $this->execxLocal('pull'); $this->reloadWorkingCopy(); } public function getCommitSummary($commit) { if ($commit == self::GIT_MAGIC_ROOT_COMMIT) { return pht('(The Empty Tree)'); } list($summary) = $this->execxLocal( 'log -n 1 --format=%C %s', '%s', $commit); return trim($summary); } public function isGitSubversionRepo() { return Filesystem::pathExists($this->getPath('.git/svn')); } public function resolveBaseCommitRule($rule, $source) { list($type, $name) = explode(':', $rule, 2); switch ($type) { case 'git': $matches = null; if (preg_match('/^merge-base\((.+)\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal( 'merge-base %s HEAD', $matches[1]); if (!$err) { $this->setBaseCommitExplanation( pht( "it is the merge-base of '%s' and HEAD, as specified by ". "'%s' in your %s 'base' configuration.", $matches[1], $rule, $source)); return trim($merge_base); } } else if (preg_match('/^branch-unique\((.+)\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal( 'merge-base %s HEAD', $matches[1]); if ($err) { return null; } $merge_base = trim($merge_base); list($commits) = $this->execxLocal( 'log --format=%C %s..HEAD --', '%H', $merge_base); $commits = array_filter(explode("\n", $commits)); if (!$commits) { return null; } $commits[] = $merge_base; $head_branch_count = null; $all_branch_names = ipull($this->getAllBranches(), 'name'); foreach ($commits as $commit) { // Ideally, we would use something like "for-each-ref --contains" // to get a filtered list of branches ready for script consumption. // Instead, try to get predictable output from "branch --contains". $flags = array(); $flags[] = '--no-color'; // NOTE: The "--no-column" flag was introduced in Git 1.7.11, so // don't pass it if we're running an older version. See T9953. $version = $this->getGitVersion(); if (version_compare($version, '1.7.11', '>=')) { $flags[] = '--no-column'; } list($branches) = $this->execxLocal( 'branch %Ls --contains %s', $flags, $commit); $branches = array_filter(explode("\n", $branches)); // Filter the list, removing the "current" marker (*) and ignoring // anything other than known branch names (mainly, any possible // "detached HEAD" or "no branch" line). foreach ($branches as $key => $branch) { $branch = trim($branch, ' *'); if (in_array($branch, $all_branch_names)) { $branches[$key] = $branch; } else { unset($branches[$key]); } } if ($head_branch_count === null) { // If this is the first commit, it's HEAD. Count how many // branches it is on; we want to include commits on the same // number of branches. This covers a case where this branch // has sub-branches and we're running "arc diff" here again // for whatever reason. $head_branch_count = count($branches); } else if (count($branches) > $head_branch_count) { $branches = implode(', ', $branches); $this->setBaseCommitExplanation( pht( "it is the first commit between '%s' (the merge-base of ". "'%s' and HEAD) which is also contained by another branch ". "(%s).", $merge_base, $matches[1], $branches)); return $commit; } } } else { list($err) = $this->execManualLocal( 'cat-file -t %s', $name); if (!$err) { $this->setBaseCommitExplanation( pht( "it is specified by '%s' in your %s 'base' configuration.", $rule, $source)); return $name; } } break; case 'arc': switch ($name) { case 'empty': $this->setBaseCommitExplanation( pht( "you specified '%s' in your %s 'base' configuration.", $rule, $source)); return self::GIT_MAGIC_ROOT_COMMIT; case 'amended': $text = $this->getCommitMessage('HEAD'); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( $text); if ($message->getRevisionID()) { $this->setBaseCommitExplanation( pht( "HEAD has been amended with 'Differential Revision:', ". "as specified by '%s' in your %s 'base' configuration.", $rule, $source)); return 'HEAD^'; } break; case 'upstream': list($err, $upstream) = $this->execManualLocal( 'rev-parse --abbrev-ref --symbolic-full-name %s', '@{upstream}'); if (!$err) { $upstream = rtrim($upstream); list($upstream_merge_base) = $this->execxLocal( 'merge-base %s HEAD', $upstream); $upstream_merge_base = rtrim($upstream_merge_base); $this->setBaseCommitExplanation( pht( "it is the merge-base of the upstream of the current branch ". "and HEAD, and matched the rule '%s' in your %s ". "'base' configuration.", $rule, $source)); return $upstream_merge_base; } break; case 'this': $this->setBaseCommitExplanation( pht( "you specified '%s' in your %s 'base' configuration.", $rule, $source)); return 'HEAD^'; } default: return null; } return null; } public function canStashChanges() { return true; } public function stashChanges() { $this->execxLocal('stash'); $this->reloadWorkingCopy(); } public function unstashChanges() { $this->execxLocal('stash pop'); } protected function didReloadCommitRange() { // After an amend, the symbolic head may resolve to a different commit. $this->resolvedHeadCommit = null; } /** * Follow the chain of tracking branches upstream until we reach a remote * or cycle locally. * * @param string Ref to start from. * @return ArcanistGitUpstreamPath Path to an upstream. */ public function getPathToUpstream($start) { $cursor = $start; $path = new ArcanistGitUpstreamPath(); while (true) { list($err, $upstream) = $this->execManualLocal( 'rev-parse --symbolic-full-name %s@{upstream}', $cursor); if ($err) { // We ended up somewhere with no tracking branch, so we're done. break; } $upstream = trim($upstream); if (preg_match('(^refs/heads/)', $upstream)) { $upstream = preg_replace('(^refs/heads/)', '', $upstream); $is_cycle = $path->getUpstream($upstream); $path->addUpstream( $cursor, array( 'type' => ArcanistGitUpstreamPath::TYPE_LOCAL, 'name' => $upstream, 'cycle' => $is_cycle, )); if ($is_cycle) { // We ran into a local cycle, so we're done. break; } // We found another local branch, so follow that one upriver. $cursor = $upstream; continue; } if (preg_match('(^refs/remotes/)', $upstream)) { $upstream = preg_replace('(^refs/remotes/)', '', $upstream); list($remote, $branch) = explode('/', $upstream, 2); $path->addUpstream( $cursor, array( 'type' => ArcanistGitUpstreamPath::TYPE_REMOTE, 'name' => $branch, 'remote' => $remote, )); // We found a remote, so we're done. break; } throw new Exception( pht( 'Got unrecognized upstream format ("%s") from Git, expected '. '"refs/heads/..." or "refs/remotes/...".', $upstream)); } return $path; } public function isPerforceRemote($remote_name) { // See T13434. In Perforce workflows, "git p4 clone" creates "p4" refs // under "refs/remotes/", but does not define a real remote named "p4". // We treat this remote as though it were a real remote during "arc land", // but it does not respond to commands like "git remote show p4", so we // need to handle it specially. if ($remote_name !== 'p4') { return false; } $remote_dir = $this->getMetadataPath().'/refs/remotes/p4'; if (!Filesystem::pathExists($remote_dir)) { return false; } return true; } public function isPushableRemote($remote_name) { $uri = $this->getGitRemotePushURI($remote_name); return ($uri !== null); } + public function isFetchableRemote($remote_name) { + $uri = $this->getGitRemoteFetchURI($remote_name); + return ($uri !== null); + } + private function getGitRemoteFetchURI($remote_name) { return $this->getGitRemoteURI($remote_name, $for_push = false); } private function getGitRemotePushURI($remote_name) { return $this->getGitRemoteURI($remote_name, $for_push = true); } private function getGitRemoteURI($remote_name, $for_push) { $remote_uri = $this->loadGitRemoteURI($remote_name, $for_push); if ($remote_uri !== null) { $remote_uri = rtrim($remote_uri); if (!strlen($remote_uri)) { $remote_uri = null; } } return $remote_uri; } private function loadGitRemoteURI($remote_name, $for_push) { // Try to identify the best URI for a given remote. This is complicated // because remotes may have different "push" and "fetch" URIs, may // rewrite URIs with "insteadOf" configuration, and different versions // of Git support different URI resolution commands. // Remotes may also have more than one URI of a given type, but we ignore // those cases here. // Start with "git remote get-url [--push]". This is the simplest and // most accurate command, but was introduced most recently in Git's // history. $argv = array(); if ($for_push) { $argv[] = '--push'; } list($err, $stdout) = $this->execManualLocal( 'remote get-url %Ls -- %s', $argv, $remote_name); if (!$err) { return $stdout; } // See T13481. If "git remote get-url [--push]" failed, it might be because // the remote does not exist, but it might also be because the version of // Git is too old to support "git remote get-url", which was introduced // in Git 2.7 (circa late 2015). $git_version = $this->getGitVersion(); if (version_compare($git_version, '2.7', '>=')) { // This version of Git should support "git remote get-url --push", but // the command failed, so conclude this is not a valid remote and thus // there is no remote URI. return null; } // If we arrive here, we're in a version of Git which is too old to // support "git remote get-url [--push]". We're going to fall back to // older and less accurate mechanisms for figuring out the remote URI. // The first mechanism we try is "git ls-remote --get-url". This exists // in Git 1.7.5 or newer. It only gives us the fetch URI, so this result // will be incorrect if a remote has different fetch and push URIs. // However, this is very rare, and this result is almost always correct. // Note that some old versions of Git do not parse "--" in this command // properly. We omit it since it doesn't seem like there's anything // dangerous an attacker can do even if they can choose a remote name to // intentionally cause an argument misparse. // This will cause the command to behave incorrectly for remotes with // names which are also valid flags, like "--quiet". list($err, $stdout) = $this->execManualLocal( 'ls-remote --get-url %s', $remote_name); if (!$err) { // The "git ls-remote --get-url" command just echoes the remote name // (like "origin") if no remote URI is found. Treat this like a failure. $output_is_input = (rtrim($stdout) === $remote_name); if (!$output_is_input) { return $stdout; } } if (version_compare($git_version, '1.7.5', '>=')) { // This version of Git should support "git ls-remote --get-url", but // the command failed (or echoed the input), so conclude the remote // really does not exist. return null; } // Fall back to the very old "git config -- remote.origin.url" command. // This does not give us push URLs and does not resolve "insteadOf" // aliases, but still works in the simplest (and most common) cases. list($err, $stdout) = $this->execManualLocal( 'config -- %s', sprintf('remote.%s.url', $remote_name)); if (!$err) { return $stdout; } return null; } protected function newCurrentCommitSymbol() { return 'HEAD'; } public function isGitLFSWorkingCopy() { // We're going to run: // // $ git ls-files -z -- ':(attr:filter=lfs)' // // ...and exit as soon as it generates any field terminated with a "\0". // // If this command generates any such output, that means this working copy // contains at least one LFS file, so it's an LFS working copy. If it // exits with no error and no output, this is not an LFS working copy. // // If it exits with an error, we're in trouble. $future = $this->buildLocalFuture( array( 'ls-files -z -- %s', ':(attr:filter=lfs)', )); $lfs_list = id(new LinesOfALargeExecFuture($future)) ->setDelimiter("\0"); try { foreach ($lfs_list as $lfs_file) { // We have our answer, so we can throw the subprocess away. $future->resolveKill(); return true; } return false; } catch (CommandException $ex) { // This is probably an older version of Git. Continue below. } // In older versions of Git, the first command will fail with an error // ("Invalid pathspec magic..."). See PHI1718. // // Some other tests we could use include: // // (1) Look for ".gitattributes" at the repository root. This approach is // a rough approximation because ".gitattributes" may be global or in a // subdirectory. See D21190. // // (2) Use "git check-attr" and pipe a bunch of files into it, roughly // like this: // // $ git ls-files -z -- | git check-attr --stdin -z filter -- // // However, the best version of this check I could come up with is fairly // slow in even moderately large repositories (~200ms in a repository with // 10K paths). See D21190. // // (3) Use "git lfs ls-files". This is even worse than piping "ls-files" // to "check-attr" in PHP (~600ms in a repository with 10K paths). // // (4) Give up and just assume the repository isn't LFS. This is the // current behavior. return false; } + protected function newLandEngine() { + return new ArcanistGitLandEngine(); + } + + public function newLocalState() { + return id(new ArcanistGitLocalState()) + ->setRepositoryAPI($this); + } + } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 1a2db585..410da8ea 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -1,1111 +1,1171 @@ getMercurialEnvironmentVariables(); $argv[0] = 'hg '.$argv[0]; $future = newv('ExecFuture', $argv) ->setEnv($env) ->setCWD($this->getPath()); return $future; } public function execPassthru($pattern /* , ... */) { $args = func_get_args(); $env = $this->getMercurialEnvironmentVariables(); $args[0] = 'hg '.$args[0]; $passthru = newv('PhutilExecPassthru', $args) ->setEnv($env) ->setCWD($this->getPath()); return $passthru->resolve(); } public function getSourceControlSystemName() { return 'hg'; } public function getMetadataPath() { return $this->getPath('.hg'); } public function getSourceControlBaseRevision() { return $this->getCanonicalRevisionName($this->getBaseCommit()); } public function getCanonicalRevisionName($string) { $match = null; if ($this->isHgSubversionRepo() && preg_match('/@([0-9]+)$/', $string, $match)) { $string = hgsprintf('svnrev(%s)', $match[1]); } list($stdout) = $this->execxLocal( 'log -l 1 --template %s -r %s --', '{node}', $string); return $stdout; } public function getHashFromFromSVNRevisionNumber($revision_id) { $matches = array(); $string = hgsprintf('svnrev(%s)', $revision_id); list($stdout) = $this->execxLocal( 'log -l 1 --template %s -r %s --', '{node}', $string); if (!$stdout) { throw new ArcanistUsageException( pht('Cannot find the HG equivalent of %s given.', $revision_id)); } return $stdout; } public function getSVNRevisionNumberFromHash($hash) { $matches = array(); list($stdout) = $this->execxLocal( 'log -r %s --template {svnrev}', $hash); if (!$stdout) { throw new ArcanistUsageException( pht('Cannot find the SVN equivalent of %s given.', $hash)); } return $stdout; } public function getSourceControlPath() { return '/'; } public function getBranchName() { if (!$this->branch) { list($stdout) = $this->execxLocal('branch'); $this->branch = trim($stdout); } return $this->branch; } protected function didReloadCommitRange() { $this->localCommitInfo = null; } protected function buildBaseCommit($symbolic_commit) { if ($symbolic_commit !== null) { try { $commit = $this->getCanonicalRevisionName( hgsprintf('ancestor(%s,.)', $symbolic_commit)); } catch (Exception $ex) { // Try it as a revset instead of a commit id try { $commit = $this->getCanonicalRevisionName( hgsprintf('ancestor(%R,.)', $symbolic_commit)); } catch (Exception $ex) { throw new ArcanistUsageException( pht( "Commit '%s' is not a valid Mercurial commit identifier.", $symbolic_commit)); } } $this->setBaseCommitExplanation( pht( 'it is the greatest common ancestor of the working directory '. 'and the commit you specified explicitly.')); return $commit; } if ($this->getBaseCommitArgumentRules() || $this->getConfigurationManager()->getConfigFromAnySource('base')) { $base = $this->resolveBaseCommit(); if (!$base) { throw new ArcanistUsageException( pht( "None of the rules in your 'base' configuration matched a valid ". "commit. Adjust rules or specify which commit you want to use ". "explicitly.")); } return $base; } // Mercurial 2.1 and up have phases which indicate if something is // published or not. To find which revs are outgoing, it's much // faster to check the phase instead of actually checking the server. if ($this->supportsPhases()) { list($err, $stdout) = $this->execManualLocal( 'log --branch %s -r %s --style default', $this->getBranchName(), 'draft()'); } else { list($err, $stdout) = $this->execManualLocal( 'outgoing --branch %s --style default', $this->getBranchName()); } if (!$err) { $logs = ArcanistMercurialParser::parseMercurialLog($stdout); } else { // Mercurial (in some versions?) raises an error when there's nothing // outgoing. $logs = array(); } if (!$logs) { $this->setBaseCommitExplanation( pht( 'you have no outgoing commits, so arc assumes you intend to submit '. 'uncommitted changes in the working copy.')); return $this->getWorkingCopyRevision(); } $outgoing_revs = ipull($logs, 'rev'); // This is essentially an implementation of a theoretical `hg merge-base` // command. $against = $this->getWorkingCopyRevision(); while (true) { // NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is // new as of July 2011, so do this in a compatible way. Also, "hg log" // and "hg outgoing" don't necessarily show parents (even if given an // explicit template consisting of just the parents token) so we need // to separately execute "hg parents". list($stdout) = $this->execxLocal( 'parents --style default --rev %s', $against); $parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout); list($p1, $p2) = array_merge($parents_logs, array(null, null)); if ($p1 && !in_array($p1['rev'], $outgoing_revs)) { $against = $p1['rev']; break; } else if ($p2 && !in_array($p2['rev'], $outgoing_revs)) { $against = $p2['rev']; break; } else if ($p1) { $against = $p1['rev']; } else { // This is the case where you have a new repository and the entire // thing is outgoing; Mercurial literally accepts "--rev null" as // meaning "diff against the empty state". $against = 'null'; break; } } if ($against == 'null') { $this->setBaseCommitExplanation( pht('this is a new repository (all changes are outgoing).')); } else { $this->setBaseCommitExplanation( pht( 'it is the first commit reachable from the working copy state '. 'which is not outgoing.')); } return $against; } public function getLocalCommitInformation() { if ($this->localCommitInfo === null) { $base_commit = $this->getBaseCommit(); list($info) = $this->execxLocal( 'log --template %s --rev %s --branch %s --', "{node}\1{rev}\1{author}\1". "{date|rfc822date}\1{branch}\1{tag}\1{parents}\1{desc}\2", hgsprintf('(%s::. - %s)', $base_commit, $base_commit), $this->getBranchName()); $logs = array_filter(explode("\2", $info)); $last_node = null; $futures = array(); $commits = array(); foreach ($logs as $log) { list($node, $rev, $full_author, $date, $branch, $tag, $parents, $desc) = explode("\1", $log, 9); list($author, $author_email) = $this->parseFullAuthor($full_author); // NOTE: If a commit has only one parent, {parents} returns empty. // If it has two parents, {parents} returns revs and short hashes, not // full hashes. Try to avoid making calls to "hg parents" because it's // relatively expensive. $commit_parents = null; if (!$parents) { if ($last_node) { $commit_parents = array($last_node); } } if (!$commit_parents) { // We didn't get a cheap hit on previous commit, so do the full-cost // "hg parents" call. We can run these in parallel, at least. $futures[$node] = $this->execFutureLocal( 'parents --template %s --rev %s', '{node}\n', $node); } $commits[$node] = array( 'author' => $author, 'time' => strtotime($date), 'branch' => $branch, 'tag' => $tag, 'commit' => $node, 'rev' => $node, // TODO: Remove eventually. 'local' => $rev, 'parents' => $commit_parents, 'summary' => head(explode("\n", $desc)), 'message' => $desc, 'authorEmail' => $author_email, ); $last_node = $node; } $futures = id(new FutureIterator($futures)) ->limit(4); foreach ($futures as $node => $future) { list($parents) = $future->resolvex(); $parents = array_filter(explode("\n", $parents)); $commits[$node]['parents'] = $parents; } // Put commits in newest-first order, to be consistent with Git and the // expected order of "hg log" and "git log" under normal circumstances. // The order of ancestors() is oldest-first. $commits = array_reverse($commits); $this->localCommitInfo = $commits; } return $this->localCommitInfo; } public function getAllFiles() { // TODO: Handle paths with newlines. $future = $this->buildLocalFuture(array('manifest')); return new LinesOfALargeExecFuture($future); } public function getChangedFiles($since_commit) { list($stdout) = $this->execxLocal( 'status --rev %s', $since_commit); return ArcanistMercurialParser::parseMercurialStatus($stdout); } public function getBlame($path) { list($stdout) = $this->execxLocal( 'annotate -u -v -c --rev %s -- %s', $this->getBaseCommit(), $path); $lines = phutil_split_lines($stdout, $retain_line_endings = true); $blame = array(); foreach ($lines as $line) { if (!strlen($line)) { continue; } $matches = null; $ok = preg_match('/^\s*([^:]+?) ([a-f0-9]{12}):/', $line, $matches); if (!$ok) { throw new Exception( pht( 'Unable to parse Mercurial blame line: %s', $line)); } $revision = $matches[2]; $author = trim($matches[1]); $blame[] = array($author, $revision); } return $blame; } protected function buildUncommittedStatus() { list($stdout) = $this->execxLocal('status'); $results = new PhutilArrayWithDefaultValue(); $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout); foreach ($working_status as $path => $mask) { if (!($mask & parent::FLAG_UNTRACKED)) { // Mark tracked files as uncommitted. $mask |= self::FLAG_UNCOMMITTED; } $results[$path] |= $mask; } return $results->toArray(); } protected function buildCommitRangeStatus() { list($stdout) = $this->execxLocal( 'status --rev %s --rev tip', $this->getBaseCommit()); $results = new PhutilArrayWithDefaultValue(); $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout); foreach ($working_status as $path => $mask) { $results[$path] |= $mask; } return $results->toArray(); } protected function didReloadWorkingCopy() { // Diffs are against ".", so we need to drop the cache if we change the // working copy. $this->rawDiffCache = array(); $this->branch = null; } private function getDiffOptions() { $options = array( '--git', '-U'.$this->getDiffLinesOfContext(), ); return implode(' ', $options); } public function getRawDiffText($path) { $options = $this->getDiffOptions(); $range = $this->getBaseCommit(); $raw_diff_cache_key = $options.' '.$range.' '.$path; if (idx($this->rawDiffCache, $raw_diff_cache_key)) { return idx($this->rawDiffCache, $raw_diff_cache_key); } list($stdout) = $this->execxLocal( 'diff %C --rev %s -- %s', $options, $range, $path); $this->rawDiffCache[$raw_diff_cache_key] = $stdout; return $stdout; } public function getFullMercurialDiff() { return $this->getRawDiffText(''); } public function getOriginalFileData($path) { return $this->getFileDataAtRevision($path, $this->getBaseCommit()); } public function getCurrentFileData($path) { return $this->getFileDataAtRevision( $path, $this->getWorkingCopyRevision()); } public function getBulkOriginalFileData($paths) { return $this->getBulkFileDataAtRevision($paths, $this->getBaseCommit()); } public function getBulkCurrentFileData($paths) { return $this->getBulkFileDataAtRevision( $paths, $this->getWorkingCopyRevision()); } private function getBulkFileDataAtRevision($paths, $revision) { // Calling 'hg cat' on each file individually is slow (1 second per file // on a large repo) because mercurial has to decompress and parse the // entire manifest every time. Do it in one large batch instead. // hg cat will write the file data to files in a temp directory $tmpdir = Filesystem::createTemporaryDirectory(); // Mercurial doesn't create the directories for us :( foreach ($paths as $path) { $tmppath = $tmpdir.'/'.$path; Filesystem::createDirectory(dirname($tmppath), 0755, true); } // NOTE: The "%s%%p" construction passes a literal "%p" to Mercurial, // which is a formatting directive for a repo-relative filepath. The // particulars of the construction avoid Windows escaping issues. See // PHI904. list($err, $stdout) = $this->execManualLocal( 'cat --rev %s --output %s%%p -- %Ls', $revision, $tmpdir.DIRECTORY_SEPARATOR, $paths); $filedata = array(); foreach ($paths as $path) { $tmppath = $tmpdir.'/'.$path; if (Filesystem::pathExists($tmppath)) { $filedata[$path] = Filesystem::readFile($tmppath); } } Filesystem::remove($tmpdir); return $filedata; } private function getFileDataAtRevision($path, $revision) { list($err, $stdout) = $this->execManualLocal( 'cat --rev %s -- %s', $revision, $path); if ($err) { // Assume this is "no file at revision", i.e. a deleted or added file. return null; } else { return $stdout; } } public function getWorkingCopyRevision() { return '.'; } public function isHistoryDefaultImmutable() { return true; } public function supportsAmend() { list($err, $stdout) = $this->execManualLocal('help commit'); if ($err) { return false; } else { return (strpos($stdout, 'amend') !== false); } } public function supportsRebase() { if ($this->supportsRebase === null) { list($err) = $this->execManualLocal('help rebase'); $this->supportsRebase = $err === 0; } return $this->supportsRebase; } public function supportsPhases() { if ($this->supportsPhases === null) { list($err) = $this->execManualLocal('help phase'); $this->supportsPhases = $err === 0; } return $this->supportsPhases; } public function supportsCommitRanges() { return true; } public function supportsLocalCommits() { return true; } public function getAllBranches() { + // TODO: This is wrong, and returns bookmarks. + list($branch_info) = $this->execxLocal('bookmarks'); if (trim($branch_info) == 'no bookmarks set') { return array(); } $matches = null; preg_match_all( '/^\s*(\*?)\s*(.+)\s(\S+)$/m', $branch_info, $matches, PREG_SET_ORDER); $return = array(); foreach ($matches as $match) { - list(, $current, $name) = $match; + list(, $current, $name, $hash) = $match; + + list($id, $hash) = explode(':', $hash); + $return[] = array( 'current' => (bool)$current, 'name' => rtrim($name), + 'hash' => $hash, ); } return $return; } public function getAllBranchRefs() { $branches = $this->getAllBranches(); $refs = array(); foreach ($branches as $branch) { + $commit_ref = $this->newCommitRef() + ->setCommitHash($branch['hash']); + $refs[] = $this->newBranchRef() ->setBranchName($branch['name']) - ->setIsCurrentBranch($branch['current']); + ->setIsCurrentBranch($branch['current']) + ->attachCommitRef($commit_ref); } return $refs; } public function getBaseCommitRef() { $base_commit = $this->getBaseCommit(); if ($base_commit === 'null') { return null; } $base_message = $this->getCommitMessage($base_commit); return $this->newCommitRef() ->setCommitHash($base_commit) ->attachMessage($base_message); } public function hasLocalCommit($commit) { try { $this->getCanonicalRevisionName($commit); return true; } catch (Exception $ex) { return false; } } public function getCommitMessage($commit) { list($message) = $this->execxLocal( 'log --template={desc} --rev %s', $commit); return $message; } public function getAllLocalChanges() { $diff = $this->getFullMercurialDiff(); if (!strlen(trim($diff))) { return array(); } $parser = new ArcanistDiffParser(); return $parser->parseDiff($diff); } public function getFinalizedRevisionMessage() { return pht( "You may now push this commit upstream, as appropriate (e.g. with ". "'%s' or by printing and faxing it).", 'hg push'); } public function getCommitMessageLog() { $base_commit = $this->getBaseCommit(); list($stdout) = $this->execxLocal( 'log --template %s --rev %s --branch %s --', "{node}\1{desc}\2", hgsprintf('(%s::. - %s)', $base_commit, $base_commit), $this->getBranchName()); $map = array(); $logs = explode("\2", trim($stdout)); foreach (array_filter($logs) as $log) { list($node, $desc) = explode("\1", $log); $map[$node] = $desc; } return array_reverse($map); } public function loadWorkingCopyDifferentialRevisions( ConduitClient $conduit, array $query) { $messages = $this->getCommitMessageLog(); $parser = new ArcanistDiffParser(); // First, try to find revisions by explicit revision IDs in commit messages. $reason_map = array(); $revision_ids = array(); foreach ($messages as $node_id => $message) { $object = ArcanistDifferentialCommitMessage::newFromRawCorpus($message); if ($object->getRevisionID()) { $revision_ids[] = $object->getRevisionID(); $reason_map[$object->getRevisionID()] = $node_id; } } if ($revision_ids) { $results = $conduit->callMethodSynchronous( 'differential.query', $query + array( 'ids' => $revision_ids, )); foreach ($results as $key => $result) { $hash = substr($reason_map[$result['id']], 0, 16); $results[$key]['why'] = pht( "Commit message for '%s' has explicit 'Differential Revision'.", $hash); } return $results; } // Try to find revisions by hash. $hashes = array(); foreach ($this->getLocalCommitInformation() as $commit) { $hashes[] = array('hgcm', $commit['commit']); } if ($hashes) { // NOTE: In the case of "arc diff . --uncommitted" in a Mercurial working // copy with dirty changes, there may be no local commits. $results = $conduit->callMethodSynchronous( 'differential.query', $query + array( 'commitHashes' => $hashes, )); foreach ($results as $key => $hash) { $results[$key]['why'] = pht( 'A mercurial commit hash in the commit range is already attached '. 'to the Differential revision.'); } return $results; } return array(); } public function updateWorkingCopy() { $this->execxLocal('up'); $this->reloadWorkingCopy(); } private function getMercurialConfig($key, $default = null) { list($stdout) = $this->execxLocal('showconfig %s', $key); if ($stdout == '') { return $default; } return rtrim($stdout); } public function getAuthor() { $full_author = $this->getMercurialConfig('ui.username'); list($author, $author_email) = $this->parseFullAuthor($full_author); return $author; } /** * Parse the Mercurial author field. * * Not everyone enters their email address as a part of the username * field. Try to make it work when it's obvious. * * @param string $full_author * @return array */ protected function parseFullAuthor($full_author) { if (strpos($full_author, '@') === false) { $author = $full_author; $author_email = null; } else { $email = new PhutilEmailAddress($full_author); $author = $email->getDisplayName(); $author_email = $email->getAddress(); } return array($author, $author_email); } public function addToCommit(array $paths) { $this->execxLocal( 'addremove -- %Ls', $paths); $this->reloadWorkingCopy(); } public function doCommit($message) { $tmp_file = new TempFile(); Filesystem::writeFile($tmp_file, $message); $this->execxLocal('commit -l %s', $tmp_file); $this->reloadWorkingCopy(); } public function amendCommit($message = null) { if ($message === null) { $message = $this->getCommitMessage('.'); } $tmp_file = new TempFile(); Filesystem::writeFile($tmp_file, $message); try { $this->execxLocal( 'commit --amend -l %s', $tmp_file); } catch (CommandException $ex) { if (preg_match('/nothing changed/', $ex->getStdout())) { // NOTE: Mercurial considers it an error to make a no-op amend. Although // we generally defer to the underlying VCS to dictate behavior, this // one seems a little goofy, and we use amend as part of various // workflows under the assumption that no-op amends are fine. If this // amend failed because it's a no-op, just continue. } else { throw $ex; } } $this->reloadWorkingCopy(); } public function getCommitSummary($commit) { if ($commit == 'null') { return pht('(The Empty Void)'); } list($summary) = $this->execxLocal( 'log --template {desc} --limit 1 --rev %s', $commit); $summary = head(explode("\n", $summary)); return trim($summary); } public function resolveBaseCommitRule($rule, $source) { list($type, $name) = explode(':', $rule, 2); // NOTE: This function MUST return node hashes or symbolic commits (like // branch names or the word "tip"), not revsets. This includes ".^" and // similar, which a revset, not a symbolic commit identifier. If you return // a revset it will be escaped later and looked up literally. switch ($type) { case 'hg': $matches = null; if (preg_match('/^gca\((.+)\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal( 'log --template={node} --rev %s', sprintf('ancestor(., %s)', $matches[1])); if (!$err) { $this->setBaseCommitExplanation( pht( "it is the greatest common ancestor of '%s' and %s, as ". "specified by '%s' in your %s 'base' configuration.", $matches[1], '.', $rule, $source)); return trim($merge_base); } } else { list($err, $commit) = $this->execManualLocal( 'log --template {node} --rev %s', hgsprintf('%s', $name)); if ($err) { list($err, $commit) = $this->execManualLocal( 'log --template {node} --rev %s', $name); } if (!$err) { $this->setBaseCommitExplanation( pht( "it is specified by '%s' in your %s 'base' configuration.", $rule, $source)); return trim($commit); } } break; case 'arc': switch ($name) { case 'empty': $this->setBaseCommitExplanation( pht( "you specified '%s' in your %s 'base' configuration.", $rule, $source)); return 'null'; case 'outgoing': list($err, $outgoing_base) = $this->execManualLocal( 'log --template={node} --rev %s', 'limit(reverse(ancestors(.) - outgoing()), 1)'); if (!$err) { $this->setBaseCommitExplanation( pht( "it is the first ancestor of the working copy that is not ". "outgoing, and it matched the rule %s in your %s ". "'base' configuration.", $rule, $source)); return trim($outgoing_base); } case 'amended': $text = $this->getCommitMessage('.'); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( $text); if ($message->getRevisionID()) { $this->setBaseCommitExplanation( pht( "'%s' has been amended with 'Differential Revision:', ". "as specified by '%s' in your %s 'base' configuration.", '.'. $rule, $source)); // NOTE: This should be safe because Mercurial doesn't support // amend until 2.2. return $this->getCanonicalRevisionName('.^'); } break; case 'bookmark': $revset = 'limit('. ' sort('. ' (ancestors(.) and bookmark() - .) or'. ' (ancestors(.) - outgoing()), '. ' -rev),'. '1)'; list($err, $bookmark_base) = $this->execManualLocal( 'log --template={node} --rev %s', $revset); if (!$err) { $this->setBaseCommitExplanation( pht( "it is the first ancestor of %s that either has a bookmark, ". "or is already in the remote and it matched the rule %s in ". "your %s 'base' configuration", '.', $rule, $source)); return trim($bookmark_base); } break; case 'this': $this->setBaseCommitExplanation( pht( "you specified '%s' in your %s 'base' configuration.", $rule, $source)); return $this->getCanonicalRevisionName('.^'); default: if (preg_match('/^nodiff\((.+)\)$/', $name, $matches)) { list($results) = $this->execxLocal( 'log --template %s --rev %s', "{node}\1{desc}\2", sprintf('ancestor(.,%s)::.^', $matches[1])); $results = array_reverse(explode("\2", trim($results))); foreach ($results as $result) { if (empty($result)) { continue; } list($node, $desc) = explode("\1", $result, 2); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( $desc); if ($message->getRevisionID()) { $this->setBaseCommitExplanation( pht( "it is the first ancestor of %s that has a diff and is ". "the gca or a descendant of the gca with '%s', ". "specified by '%s' in your %s 'base' configuration.", '.', $matches[1], $rule, $source)); return $node; } } } break; } break; default: return null; } return null; } public function isHgSubversionRepo() { return file_exists($this->getPath('.hg/svn/rev_map')); } public function getSubversionInfo() { $info = array(); $base_path = null; $revision = null; list($err, $raw_info) = $this->execManualLocal('svn info'); if (!$err) { foreach (explode("\n", trim($raw_info)) as $line) { list($key, $value) = explode(': ', $line, 2); switch ($key) { case 'URL': $info['base_path'] = $value; $base_path = $value; break; case 'Repository UUID': $info['uuid'] = $value; break; case 'Revision': $revision = $value; break; default: break; } } if ($base_path && $revision) { $info['base_revision'] = $base_path.'@'.$revision; } } return $info; } public function getActiveBookmark() { $bookmarks = $this->getBookmarks(); foreach ($bookmarks as $bookmark) { if ($bookmark['is_active']) { return $bookmark['name']; } } return null; } public function isBookmark($name) { $bookmarks = $this->getBookmarks(); foreach ($bookmarks as $bookmark) { if ($bookmark['name'] === $name) { return true; } } return false; } public function isBranch($name) { $branches = $this->getBranches(); foreach ($branches as $branch) { if ($branch['name'] === $name) { return true; } } return false; } public function getBranches() { list($stdout) = $this->execxLocal('--debug branches'); $lines = ArcanistMercurialParser::parseMercurialBranches($stdout); $branches = array(); foreach ($lines as $name => $spec) { $branches[] = array( 'name' => $name, 'revision' => $spec['rev'], ); } return $branches; } public function getBookmarks() { $bookmarks = array(); list($raw_output) = $this->execxLocal('bookmarks'); $raw_output = trim($raw_output); if ($raw_output !== 'no bookmarks set') { foreach (explode("\n", $raw_output) as $line) { // example line: * mybook 2:6b274d49be97 list($name, $revision) = $this->splitBranchOrBookmarkLine($line); $is_active = false; if ('*' === $name[0]) { $is_active = true; $name = substr($name, 2); } $bookmarks[] = array( 'is_active' => $is_active, 'name' => $name, 'revision' => $revision, ); } } return $bookmarks; } + public function getBookmarkCommitHash($name) { + // TODO: Cache this. + + $bookmarks = $this->getBookmarks($name); + $bookmarks = ipull($bookmarks, null, 'name'); + + foreach ($bookmarks as $bookmark) { + if ($bookmark['name'] === $name) { + return $bookmark['revision']; + } + } + + throw new Exception(pht('No bookmark "%s".', $name)); + } + + public function getBranchCommitHash($name) { + // TODO: Cache this. + // TODO: This won't work when there are multiple branch heads with the + // same name. + + $branches = $this->getBranches($name); + + $heads = array(); + foreach ($branches as $branch) { + if ($branch['name'] === $name) { + $heads[] = $branch; + } + } + + if (count($heads) === 1) { + return idx(head($heads), 'revision'); + } + + if (!$heads) { + throw new Exception(pht('No branch "%s".', $name)); + } + + throw new Exception(pht('Too many branch heads for "%s".', $name)); + } + private function splitBranchOrBookmarkLine($line) { // branches and bookmarks are printed in the format: // default 0:a5ead76cdf85 (inactive) // * mybook 2:6b274d49be97 // this code divides the name half from the revision half // it does not parse the * and (inactive) bits $colon_index = strrpos($line, ':'); $before_colon = substr($line, 0, $colon_index); $start_rev_index = strrpos($before_colon, ' '); $name = substr($line, 0, $start_rev_index); $rev = substr($line, $start_rev_index); return array(trim($name), trim($rev)); } public function getRemoteURI() { list($stdout) = $this->execxLocal('paths default'); $stdout = trim($stdout); if (strlen($stdout)) { return $stdout; } return null; } private function getMercurialEnvironmentVariables() { $env = array(); // Mercurial has a "defaults" feature which basically breaks automation by // allowing the user to add random flags to any command. This feature is // "deprecated" and "a bad idea" that you should "forget ... existed" // according to project lead Matt Mackall: // // http://markmail.org/message/hl3d6eprubmkkqh5 // // There is an HGPLAIN environmental variable which enables "plain mode" // and hopefully disables this stuff. $env['HGPLAIN'] = 1; return $env; } + protected function newLandEngine() { + return new ArcanistMercurialLandEngine(); + } + + public function newLocalState() { + return id(new ArcanistMercurialLocalState()) + ->setRepositoryAPI($this); + } + + } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 5d2c748f..43026c60 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -1,737 +1,752 @@ diffLinesOfContext; } public function setDiffLinesOfContext($lines) { $this->diffLinesOfContext = $lines; return $this; } public function getWorkingCopyIdentity() { return $this->configurationManager->getWorkingCopyIdentity(); } public function getConfigurationManager() { return $this->configurationManager; } public static function newAPIFromConfigurationManager( ArcanistConfigurationManager $configuration_manager) { $working_copy = $configuration_manager->getWorkingCopyIdentity(); if (!$working_copy) { throw new Exception( pht( 'Trying to create a %s without a working copy!', __CLASS__)); } $root = $working_copy->getProjectRoot(); switch ($working_copy->getVCSType()) { case 'svn': $api = new ArcanistSubversionAPI($root); break; case 'hg': $api = new ArcanistMercurialAPI($root); break; case 'git': $api = new ArcanistGitAPI($root); break; default: throw new Exception( pht( 'The current working directory is not part of a working copy for '. 'a supported version control system (Git, Subversion or '. 'Mercurial).')); } $api->configurationManager = $configuration_manager; return $api; } public function __construct($path) { $this->path = $path; } public function getPath($to_file = null) { if ($to_file !== null) { return $this->path.DIRECTORY_SEPARATOR. ltrim($to_file, DIRECTORY_SEPARATOR); } else { return $this->path.DIRECTORY_SEPARATOR; } } /* -( Path Status )-------------------------------------------------------- */ abstract protected function buildUncommittedStatus(); abstract protected function buildCommitRangeStatus(); /** * Get a list of uncommitted paths in the working copy that have been changed * or are affected by other status effects, like conflicts or untracked * files. * * Convenience methods @{method:getUntrackedChanges}, * @{method:getUnstagedChanges}, @{method:getUncommittedChanges}, * @{method:getMergeConflicts}, and @{method:getIncompleteChanges} allow * simpler selection of paths in a specific state. * * This method returns a map of paths to bitmasks with status, using * `FLAG_` constants. For example: * * array( * 'some/uncommitted/file.txt' => ArcanistRepositoryAPI::FLAG_UNSTAGED, * ); * * A file may be in several states. Not all states are possible with all * version control systems. * * @return map Map of paths, see above. * @task status */ final public function getUncommittedStatus() { if ($this->uncommittedStatusCache === null) { $status = $this->buildUncommittedStatus(); ksort($status); $this->uncommittedStatusCache = $status; } return $this->uncommittedStatusCache; } /** * @task status */ final public function getUntrackedChanges() { return $this->getUncommittedPathsWithMask(self::FLAG_UNTRACKED); } /** * @task status */ final public function getUnstagedChanges() { return $this->getUncommittedPathsWithMask(self::FLAG_UNSTAGED); } /** * @task status */ final public function getUncommittedChanges() { return $this->getUncommittedPathsWithMask(self::FLAG_UNCOMMITTED); } /** * @task status */ final public function getMergeConflicts() { return $this->getUncommittedPathsWithMask(self::FLAG_CONFLICT); } /** * @task status */ final public function getIncompleteChanges() { return $this->getUncommittedPathsWithMask(self::FLAG_INCOMPLETE); } /** * @task status */ final public function getMissingChanges() { return $this->getUncommittedPathsWithMask(self::FLAG_MISSING); } /** * @task status */ final public function getDirtyExternalChanges() { return $this->getUncommittedPathsWithMask(self::FLAG_EXTERNALS); } /** * @task status */ private function getUncommittedPathsWithMask($mask) { $match = array(); foreach ($this->getUncommittedStatus() as $path => $flags) { if ($flags & $mask) { $match[] = $path; } } return $match; } /** * Get a list of paths affected by the commits in the current commit range. * * See @{method:getUncommittedStatus} for a description of the return value. * * @return map Map from paths to status. * @task status */ final public function getCommitRangeStatus() { if ($this->commitRangeStatusCache === null) { $status = $this->buildCommitRangeStatus(); ksort($status); $this->commitRangeStatusCache = $status; } return $this->commitRangeStatusCache; } /** * Get a list of paths affected by commits in the current commit range, or * uncommitted changes in the working copy. See @{method:getUncommittedStatus} * or @{method:getCommitRangeStatus} to retrieve smaller parts of the status. * * See @{method:getUncommittedStatus} for a description of the return value. * * @return map Map from paths to status. * @task status */ final public function getWorkingCopyStatus() { $range_status = $this->getCommitRangeStatus(); $uncommitted_status = $this->getUncommittedStatus(); $result = new PhutilArrayWithDefaultValue($range_status); foreach ($uncommitted_status as $path => $mask) { $result[$path] |= $mask; } $result = $result->toArray(); ksort($result); return $result; } /** * Drops caches after changes to the working copy. By default, some queries * against the working copy are cached. They * * @return this * @task status */ final public function reloadWorkingCopy() { $this->uncommittedStatusCache = null; $this->commitRangeStatusCache = null; $this->didReloadWorkingCopy(); $this->reloadCommitRange(); return $this; } /** * Hook for implementations to dirty working copy caches after the working * copy has been updated. * * @return void * @task status */ protected function didReloadWorkingCopy() { return; } /** * Fetches the original file data for each path provided. * * @return map Map from path to file data. */ public function getBulkOriginalFileData($paths) { $filedata = array(); foreach ($paths as $path) { $filedata[$path] = $this->getOriginalFileData($path); } return $filedata; } /** * Fetches the current file data for each path provided. * * @return map Map from path to file data. */ public function getBulkCurrentFileData($paths) { $filedata = array(); foreach ($paths as $path) { $filedata[$path] = $this->getCurrentFileData($path); } return $filedata; } /** * @return Traversable */ abstract public function getAllFiles(); abstract public function getBlame($path); abstract public function getRawDiffText($path); abstract public function getOriginalFileData($path); abstract public function getCurrentFileData($path); abstract public function getLocalCommitInformation(); abstract public function getSourceControlBaseRevision(); abstract public function getCanonicalRevisionName($string); abstract public function getBranchName(); abstract public function getSourceControlPath(); abstract public function isHistoryDefaultImmutable(); abstract public function supportsAmend(); abstract public function getWorkingCopyRevision(); abstract public function updateWorkingCopy(); abstract public function getMetadataPath(); abstract public function loadWorkingCopyDifferentialRevisions( ConduitClient $conduit, array $query); abstract public function getRemoteURI(); public function getChangedFiles($since_commit) { throw new ArcanistCapabilityNotSupportedException($this); } public function getAuthor() { throw new ArcanistCapabilityNotSupportedException($this); } public function addToCommit(array $paths) { throw new ArcanistCapabilityNotSupportedException($this); } abstract public function supportsLocalCommits(); public function doCommit($message) { throw new ArcanistCapabilityNotSupportedException($this); } public function amendCommit($message = null) { throw new ArcanistCapabilityNotSupportedException($this); } public function getAllBranches() { // TODO: Implement for Mercurial/SVN and make abstract. return array(); } public function getAllBranchRefs() { throw new ArcanistCapabilityNotSupportedException($this); } public function getBaseCommitRef() { throw new ArcanistCapabilityNotSupportedException($this); } public function hasLocalCommit($commit) { throw new ArcanistCapabilityNotSupportedException($this); } public function getCommitMessage($commit) { throw new ArcanistCapabilityNotSupportedException($this); } public function getCommitSummary($commit) { throw new ArcanistCapabilityNotSupportedException($this); } public function getAllLocalChanges() { throw new ArcanistCapabilityNotSupportedException($this); } public function getFinalizedRevisionMessage() { throw new ArcanistCapabilityNotSupportedException($this); } public function execxLocal($pattern /* , ... */) { $args = func_get_args(); return $this->buildLocalFuture($args)->resolvex(); } public function execManualLocal($pattern /* , ... */) { $args = func_get_args(); return $this->buildLocalFuture($args)->resolve(); } public function execFutureLocal($pattern /* , ... */) { $args = func_get_args(); return $this->buildLocalFuture($args); } abstract protected function buildLocalFuture(array $argv); public function canStashChanges() { return false; } public function stashChanges() { throw new ArcanistCapabilityNotSupportedException($this); } public function unstashChanges() { throw new ArcanistCapabilityNotSupportedException($this); } /* -( Scratch Files )------------------------------------------------------ */ /** * Try to read a scratch file, if it exists and is readable. * * @param string Scratch file name. * @return mixed String for file contents, or false for failure. * @task scratch */ public function readScratchFile($path) { $full_path = $this->getScratchFilePath($path); if (!$full_path) { return false; } if (!Filesystem::pathExists($full_path)) { return false; } try { $result = Filesystem::readFile($full_path); } catch (FilesystemException $ex) { return false; } return $result; } /** * Try to write a scratch file, if there's somewhere to put it and we can * write there. * * @param string Scratch file name to write. * @param string Data to write. * @return bool True on success, false on failure. * @task scratch */ public function writeScratchFile($path, $data) { $dir = $this->getScratchFilePath(''); if (!$dir) { return false; } if (!Filesystem::pathExists($dir)) { try { Filesystem::createDirectory($dir); } catch (Exception $ex) { return false; } } try { Filesystem::writeFile($this->getScratchFilePath($path), $data); } catch (FilesystemException $ex) { return false; } return true; } /** * Try to remove a scratch file. * * @param string Scratch file name to remove. * @return bool True if the file was removed successfully. * @task scratch */ public function removeScratchFile($path) { $full_path = $this->getScratchFilePath($path); if (!$full_path) { return false; } try { Filesystem::remove($full_path); } catch (FilesystemException $ex) { return false; } return true; } /** * Get a human-readable description of the scratch file location. * * @param string Scratch file name. * @return mixed String, or false on failure. * @task scratch */ public function getReadableScratchFilePath($path) { $full_path = $this->getScratchFilePath($path); if ($full_path) { return Filesystem::readablePath( $full_path, $this->getPath()); } else { return false; } } /** * Get the path to a scratch file, if possible. * * @param string Scratch file name. * @return mixed File path, or false on failure. * @task scratch */ public function getScratchFilePath($path) { $new_scratch_path = Filesystem::resolvePath( 'arc', $this->getMetadataPath()); static $checked = false; if (!$checked) { $checked = true; $old_scratch_path = $this->getPath('.arc'); // we only want to do the migration once // unfortunately, people have checked in .arc directories which // means that the old one may get recreated after we delete it if (Filesystem::pathExists($old_scratch_path) && !Filesystem::pathExists($new_scratch_path)) { Filesystem::createDirectory($new_scratch_path); $existing_files = Filesystem::listDirectory($old_scratch_path, true); foreach ($existing_files as $file) { $new_path = Filesystem::resolvePath($file, $new_scratch_path); $old_path = Filesystem::resolvePath($file, $old_scratch_path); Filesystem::writeFile( $new_path, Filesystem::readFile($old_path)); } Filesystem::remove($old_scratch_path); } } return Filesystem::resolvePath($path, $new_scratch_path); } /* -( Base Commits )------------------------------------------------------- */ abstract public function supportsCommitRanges(); final public function setBaseCommit($symbolic_commit) { if (!$this->supportsCommitRanges()) { throw new ArcanistCapabilityNotSupportedException($this); } $this->symbolicBaseCommit = $symbolic_commit; $this->reloadCommitRange(); return $this; } public function setHeadCommit($symbolic_commit) { throw new ArcanistCapabilityNotSupportedException($this); } final public function getBaseCommit() { if (!$this->supportsCommitRanges()) { throw new ArcanistCapabilityNotSupportedException($this); } if ($this->resolvedBaseCommit === null) { $commit = $this->buildBaseCommit($this->symbolicBaseCommit); $this->resolvedBaseCommit = $commit; } return $this->resolvedBaseCommit; } public function getHeadCommit() { throw new ArcanistCapabilityNotSupportedException($this); } final public function reloadCommitRange() { $this->resolvedBaseCommit = null; $this->baseCommitExplanation = null; $this->didReloadCommitRange(); return $this; } protected function didReloadCommitRange() { return; } protected function buildBaseCommit($symbolic_commit) { throw new ArcanistCapabilityNotSupportedException($this); } public function getBaseCommitExplanation() { return $this->baseCommitExplanation; } public function setBaseCommitExplanation($explanation) { $this->baseCommitExplanation = $explanation; return $this; } public function resolveBaseCommitRule($rule, $source) { return null; } public function setBaseCommitArgumentRules($base_commit_argument_rules) { $this->baseCommitArgumentRules = $base_commit_argument_rules; return $this; } public function getBaseCommitArgumentRules() { return $this->baseCommitArgumentRules; } public function resolveBaseCommit() { $base_commit_rules = array( 'runtime' => $this->getBaseCommitArgumentRules(), 'local' => '', 'project' => '', 'user' => '', 'system' => '', ); $all_sources = $this->configurationManager->getConfigFromAllSources('base'); $base_commit_rules = $all_sources + $base_commit_rules; $parser = new ArcanistBaseCommitParser($this); $commit = $parser->resolveBaseCommit($base_commit_rules); return $commit; } public function getRepositoryUUID() { return null; } final public function newFuture($pattern /* , ... */) { $args = func_get_args(); return $this->buildLocalFuture($args) ->setResolveOnError(false); } final public function setRuntime(ArcanistRuntime $runtime) { $this->runtime = $runtime; return $this; } final public function getRuntime() { return $this->runtime; } final protected function getSymbolEngine() { return $this->getRuntime()->getSymbolEngine(); } final public function getCurrentWorkingCopyStateRef() { if ($this->currentWorkingCopyStateRef === false) { $ref = $this->newCurrentWorkingCopyStateRef(); $this->currentWorkingCopyStateRef = $ref; } return $this->currentWorkingCopyStateRef; } protected function newCurrentWorkingCopyStateRef() { $commit_ref = $this->getCurrentCommitRef(); if (!$commit_ref) { return null; } return id(new ArcanistWorkingCopyStateRef()) ->setCommitRef($commit_ref); } final public function getCurrentCommitRef() { if ($this->currentCommitRef === false) { $this->currentCommitRef = $this->newCurrentCommitRef(); } return $this->currentCommitRef; } protected function newCurrentCommitRef() { $symbols = $this->getSymbolEngine(); $commit_symbol = $this->newCurrentCommitSymbol(); return $symbols->loadCommitForSymbol($commit_symbol); } protected function newCurrentCommitSymbol() { throw new ArcanistCapabilityNotSupportedException($this); } final public function newCommitRef() { return new ArcanistCommitRef(); } final public function newBranchRef() { return new ArcanistBranchRef(); } + + final public function getLandEngine() { + $engine = $this->newLandEngine(); + + if ($engine) { + $engine->setRepositoryAPI($this); + } + + return $engine; + } + + protected function newLandEngine() { + return null; + } + } diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php new file mode 100644 index 00000000..d348e4b1 --- /dev/null +++ b/src/repository/state/ArcanistMercurialLocalState.php @@ -0,0 +1,60 @@ +localRef; + } + + public function getLocalPath() { + return $this->localPath; + } + + protected function executeSaveLocalState() { + $api = $this->getRepositoryAPI(); + // TODO: Fix this. + } + + protected function executeRestoreLocalState() { + $api = $this->getRepositoryAPI(); + // TODO: Fix this. + + // TODO: In Mercurial, we may want to discard commits we've created. + // $repository_api->execxLocal( + // '--config extensions.mq= strip %s', + // $this->onto); + + } + + protected function executeDiscardLocalState() { + // TODO: Fix this. + } + + protected function canStashChanges() { + // Depends on stash extension. + return false; + } + + protected function getIgnoreHints() { + // TODO: Provide this. + return array(); + } + + protected function saveStash() { + return null; + } + + protected function restoreStash($stash_ref) { + return null; + } + + protected function discardStash($stash_ref) { + return null; + } + +} diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php index eb5883fe..d27d4a16 100644 --- a/src/repository/state/ArcanistRepositoryLocalState.php +++ b/src/repository/state/ArcanistRepositoryLocalState.php @@ -1,245 +1,248 @@ workflow = $workflow; return $this; } final public function getWorkflow() { return $this->workflow; } final public function setRepositoryAPI(ArcanistRepositoryAPI $api) { $this->repositoryAPI = $api; return $this; } final public function getRepositoryAPI() { return $this->repositoryAPI; } final public function saveLocalState() { $api = $this->getRepositoryAPI(); $working_copy_display = tsprintf( " %s: %s\n", pht('Working Copy'), $api->getPath()); $conflicts = $api->getMergeConflicts(); if ($conflicts) { echo tsprintf( "\n%!\n%W\n\n%s\n", pht('MERGE CONFLICTS'), pht('You have merge conflicts in this working copy.'), $working_copy_display); $lists = array(); $lists[] = $this->newDisplayFileList( pht('Merge conflicts in working copy:'), $conflicts); $this->printFileLists($lists); throw new PhutilArgumentUsageException( pht( 'Resolve merge conflicts before proceeding.')); } $externals = $api->getDirtyExternalChanges(); if ($externals) { $message = pht( '%s submodule(s) have uncommitted or untracked changes:', new PhutilNumber(count($externals))); $prompt = pht( 'Ignore the changes to these %s submodule(s) and continue?', new PhutilNumber(count($externals))); $list = id(new PhutilConsoleList()) ->setWrap(false) ->addItems($externals); id(new PhutilConsoleBlock()) ->addParagraph($message) ->addList($list) ->draw(); $ok = phutil_console_confirm($prompt, $default_no = false); if (!$ok) { throw new ArcanistUserAbortException(); } } $uncommitted = $api->getUncommittedChanges(); $unstaged = $api->getUnstagedChanges(); $untracked = $api->getUntrackedChanges(); // We already dealt with externals. $unstaged = array_diff($unstaged, $externals); // We only want files which are purely uncommitted. $uncommitted = array_diff($uncommitted, $unstaged); $uncommitted = array_diff($uncommitted, $externals); if ($untracked || $unstaged || $uncommitted) { echo tsprintf( "\n%!\n%W\n\n%s\n", pht('UNCOMMITTED CHANGES'), pht('You have uncommitted changes in this working copy.'), $working_copy_display); $lists = array(); $lists[] = $this->newDisplayFileList( pht('Untracked changes in working copy:'), $untracked); $lists[] = $this->newDisplayFileList( pht('Unstaged changes in working copy:'), $unstaged); $lists[] = $this->newDisplayFileList( pht('Uncommitted changes in working copy:'), $uncommitted); $this->printFileLists($lists); if ($untracked) { $hints = $this->getIgnoreHints(); foreach ($hints as $hint) { echo tsprintf("%?\n", $hint); } } if ($this->canStashChanges()) { $query = pht('Stash these changes and continue?'); $this->getWorkflow() ->getPrompt('arc.state.stash') ->setQuery($query) ->execute(); $stash_ref = $this->saveStash(); if ($stash_ref === null) { throw new Exception( pht( 'Expected a non-null return from call to "%s->saveStash()".', get_class($this))); } $this->stashRef = $stash_ref; } else { throw new PhutilArgumentUsageException( pht( 'You can not continue with uncommitted changes. Commit or '. 'discard them before proceeding.')); } } $this->executeSaveLocalState(); $this->shouldRestore = true; + // TODO: Detect when we're in the middle of a rebase. + // TODO: Detect when we're in the middle of a cherry-pick. + return $this; } final public function restoreLocalState() { $this->shouldRestore = false; $this->executeRestoreLocalState(); if ($this->stashRef !== null) { $this->restoreStash($this->stashRef); } return $this; } final public function discardLocalState() { $this->shouldRestore = false; $this->executeDiscardLocalState(); if ($this->stashRef !== null) { $this->restoreStash($this->stashRef); $this->discardStash($this->stashRef); $this->stashRef = null; } return $this; } final public function __destruct() { if ($this->shouldRestore) { $this->restoreLocalState(); } $this->discardLocalState(); } protected function canStashChanges() { return false; } protected function saveStash() { throw new PhutilMethodNotImplementedException(); } protected function restoreStash($ref) { throw new PhutilMethodNotImplementedException(); } protected function discardStash($ref) { throw new PhutilMethodNotImplementedException(); } abstract protected function executeSaveLocalState(); abstract protected function executeRestoreLocalState(); abstract protected function executeDiscardLocalState(); protected function getIgnoreHints() { return array(); } final protected function newDisplayFileList($title, array $files) { if (!$files) { return null; } $items = array(); $items[] = tsprintf("%s\n\n", $title); foreach ($files as $file) { $items[] = tsprintf( " %s\n", $file); } return $items; } final protected function printFileLists(array $lists) { $lists = array_filter($lists); $last_key = last_key($lists); foreach ($lists as $key => $list) { foreach ($list as $item) { echo tsprintf('%B', $item); } if ($key !== $last_key) { echo tsprintf("\n\n"); } } echo tsprintf("\n"); } } diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index 2fa7b022..5fd8c993 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -1,1610 +1,283 @@ revision; - } +final class ArcanistLandWorkflow + extends ArcanistArcWorkflow { public function getWorkflowName() { return 'land'; } - public function getCommandSynopses() { - return phutil_console_format(<<newWorkflowInformation() + ->setHelp($help); } - public function getArguments() { + public function getWorkflowArguments() { return array( - 'onto' => array( - 'param' => 'master', - 'help' => pht( - "Land feature branch onto a branch other than the default ". - "('master' in git, 'default' in hg). You can change the default ". - "by setting '%s' with `%s` or for the entire project in %s.", - 'arc.land.onto.default', - 'arc set-config', - '.arcconfig'), - ), - 'hold' => array( - 'help' => pht( - 'Prepare the change to be pushed, but do not actually push it.'), - ), - 'keep-branch' => array( - 'help' => pht( - 'Keep the feature branch after pushing changes to the '. - 'remote (by default, it is deleted).'), - ), - 'remote' => array( - 'param' => 'origin', - 'help' => pht( - 'Push to a remote other than the default.'), - ), - 'merge' => array( - 'help' => pht( - 'Perform a %s merge, not a %s merge. If the project '. - 'is marked as having an immutable history, this is the default '. - 'behavior.', - '--no-ff', - '--squash'), - 'supports' => array( - 'git', - ), - 'nosupport' => array( - 'hg' => pht( - 'Use the %s strategy when landing in mercurial.', - '--squash'), - ), - ), - 'squash' => array( - 'help' => pht( - 'Perform a %s merge, not a %s merge. If the project is '. - 'marked as having a mutable history, this is the default behavior.', - '--squash', - '--no-ff'), - 'conflicts' => array( - 'merge' => pht( - '%s and %s are conflicting merge strategies.', - '--merge', - '--squash'), - ), - ), - 'delete-remote' => array( - 'help' => pht( - 'Delete the feature branch in the remote after landing it.'), - 'conflicts' => array( - 'keep-branch' => true, - ), - 'supports' => array( - 'hg', - ), - ), - 'revision' => array( - 'param' => 'id', - 'help' => pht( - 'Use the message from a specific revision, rather than '. - 'inferring the revision based on branch content.'), - ), - 'preview' => array( - 'help' => pht( - 'Prints the commits that would be landed. Does not '. - 'actually modify or land the commits.'), - ), - '*' => 'branch', + $this->newWorkflowArgument('hold') + ->setHelp( + pht( + 'Prepare the change to be pushed, but do not actually push it.')), + $this->newWorkflowArgument('keep-branches') + ->setHelp( + pht( + 'Keep local branches around after changes are pushed. By '. + 'default, local branches are deleted after they land.')), + $this->newWorkflowArgument('onto-remote') + ->setParameter('remote-name') + ->setHelp(pht('Push to a remote other than the default.')), + + // TODO: Formally allow flags to be bound to related configuration + // for documentation, e.g. "setRelatedConfiguration('arc.land.onto')". + + $this->newWorkflowArgument('onto') + ->setParameter('branch-name') + ->setRepeatable(true) + ->setHelp( + pht( + 'After merging, push changes onto a specified branch. '. + 'Specifying this flag multiple times will push multiple '. + 'branches.')), + $this->newWorkflowArgument('strategy') + ->setParameter('strategy-name') + ->setHelp( + pht( + // TODO: Improve this. + 'Merge using a particular strategy.')), + $this->newWorkflowArgument('revision') + ->setParameter('revision-identifier') + ->setHelp( + pht( + 'Land a specific revision, rather than determining the revisions '. + 'from the commits that are landing.')), + $this->newWorkflowArgument('preview') + ->setHelp( + pht( + 'Shows the changes that will land. Does not modify the working '. + 'copy or the remote.')), + $this->newWorkflowArgument('into') + ->setParameter('commit-ref') + ->setHelp( + pht( + 'Specifies the state to merge into. By default, this is the same '. + 'as the "onto" ref.')), + $this->newWorkflowArgument('into-remote') + ->setParameter('remote-name') + ->setHelp( + pht( + 'Specifies the remote to fetch the "into" ref from. By '. + 'default, this is the same as the "onto" remote.')), + $this->newWorkflowArgument('into-local') + ->setHelp( + pht( + 'Use the local "into" ref state instead of fetching it from '. + 'a remote.')), + $this->newWorkflowArgument('into-empty') + ->setHelp( + pht( + 'Merge into the empty state instead of an existing state. This '. + 'mode is primarily useful when creating a new repository, and '. + 'selected automatically if the "onto" ref does not exist and the '. + '"into" state is not specified.')), + $this->newWorkflowArgument('incremental') + ->setHelp( + pht( + 'When landing multiple revisions at once, push and rebase '. + 'after each operation instead of waiting until all merges '. + 'are completed. This is slower than the default behavior and '. + 'not atomic, but may make it easier to resolve conflicts and '. + 'land complicated changes by letting you make progress one '. + 'step at a time.')), + $this->newWorkflowArgument('ref') + ->setWildcard(true), ); } - public function run() { - $this->readArguments(); - - $engine = null; - if ($this->isGit && !$this->isGitSvn) { - $engine = new ArcanistGitLandEngine(); - } - - if ($engine) { - $should_hold = $this->getArgument('hold'); - $remote_arg = $this->getArgument('remote'); - $onto_arg = $this->getArgument('onto'); - - $engine - ->setWorkflow($this) - ->setRepositoryAPI($this->getRepositoryAPI()) - ->setSourceRef($this->branch) - ->setShouldHold($should_hold) - ->setShouldKeep($this->keepBranch) - ->setShouldSquash($this->useSquash) - ->setShouldPreview($this->preview) - ->setRemoteArgument($remote_arg) - ->setOntoArgument($onto_arg) - ->setBuildMessageCallback(array($this, 'buildEngineMessage')); - - // The goal here is to raise errors with flags early (which is cheap), - // before we test if the working copy is clean (which can be slow). This - // could probably be structured more cleanly. - - $engine->parseArguments(); - - // This must be configured or we fail later inside "buildEngineMessage()". - // This is less than ideal. - $this->ontoRemoteBranch = sprintf( - '%s/%s', - $engine->getTargetRemote(), - $engine->getTargetOnto()); - - $this->requireCleanWorkingCopy(); - $engine->execute(); - - if (!$should_hold && !$this->preview) { - $this->didPush(); - } - - return 0; - } - - $this->validate(); - - try { - $this->pullFromRemote(); - } catch (Exception $ex) { - $this->restoreBranch(); - throw $ex; - } - - $this->printPendingCommits(); - if ($this->preview) { - $this->restoreBranch(); - return 0; - } - - $this->checkoutBranch(); - $this->findRevision(); - - if ($this->useSquash) { - $this->rebase(); - $this->squash(); - } else { - $this->merge(); - } - - $this->push(); - - if (!$this->keepBranch) { - $this->cleanupBranch(); - } - - if ($this->oldBranch != $this->onto) { - // If we were on some branch A and the user ran "arc land B", - // switch back to A. - if ($this->keepBranch || $this->oldBranch != $this->branch) { - $this->restoreBranch(); - } - } - - echo pht('Done.'), "\n"; - - return 0; - } - - private function getUpstreamMatching($branch, $pattern) { - if ($this->isGit) { - $repository_api = $this->getRepositoryAPI(); - list($err, $fullname) = $repository_api->execManualLocal( - 'rev-parse --symbolic-full-name %s@{upstream}', - $branch); - if (!$err) { - $matches = null; - if (preg_match($pattern, $fullname, $matches)) { - return last($matches); - } - } - } - return null; - } - - private function getGitSvnTrunk() { - if (!$this->isGitSvn) { - return null; - } - - // See T13293, this depends on the options passed when cloning. - // On any error we return `trunk`, which was the previous default. - - $repository_api = $this->getRepositoryAPI(); - list($err, $refspec) = $repository_api->execManualLocal( - 'config svn-remote.svn.fetch'); - - if ($err) { - return 'trunk'; - } - - $refspec = rtrim(substr($refspec, strrpos($refspec, ':') + 1)); - - $prefix = 'refs/remotes/'; - if (substr($refspec, 0, strlen($prefix)) !== $prefix) { - return 'trunk'; - } - - $refspec = substr($refspec, strlen($prefix)); - return $refspec; - } - - private function readArguments() { - $repository_api = $this->getRepositoryAPI(); - $this->isGit = $repository_api instanceof ArcanistGitAPI; - $this->isHg = $repository_api instanceof ArcanistMercurialAPI; - - if ($this->isGit) { - $repository = $this->loadProjectRepository(); - $this->isGitSvn = (idx($repository, 'vcs') == 'svn'); - } - - if ($this->isHg) { - $this->isHgSvn = $repository_api->isHgSubversionRepo(); - } - - $branch = $this->getArgument('branch'); - if (empty($branch)) { - $branch = $this->getBranchOrBookmark(); - if ($branch !== null) { - $this->branchType = $this->getBranchType($branch); - - // TODO: This message is misleading when landing a detached head or - // a tag in Git. - - echo pht("Landing current %s '%s'.", $this->branchType, $branch), "\n"; - $branch = array($branch); - } - } - - if (count($branch) !== 1) { - throw new ArcanistUsageException( - pht('Specify exactly one branch or bookmark to land changes from.')); - } - $this->branch = head($branch); - $this->keepBranch = $this->getArgument('keep-branch'); - - $this->preview = $this->getArgument('preview'); - - if (!$this->branchType) { - $this->branchType = $this->getBranchType($this->branch); - } - - $onto_default = $this->isGit ? 'master' : 'default'; - $onto_default = nonempty( - $this->getConfigFromAnySource('arc.land.onto.default'), - $onto_default); - $onto_default = coalesce( - $this->getUpstreamMatching($this->branch, '/^refs\/heads\/(.+)$/'), - $onto_default); - $this->onto = $this->getArgument('onto', $onto_default); - $this->ontoType = $this->getBranchType($this->onto); - - $remote_default = $this->isGit ? 'origin' : ''; - $remote_default = coalesce( - $this->getUpstreamMatching($this->onto, '/^refs\/remotes\/(.+?)\//'), - $remote_default); - $this->remote = $this->getArgument('remote', $remote_default); - - if ($this->getArgument('merge')) { - $this->useSquash = false; - } else if ($this->getArgument('squash')) { - $this->useSquash = true; - } else { - $this->useSquash = !$this->isHistoryImmutable(); - } - - $this->ontoRemoteBranch = $this->onto; - if ($this->isGitSvn) { - $this->ontoRemoteBranch = $this->getGitSvnTrunk(); - } else if ($this->isGit) { - $this->ontoRemoteBranch = $this->remote.'/'.$this->onto; - } - - $this->oldBranch = $this->getBranchOrBookmark(); - } - - private function validate() { - $repository_api = $this->getRepositoryAPI(); - - if ($this->onto == $this->branch) { - $message = pht( - "You can not land a %s onto itself -- you are trying ". - "to land '%s' onto '%s'. For more information on how to push ". - "changes, see 'Pushing and Closing Revisions' in 'Arcanist User ". - "Guide: arc diff' in the documentation.", - $this->branchType, - $this->branch, - $this->onto); - if (!$this->isHistoryImmutable()) { - $message .= ' '.pht("You may be able to '%s' instead.", 'arc amend'); - } - throw new ArcanistUsageException($message); - } - - if ($this->isHg) { - if ($this->useSquash) { - if (!$repository_api->supportsRebase()) { - throw new ArcanistUsageException( - pht( - 'You must enable the rebase extension to use the %s strategy.', - '--squash')); - } - } - - if ($this->branchType != $this->ontoType) { - throw new ArcanistUsageException(pht( - 'Source %s is a %s but destination %s is a %s. When landing a '. - '%s, the destination must also be a %s. Use %s to specify a %s, '. - 'or set %s in %s.', - $this->branch, - $this->branchType, - $this->onto, - $this->ontoType, - $this->branchType, - $this->branchType, - '--onto', - $this->branchType, - 'arc.land.onto.default', - '.arcconfig')); - } - } - - if ($this->isGit) { - list($err) = $repository_api->execManualLocal( - 'rev-parse --verify %s', - $this->branch); - - if ($err) { - throw new ArcanistUsageException( - pht("Branch '%s' does not exist.", $this->branch)); - } - } - - $this->requireCleanWorkingCopy(); - } - - private function checkoutBranch() { - $repository_api = $this->getRepositoryAPI(); - if ($this->getBranchOrBookmark() != $this->branch) { - $repository_api->execxLocal('checkout %s', $this->branch); - } - - switch ($this->branchType) { - case self::REFTYPE_BOOKMARK: - $message = pht( - 'Switched to bookmark **%s**. Identifying and merging...', - $this->branch); - break; - case self::REFTYPE_BRANCH: - default: - $message = pht( - 'Switched to branch **%s**. Identifying and merging...', - $this->branch); - break; - } - - echo phutil_console_format($message."\n"); - } - - private function printPendingCommits() { - $repository_api = $this->getRepositoryAPI(); - - if ($repository_api instanceof ArcanistGitAPI) { - list($out) = $repository_api->execxLocal( - 'log --oneline %s %s --', - $this->branch, - '^'.$this->onto); - } else if ($repository_api instanceof ArcanistMercurialAPI) { - $common_ancestor = $repository_api->getCanonicalRevisionName( - hgsprintf('ancestor(%s,%s)', - $this->onto, - $this->branch)); - - $branch_range = hgsprintf( - 'reverse((%s::%s) - %s)', - $common_ancestor, - $this->branch, - $common_ancestor); - - list($out) = $repository_api->execxLocal( - 'log -r %s --template %s', - $branch_range, - '{node|short} {desc|firstline}\n'); - } - - if (!trim($out)) { - $this->restoreBranch(); - throw new ArcanistUsageException( - pht('No commits to land from %s.', $this->branch)); - } - - echo pht("The following commit(s) will be landed:\n\n%s", $out), "\n"; - } - - private function findRevision() { - $repository_api = $this->getRepositoryAPI(); - - $this->parseBaseCommitArgument(array($this->ontoRemoteBranch)); - - $revision_id = $this->getArgument('revision'); - if ($revision_id) { - $revision_id = $this->normalizeRevisionID($revision_id); - $revisions = $this->getConduit()->callMethodSynchronous( - 'differential.query', - array( - 'ids' => array($revision_id), - )); - if (!$revisions) { - throw new ArcanistUsageException(pht( - "No such revision '%s'!", - "D{$revision_id}")); - } - } else { - $revisions = $repository_api->loadWorkingCopyDifferentialRevisions( - $this->getConduit(), - array()); - } - - if (!count($revisions)) { - throw new ArcanistUsageException(pht( - "arc can not identify which revision exists on %s '%s'. Update the ". - "revision with recent changes to synchronize the %s name and hashes, ". - "or use '%s' to amend the commit message at HEAD, or use ". - "'%s' to select a revision explicitly.", - $this->branchType, - $this->branch, - $this->branchType, - 'arc amend', - '--revision ')); - } else if (count($revisions) > 1) { - switch ($this->branchType) { - case self::REFTYPE_BOOKMARK: - $message = pht( - "There are multiple revisions on feature bookmark '%s' which are ". - "not present on '%s':\n\n". - "%s\n". - 'Separate these revisions onto different bookmarks, or use '. - '--revision to use the commit message from '. - 'and land them all.', - $this->branch, - $this->onto, - $this->renderRevisionList($revisions)); - break; - case self::REFTYPE_BRANCH: - default: - $message = pht( - "There are multiple revisions on feature branch '%s' which are ". - "not present on '%s':\n\n". - "%s\n". - 'Separate these revisions onto different branches, or use '. - '--revision to use the commit message from '. - 'and land them all.', - $this->branch, - $this->onto, - $this->renderRevisionList($revisions)); - break; - } - - throw new ArcanistUsageException($message); - } - - $this->revision = head($revisions); - - $rev_status = $this->revision['status']; - $rev_id = $this->revision['id']; - $rev_title = $this->revision['title']; - $rev_auxiliary = idx($this->revision, 'auxiliary', array()); - - $full_name = pht('D%d: %s', $rev_id, $rev_title); - - if ($this->revision['authorPHID'] != $this->getUserPHID()) { - $other_author = $this->getConduit()->callMethodSynchronous( - 'user.query', - array( - 'phids' => array($this->revision['authorPHID']), - )); - $other_author = ipull($other_author, 'userName', 'phid'); - $other_author = $other_author[$this->revision['authorPHID']]; - $ok = phutil_console_confirm(pht( - "This %s has revision '%s' but you are not the author. Land this ". - "revision by %s?", - $this->branchType, - $full_name, - $other_author)); - if (!$ok) { - throw new ArcanistUserAbortException(); - } - } - - $state_warning = null; - $state_header = null; - if ($rev_status == ArcanistDifferentialRevisionStatus::CHANGES_PLANNED) { - $state_header = pht('REVISION HAS CHANGES PLANNED'); - $state_warning = pht( - 'The revision you are landing ("%s") is currently in the "%s" state, '. - 'indicating that you expect to revise it before moving forward.'. - "\n\n". - 'Normally, you should resubmit it for review and wait until it is '. - '"%s" by reviewers before you continue.'. - "\n\n". - 'To resubmit the revision for review, either: update the revision '. - 'with revised changes; or use "Request Review" from the web interface.', - $full_name, - pht('Changes Planned'), - pht('Accepted')); - } else if ($rev_status != ArcanistDifferentialRevisionStatus::ACCEPTED) { - $state_header = pht('REVISION HAS NOT BEEN ACCEPTED'); - $state_warning = pht( - 'The revision you are landing ("%s") has not been "%s" by reviewers.', - $full_name, - pht('Accepted')); - } - - if ($state_warning !== null) { - $prompt = pht('Land revision in the wrong state?'); - - id(new PhutilConsoleBlock()) - ->addParagraph(tsprintf('** %s **', $state_header)) - ->addParagraph(tsprintf('%B', $state_warning)) - ->draw(); - - $ok = phutil_console_confirm($prompt); - if (!$ok) { - throw new ArcanistUserAbortException(); - } - } - - if ($rev_auxiliary) { - $phids = idx($rev_auxiliary, 'phabricator:depends-on', array()); - if ($phids) { - $dep_on_revs = $this->getConduit()->callMethodSynchronous( - 'differential.query', - array( - 'phids' => $phids, - 'status' => 'status-open', - )); - - $open_dep_revs = array(); - foreach ($dep_on_revs as $dep_on_rev) { - $dep_on_rev_id = $dep_on_rev['id']; - $dep_on_rev_title = $dep_on_rev['title']; - $dep_on_rev_status = $dep_on_rev['status']; - $open_dep_revs[$dep_on_rev_id] = $dep_on_rev_title; - } - - if (!empty($open_dep_revs)) { - $open_revs = array(); - foreach ($open_dep_revs as $id => $title) { - $open_revs[] = ' - D'.$id.': '.$title; - } - $open_revs = implode("\n", $open_revs); - - echo pht( - "Revision '%s' depends on open revisions:\n\n%s", - "D{$rev_id}: {$rev_title}", - $open_revs); - - $ok = phutil_console_confirm(pht('Continue anyway?')); - if (!$ok) { - throw new ArcanistUserAbortException(); - } - } - } - } - - $message = $this->getConduit()->callMethodSynchronous( - 'differential.getcommitmessage', - array( - 'revision_id' => $rev_id, - )); - - $this->messageFile = new TempFile(); - Filesystem::writeFile($this->messageFile, $message); - - echo pht( - "Landing revision '%s'...", - "D{$rev_id}: {$rev_title}")."\n"; - - $diff_phid = idx($this->revision, 'activeDiffPHID'); - if ($diff_phid) { - $this->checkForBuildables($diff_phid); - } - } - - private function pullFromRemote() { - $repository_api = $this->getRepositoryAPI(); - - $local_ahead_of_remote = false; - if ($this->isGit) { - $repository_api->execxLocal('checkout %s', $this->onto); - - echo phutil_console_format(pht( - "Switched to branch **%s**. Updating branch...\n", - $this->onto)); - - try { - $repository_api->execxLocal('pull --ff-only --no-stat'); - } catch (CommandException $ex) { - if (!$this->isGitSvn) { - throw $ex; - } - } - list($out) = $repository_api->execxLocal( - 'log %s..%s', - $this->ontoRemoteBranch, - $this->onto); - if (strlen(trim($out))) { - $local_ahead_of_remote = true; - } else if ($this->isGitSvn) { - $repository_api->execxLocal('svn rebase'); - } - - } else if ($this->isHg) { - echo phutil_console_format(pht('Updating **%s**...', $this->onto)."\n"); - - try { - list($out, $err) = $repository_api->execxLocal('pull'); - - $divergedbookmark = $this->onto.'@'.$repository_api->getBranchName(); - if (strpos($err, $divergedbookmark) !== false) { - throw new ArcanistUsageException(phutil_console_format(pht( - "Local bookmark **%s** has diverged from the server's **%s** ". - "(now labeled **%s**). Please resolve this divergence and run ". - "'%s' again.", - $this->onto, - $this->onto, - $divergedbookmark, - 'arc land'))); - } - } catch (CommandException $ex) { - $err = $ex->getError(); - $stdout = $ex->getStdout(); - - // Copied from: PhabricatorRepositoryPullLocalDaemon.php - // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the - // behavior of "hg pull" to return 1 in case of a successful pull - // with no changes. This behavior has been reverted, but users who - // updated between Feb 1, 2012 and Mar 1, 2012 will have the - // erroring version. Do a dumb test against stdout to check for this - // possibility. - // See: https://github.com/phacility/phabricator/issues/101/ - - // NOTE: Mercurial has translated versions, which translate this error - // string. In a translated version, the string will be something else, - // like "aucun changement trouve". There didn't seem to be an easy way - // to handle this (there are hard ways but this is not a common - // problem and only creates log spam, not application failures). - // Assume English. - - // TODO: Remove this once we're far enough in the future that - // deployment of 2.1 is exceedingly rare? - if ($err != 1 || !preg_match('/no changes found/', $stdout)) { - throw $ex; - } - } - - // Pull succeeded. Now make sure master is not on an outgoing change - if ($repository_api->supportsPhases()) { - list($out) = $repository_api->execxLocal( - 'log -r %s --template %s', $this->onto, '{phase}'); - if ($out != 'public') { - $local_ahead_of_remote = true; - } - } else { - // execManual instead of execx because outgoing returns - // code 1 when there is nothing outgoing - list($err, $out) = $repository_api->execManualLocal( - 'outgoing -r %s', - $this->onto); - - // $err === 0 means something is outgoing - if ($err === 0) { - $local_ahead_of_remote = true; - } - } - } - - if ($local_ahead_of_remote) { - throw new ArcanistUsageException(pht( - "Local %s '%s' is ahead of remote %s '%s', so landing a feature ". - "%s would push additional changes. Push or reset the changes in '%s' ". - "before running '%s'.", - $this->ontoType, - $this->onto, - $this->ontoType, - $this->ontoRemoteBranch, - $this->ontoType, - $this->onto, - 'arc land')); - } - } - - private function rebase() { - $repository_api = $this->getRepositoryAPI(); - - chdir($repository_api->getPath()); - if ($this->isHg) { - $onto_tip = $repository_api->getCanonicalRevisionName($this->onto); - $common_ancestor = $repository_api->getCanonicalRevisionName( - hgsprintf('ancestor(%s, %s)', $this->onto, $this->branch)); - - // Only rebase if the local branch is not at the tip of the onto branch. - if ($onto_tip != $common_ancestor) { - // keep branch here so later we can decide whether to remove it - $err = $repository_api->execPassthru( - 'rebase -d %s --keepbranches', - $this->onto); - if ($err) { - echo phutil_console_format("%s\n", pht('Aborting rebase')); - $repository_api->execManualLocal('rebase --abort'); - $this->restoreBranch(); - throw new ArcanistUsageException(pht( - "'%s' failed and the rebase was aborted. This is most ". - "likely due to conflicts. Manually rebase %s onto %s, resolve ". - "the conflicts, then run '%s' again.", - sprintf('hg rebase %s', $this->onto), - $this->branch, - $this->onto, - 'arc land')); - } - } - } - - $repository_api->reloadWorkingCopy(); - } - - private function squash() { - $repository_api = $this->getRepositoryAPI(); - - if ($this->isGit) { - $repository_api->execxLocal('checkout %s', $this->onto); - $repository_api->execxLocal( - 'merge --no-stat --squash --ff-only %s', - $this->branch); - } else if ($this->isHg) { - // The hg code is a little more complex than git's because we - // need to handle the case where the landing branch has child branches: - // -a--------b master - // \ - // w--x mybranch - // \--y subbranch1 - // \--z subbranch2 - // - // arc land --branch mybranch --onto master : - // -a--b--wx master - // \--y subbranch1 - // \--z subbranch2 - - $branch_rev_id = $repository_api->getCanonicalRevisionName($this->branch); - - // At this point $this->onto has been pulled from remote and - // $this->branch has been rebased on top of onto(by the rebase() - // function). So we're guaranteed to have onto as an ancestor of branch - // when we use first((onto::branch)-onto) below. - $branch_root = $repository_api->getCanonicalRevisionName( - hgsprintf('first((%s::%s)-%s)', - $this->onto, - $this->branch, - $this->onto)); - - $branch_range = hgsprintf( - '(%s::%s)', - $branch_root, - $this->branch); - - if (!$this->keepBranch) { - $this->handleAlternateBranches($branch_root, $branch_range); - } - - // Collapse just the landing branch onto master. - // Leave its children on the original branch. - $err = $repository_api->execPassthru( - 'rebase --collapse --keep --logfile %s -r %s -d %s', - $this->messageFile, - $branch_range, - $this->onto); - - if ($err) { - $repository_api->execManualLocal('rebase --abort'); - $this->restoreBranch(); - throw new ArcanistUsageException( + protected function newPrompts() { + return array( + $this->newPrompt('arc.land.large-working-set') + ->setDescription( pht( - "Squashing the commits under %s failed. ". - "Manually squash your commits and run '%s' again.", - $this->branch, - 'arc land')); - } - - if ($repository_api->isBookmark($this->branch)) { - // a bug in mercurial means bookmarks end up on the revision prior - // to the collapse when using --collapse with --keep, - // so we manually move them to the correct spots - // see: http://bz.selenic.com/show_bug.cgi?id=3716 - $repository_api->execxLocal( - 'bookmark -f %s', - $this->onto); - - $repository_api->execxLocal( - 'bookmark -f %s -r %s', - $this->branch, - $branch_rev_id); - } - - // check if the branch had children - list($output) = $repository_api->execxLocal( - 'log -r %s --template %s', - hgsprintf('children(%s)', $this->branch), - '{node}\n'); - - $child_branch_roots = phutil_split_lines($output, false); - $child_branch_roots = array_filter($child_branch_roots); - if ($child_branch_roots) { - // move the branch's children onto the collapsed commit - foreach ($child_branch_roots as $child_root) { - $repository_api->execxLocal( - 'rebase -d %s -s %s --keep --keepbranches', - $this->onto, - $child_root); - } - } - - // All the rebases may have moved us to another branch - // so we move back. - $repository_api->execxLocal('checkout %s', $this->onto); - } - } - - /** - * Detect alternate branches and prompt the user for how to handle - * them. An alternate branch is a branch that forks from the landing - * branch prior to the landing branch tip. - * - * In a situation like this: - * -a--------b master - * \ - * w--x landingbranch - * \ \-- g subbranch - * \--y altbranch1 - * \--z altbranch2 - * - * y and z are alternate branches and will get deleted by the squash, - * so we need to detect them and ask the user what they want to do. - * - * @param string The revision id of the landing branch's root commit. - * @param string The revset specifying all the commits in the landing branch. - * @return void - */ - private function handleAlternateBranches($branch_root, $branch_range) { - $repository_api = $this->getRepositoryAPI(); - - // Using the tree in the doccomment, the revset below resolves as follows: - // 1. roots(descendants(w) - descendants(x) - (w::x)) - // 2. roots({x,g,y,z} - {g} - {w,x}) - // 3. roots({y,z}) - // 4. {y,z} - $alt_branch_revset = hgsprintf( - 'roots(descendants(%s)-descendants(%s)-%R)', - $branch_root, - $this->branch, - $branch_range); - list($alt_branches) = $repository_api->execxLocal( - 'log --template %s -r %s', - '{node}\n', - $alt_branch_revset); - - $alt_branches = phutil_split_lines($alt_branches, false); - $alt_branches = array_filter($alt_branches); - - $alt_count = count($alt_branches); - if ($alt_count > 0) { - $input = phutil_console_prompt(pht( - "%s '%s' has %s %s(s) forking off of it that would be deleted ". - "during a squash. Would you like to keep a non-squashed copy, rebase ". - "them on top of '%s', or abort and deal with them yourself? ". - "(k)eep, (r)ebase, (a)bort:", - ucfirst($this->branchType), - $this->branch, - $alt_count, - $this->branchType, - $this->branch)); - - if ($input == 'k' || $input == 'keep') { - $this->keepBranch = true; - } else if ($input == 'r' || $input == 'rebase') { - foreach ($alt_branches as $alt_branch) { - $repository_api->execxLocal( - 'rebase --keep --keepbranches -d %s -s %s', - $this->branch, - $alt_branch); - } - } else if ($input == 'a' || $input == 'abort') { - $branch_string = implode("\n", $alt_branches); - echo - "\n", + 'Confirms landing more than %s commit(s) in a single operation.', + new PhutilNumber($this->getLargeWorkingSetLimit()))), + $this->newPrompt('arc.land.confirm') + ->setDescription( pht( - "Remove the %s starting at these revisions and run %s again:\n%s", - $this->branchType.'s', - $branch_string, - 'arc land'), - "\n\n"; - throw new ArcanistUserAbortException(); - } else { - throw new ArcanistUsageException( - pht('Invalid choice. Aborting arc land.')); - } - } - } - - private function merge() { - $repository_api = $this->getRepositoryAPI(); - - // In immutable histories, do a --no-ff merge to force a merge commit with - // the right message. - $repository_api->execxLocal('checkout %s', $this->onto); - - chdir($repository_api->getPath()); - if ($this->isGit) { - $err = phutil_passthru( - 'git merge --no-stat --no-ff --no-commit %s', - $this->branch); - - if ($err) { - throw new ArcanistUsageException(pht( - "'%s' failed. Your working copy has been left in a partially ". - "merged state. You can: abort with '%s'; or follow the ". - "instructions to complete the merge.", - 'git merge', - 'git merge --abort')); - } - } else if ($this->isHg) { - // HG arc land currently doesn't support --merge. - // When merging a bookmark branch to a master branch that - // hasn't changed since the fork, mercurial fails to merge. - // Instead of only working in some cases, we just disable --merge - // until there is a demand for it. - // The user should never reach this line, since --merge is - // forbidden at the command line argument level. - throw new ArcanistUsageException( - pht('%s is not currently supported for hg repos.', '--merge')); - } - } - - private function push() { - $repository_api = $this->getRepositoryAPI(); - - // These commands can fail legitimately (e.g. commit hooks) - try { - if ($this->isGit) { - $repository_api->execxLocal('commit -F %s', $this->messageFile); - if (phutil_is_windows()) { - // Occasionally on large repositories on Windows, Git can exit with - // an unclean working copy here. This prevents reverts from being - // pushed to the remote when this occurs. - $this->requireCleanWorkingCopy(); - } - } else if ($this->isHg) { - // hg rebase produces a commit earlier as part of rebase - if (!$this->useSquash) { - $repository_api->execxLocal( - 'commit --logfile %s', - $this->messageFile); - } - } - // We dispatch this event so we can run checks on the merged revision, - // right before it gets pushed out. It's easier to do this in arc land - // than to try to hook into git/hg. - $this->didCommitMerge(); - } catch (Exception $ex) { - $this->executeCleanupAfterFailedPush(); - throw $ex; - } - - if ($this->getArgument('hold')) { - echo phutil_console_format(pht( - 'Holding change in **%s**: it has NOT been pushed yet.', - $this->onto)."\n"); - } else { - echo pht('Pushing change...'), "\n\n"; - - chdir($repository_api->getPath()); - - if ($this->isGitSvn) { - $err = phutil_passthru('git svn dcommit'); - $cmd = 'git svn dcommit'; - } else if ($this->isGit) { - $err = phutil_passthru('git push %s %s', $this->remote, $this->onto); - $cmd = 'git push'; - } else if ($this->isHgSvn) { - // hg-svn doesn't support 'push -r', so we do a normal push - // which hg-svn modifies to only push the current branch and - // ancestors. - $err = $repository_api->execPassthru('push %s', $this->remote); - $cmd = 'hg push'; - } else if ($this->isHg) { - if (strlen($this->remote)) { - $err = $repository_api->execPassthru( - 'push -r %s %s', - $this->onto, - $this->remote); - } else { - $err = $repository_api->execPassthru( - 'push -r %s', - $this->onto); - } - $cmd = 'hg push'; - } - - if ($err) { - echo phutil_console_format( - "** %s **\n", - pht('PUSH FAILED!')); - $this->executeCleanupAfterFailedPush(); - if ($this->isGit) { - throw new ArcanistUsageException(pht( - "'%s' failed! Fix the error and run '%s' again.", - $cmd, - 'arc land')); - } - throw new ArcanistUsageException(pht( - "'%s' failed! Fix the error and push this change manually.", - $cmd)); - } - - $this->didPush(); - - echo "\n"; - } - } - - private function executeCleanupAfterFailedPush() { - $repository_api = $this->getRepositoryAPI(); - if ($this->isGit) { - $repository_api->execxLocal('reset --hard HEAD^'); - $this->restoreBranch(); - } else if ($this->isHg) { - $repository_api->execxLocal( - '--config extensions.mq= strip %s', - $this->onto); - $this->restoreBranch(); - } - } - - private function cleanupBranch() { - $repository_api = $this->getRepositoryAPI(); - - echo pht('Cleaning up feature %s...', $this->branchType), "\n"; - if ($this->isGit) { - list($ref) = $repository_api->execxLocal( - 'rev-parse --verify %s', - $this->branch); - $ref = trim($ref); - $recovery_command = csprintf( - 'git checkout -b %s %s', - $this->branch, - $ref); - echo pht('(Use `%s` if you want it back.)', $recovery_command), "\n"; - $repository_api->execxLocal('branch -D %s', $this->branch); - } else if ($this->isHg) { - $common_ancestor = $repository_api->getCanonicalRevisionName( - hgsprintf('ancestor(%s,%s)', $this->onto, $this->branch)); - - $branch_root = $repository_api->getCanonicalRevisionName( - hgsprintf('first((%s::%s)-%s)', - $common_ancestor, - $this->branch, - $common_ancestor)); - - $repository_api->execxLocal( - '--config extensions.mq= strip -r %s', - $branch_root); - - if ($repository_api->isBookmark($this->branch)) { - $repository_api->execxLocal('bookmark -d %s', $this->branch); - } - } - - if ($this->getArgument('delete-remote')) { - if ($this->isHg) { - // named branches were closed as part of the earlier commit - // so only worry about bookmarks - if ($repository_api->isBookmark($this->branch)) { - $repository_api->execxLocal( - 'push -B %s %s', - $this->branch, - $this->remote); - } - } - } - } - - public function getSupportedRevisionControlSystems() { - return array('git', 'hg'); - } - - private function getBranchOrBookmark() { - $repository_api = $this->getRepositoryAPI(); - if ($this->isGit) { - $branch = $repository_api->getBranchName(); - - // If we don't have a branch name, just use whatever's at HEAD. - if (!strlen($branch) && !$this->isGitSvn) { - $branch = $repository_api->getWorkingCopyRevision(); - } - } else if ($this->isHg) { - $branch = $repository_api->getActiveBookmark(); - if (!$branch) { - $branch = $repository_api->getBranchName(); - } - } - - return $branch; - } - - private function getBranchType($branch) { - $repository_api = $this->getRepositoryAPI(); - if ($this->isHg && $repository_api->isBookmark($branch)) { - return 'bookmark'; - } - return 'branch'; - } - - /** - * Restore the original branch, e.g. after a successful land or a failed - * pull. - */ - private function restoreBranch() { - $repository_api = $this->getRepositoryAPI(); - $repository_api->execxLocal('checkout %s', $this->oldBranch); - if ($this->isGit) { - $repository_api->execxLocal('submodule update --init --recursive'); - } - echo pht( - "Switched back to %s %s.\n", - $this->branchType, - phutil_console_format('**%s**', $this->oldBranch)); + 'Confirms that the correct changes have been selected.')), + $this->newPrompt('arc.land.implicit') + ->setDescription( + pht( + 'Confirms that local commits which are not associated with '. + 'a revision should land.')), + $this->newPrompt('arc.land.unauthored') + ->setDescription( + pht( + 'Confirms that revisions you did not author should land.')), + $this->newPrompt('arc.land.changes-planned') + ->setDescription( + pht( + 'Confirms that revisions with changes planned should land.')), + $this->newPrompt('arc.land.closed') + ->setDescription( + pht( + 'Confirms that revisions that are already closed should land.')), + $this->newPrompt('arc.land.not-accepted') + ->setDescription( + pht( + 'Confirms that revisions that are not accepted should land.')), + $this->newPrompt('arc.land.open-parents') + ->setDescription( + pht( + 'Confirms that revisions with open parent revisions should '. + 'land.')), + $this->newPrompt('arc.land.failed-builds') + ->setDescription( + pht( + 'Confirms that revisions with failed builds.')), + $this->newPrompt('arc.land.ongoing-builds') + ->setDescription( + pht( + 'Confirms that revisions with ongoing builds.')), + ); } - - /** - * Check if a diff has a running or failed buildable, and prompt the user - * before landing if it does. - */ - private function checkForBuildables($diff_phid) { - // Try to use the more modern check which respects the "Warn on Land" - // behavioral flag on build plans if we can. This newer check won't work - // unless the server is running code from March 2019 or newer since the - // API methods we need won't exist yet. We'll fall back to the older check - // if this one doesn't work out. - try { - $this->checkForBuildablesWithPlanBehaviors($diff_phid); - return; - } catch (ArcanistUserAbortException $abort_ex) { - throw $abort_ex; - } catch (Exception $ex) { - // Continue with the older approach, below. - } - - // NOTE: Since Harbormaster is still beta and this stuff all got added - // recently, just bail if we can't find a buildable. This is just an - // advisory check intended to prevent human error. - - try { - $buildables = $this->getConduit()->callMethodSynchronous( - 'harbormaster.querybuildables', - array( - 'buildablePHIDs' => array($diff_phid), - 'manualBuildables' => false, - )); - } catch (ConduitClientException $ex) { - return; - } - - if (!$buildables['data']) { - // If there's no corresponding buildable, we're done. - return; - } - - $console = PhutilConsole::getConsole(); - - $buildable = head($buildables['data']); - - if ($buildable['buildableStatus'] == 'passed') { - $console->writeOut( - "** %s ** %s\n", - pht('BUILDS PASSED'), - pht('Harbormaster builds for the active diff completed successfully.')); - return; - } - - switch ($buildable['buildableStatus']) { - case 'building': - $message = pht( - 'Harbormaster is still building the active diff for this revision.'); - $prompt = pht('Land revision anyway, despite ongoing build?'); - break; - case 'failed': - $message = pht( - 'Harbormaster failed to build the active diff for this revision.'); - $prompt = pht('Land revision anyway, despite build failures?'); - break; - default: - // If we don't recognize the status, just bail. - return; - } - - $builds = $this->queryBuilds( - array( - 'buildablePHIDs' => array($buildable['phid']), - )); - - $console->writeOut($message."\n\n"); - - $builds = msortv($builds, 'getStatusSortVector'); - foreach ($builds as $build) { - $ansi_color = $build->getStatusANSIColor(); - $status_name = $build->getStatusName(); - $object_name = $build->getObjectName(); - $build_name = $build->getName(); - - echo tsprintf( - " ** %s ** %s: %s\n", - $status_name, - $object_name, - $build_name); - } - - $console->writeOut( - "\n%s\n\n **%s**: __%s__", - pht('You can review build details here:'), - pht('Harbormaster URI'), - $buildable['uri']); - - if (!phutil_console_confirm($prompt)) { - throw new ArcanistUserAbortException(); - } + public function getLargeWorkingSetLimit() { + return 50; } - private function checkForBuildablesWithPlanBehaviors($diff_phid) { - // TODO: These queries should page through all results instead of fetching - // only the first page, but we don't have good primitives to support that - // in "master" yet. - - $this->writeInfo( - pht('BUILDS'), - pht('Checking build status...')); - - $raw_buildables = $this->getConduit()->callMethodSynchronous( - 'harbormaster.buildable.search', - array( - 'constraints' => array( - 'objectPHIDs' => array( - $diff_phid, - ), - 'manual' => false, - ), - )); - - if (!$raw_buildables['data']) { - return; - } - - $buildables = $raw_buildables['data']; - $buildable_phids = ipull($buildables, 'phid'); - - $raw_builds = $this->getConduit()->callMethodSynchronous( - 'harbormaster.build.search', - array( - 'constraints' => array( - 'buildables' => $buildable_phids, - ), - )); - - if (!$raw_builds['data']) { - return; - } - - $builds = array(); - foreach ($raw_builds['data'] as $raw_build) { - $build_ref = ArcanistBuildRef::newFromConduit($raw_build); - $build_phid = $build_ref->getPHID(); - $builds[$build_phid] = $build_ref; - } - - $plan_phids = mpull($builds, 'getBuildPlanPHID'); - $plan_phids = array_values($plan_phids); - - $raw_plans = $this->getConduit()->callMethodSynchronous( - 'harbormaster.buildplan.search', - array( - 'constraints' => array( - 'phids' => $plan_phids, - ), - )); - - $plans = array(); - foreach ($raw_plans['data'] as $raw_plan) { - $plan_ref = ArcanistBuildPlanRef::newFromConduit($raw_plan); - $plan_phid = $plan_ref->getPHID(); - $plans[$plan_phid] = $plan_ref; - } - - $ongoing_builds = array(); - $failed_builds = array(); - - $builds = msortv($builds, 'getStatusSortVector'); - foreach ($builds as $build_ref) { - $plan = idx($plans, $build_ref->getBuildPlanPHID()); - if (!$plan) { - continue; - } - - $plan_behavior = $plan->getBehavior('arc-land', 'always'); - $if_building = ($plan_behavior == 'building'); - $if_complete = ($plan_behavior == 'complete'); - $if_never = ($plan_behavior == 'never'); - - // If the build plan "Never" warns when landing, skip it. - if ($if_never) { - continue; - } - - // If the build plan warns when landing "If Complete" but the build is - // not complete, skip it. - if ($if_complete && !$build_ref->isComplete()) { - continue; - } - - // If the build plan warns when landing "If Building" but the build is - // complete, skip it. - if ($if_building && $build_ref->isComplete()) { - continue; - } - - // Ignore passing builds. - if ($build_ref->isPassed()) { - continue; - } - - if (!$build_ref->isComplete()) { - $ongoing_builds[] = $build_ref; - } else { - $failed_builds[] = $build_ref; - } - } + public function runWorkflow() { + $working_copy = $this->getWorkingCopy(); + $repository_api = $working_copy->getRepositoryAPI(); - if (!$ongoing_builds && !$failed_builds) { - return; - } - - if ($failed_builds) { - $this->writeWarn( - pht('BUILD FAILURES'), - pht( - 'Harbormaster failed to build the active diff for this revision:')); - $prompt = pht('Land revision anyway, despite build failures?'); - } else if ($ongoing_builds) { - $this->writeWarn( - pht('ONGOING BUILDS'), + $land_engine = $repository_api->getLandEngine(); + if (!$land_engine) { + throw new PhutilArgumentUsageException( pht( - 'Harbormaster is still building the active diff for this revision:')); - $prompt = pht('Land revision anyway, despite ongoing build?'); - } - - $show_builds = array_merge($failed_builds, $ongoing_builds); - echo "\n"; - foreach ($show_builds as $build_ref) { - $ansi_color = $build_ref->getStatusANSIColor(); - $status_name = $build_ref->getStatusName(); - $object_name = $build_ref->getObjectName(); - $build_name = $build_ref->getName(); - - echo tsprintf( - " ** %s ** %s: %s\n", - $status_name, - $object_name, - $build_name); - } - - echo tsprintf( - "\n%s\n\n", - pht('You can review build details here:')); - - foreach ($buildables as $buildable) { - $buildable_uri = id(new PhutilURI($this->getConduitURI())) - ->setPath(sprintf('/B%d', $buildable['id'])); - - echo tsprintf( - " **%s**: __%s__\n", - pht('Buildable %d', $buildable['id']), - $buildable_uri); - } - - if (!phutil_console_confirm($prompt)) { - throw new ArcanistUserAbortException(); - } - } - - public function buildEngineMessage(ArcanistLandEngine $engine) { - // TODO: This is oh-so-gross. - $this->findRevision(); - $engine->setCommitMessageFile($this->messageFile); - } - - public function didCommitMerge() { - $this->dispatchEvent( - ArcanistEventType::TYPE_LAND_WILLPUSHREVISION, - array()); - } - - public function didPush() { - $this->askForRepositoryUpdate(); - - $mark_workflow = $this->buildChildWorkflow( - 'close-revision', - array( - '--finalize', - '--quiet', - $this->revision['id'], - )); - $mark_workflow->run(); + '"arc land" must be run in a Git or Mercurial working copy.')); + } + + $is_incremental = $this->getArgument('incremental'); + $source_refs = $this->getArgument('ref'); + + $onto_remote_arg = $this->getArgument('onto-remote'); + $onto_args = $this->getArgument('onto'); + + $into_remote = $this->getArgument('into-remote'); + $into_empty = $this->getArgument('into-empty'); + $into_local = $this->getArgument('into-local'); + $into = $this->getArgument('into'); + + $is_preview = $this->getArgument('preview'); + $should_hold = $this->getArgument('hold'); + $should_keep = $this->getArgument('keep-branches'); + + $revision = $this->getArgument('revision'); + $strategy = $this->getArgument('strategy'); + + $land_engine + ->setViewer($this->getViewer()) + ->setWorkflow($this) + ->setLogEngine($this->getLogEngine()) + ->setSourceRefs($source_refs) + ->setShouldHold($should_hold) + ->setShouldKeep($should_keep) + ->setStrategyArgument($strategy) + ->setShouldPreview($is_preview) + ->setOntoRemoteArgument($onto_remote_arg) + ->setOntoArguments($onto_args) + ->setIntoRemoteArgument($into_remote) + ->setIntoEmptyArgument($into_empty) + ->setIntoLocalArgument($into_local) + ->setIntoArgument($into) + ->setIsIncremental($is_incremental) + ->setRevisionSymbol($revision); + + $land_engine->execute(); } - private function queryBuilds(array $constraints) { - $conduit = $this->getConduit(); - - // NOTE: This method only loads the 100 most recent builds. It's rare for - // a revision to have more builds than that and there's currently no paging - // wrapper for "*.search" Conduit API calls available in Arcanist. - - try { - $raw_result = $conduit->callMethodSynchronous( - 'harbormaster.build.search', - array( - 'constraints' => $constraints, - )); - } catch (Exception $ex) { - // If the server doesn't have "harbormaster.build.search" yet (Aug 2016), - // try the older "harbormaster.querybuilds" instead. - $raw_result = $conduit->callMethodSynchronous( - 'harbormaster.querybuilds', - $constraints); - } - - $refs = array(); - foreach ($raw_result['data'] as $raw_data) { - $refs[] = ArcanistBuildRef::newFromConduit($raw_data); - } - - return $refs; - } - - } diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php index f4514698..1e0a1093 100644 --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -1,2443 +1,2452 @@ toolset = $toolset; return $this; } final public function getToolset() { return $this->toolset; } final public function setRuntime(ArcanistRuntime $runtime) { $this->runtime = $runtime; return $this; } final public function getRuntime() { return $this->runtime; } final public function setConfigurationEngine( ArcanistConfigurationEngine $engine) { $this->configurationEngine = $engine; return $this; } final public function getConfigurationEngine() { return $this->configurationEngine; } final public function setConfigurationSourceList( ArcanistConfigurationSourceList $list) { $this->configurationSourceList = $list; return $this; } final public function getConfigurationSourceList() { return $this->configurationSourceList; } public function newPhutilWorkflow() { $arguments = $this->getWorkflowArguments(); assert_instances_of($arguments, 'ArcanistWorkflowArgument'); $specs = mpull($arguments, 'getPhutilSpecification'); $phutil_workflow = id(new ArcanistPhutilWorkflow()) ->setName($this->getWorkflowName()) ->setWorkflow($this) ->setArguments($specs); $information = $this->getWorkflowInformation(); if ($information !== null) { if (!($information instanceof ArcanistWorkflowInformation)) { throw new Exception( pht( 'Expected workflow ("%s", of class "%s") to return an '. '"ArcanistWorkflowInformation" object from call to '. '"getWorkflowInformation()", got %s.', $this->getWorkflowName(), get_class($this), phutil_describe_type($information))); } } if ($information) { $synopsis = $information->getSynopsis(); if (strlen($synopsis)) { $phutil_workflow->setSynopsis($synopsis); } $examples = $information->getExamples(); if ($examples) { $examples = implode("\n", $examples); $phutil_workflow->setExamples($examples); } $help = $information->getHelp(); if (strlen($help)) { // Unwrap linebreaks in the help text so we don't get weird formatting. $help = preg_replace("/(?<=\S)\n(?=\S)/", ' ', $help); $phutil_workflow->setHelp($help); } } return $phutil_workflow; } final public function newLegacyPhutilWorkflow() { $phutil_workflow = id(new ArcanistPhutilWorkflow()) ->setName($this->getWorkflowName()); $arguments = $this->getArguments(); $specs = array(); foreach ($arguments as $key => $argument) { if ($key == '*') { $key = $argument; $argument = array( 'wildcard' => true, ); } unset($argument['paramtype']); unset($argument['supports']); unset($argument['nosupport']); unset($argument['passthru']); unset($argument['conflict']); $spec = array( 'name' => $key, ) + $argument; $specs[] = $spec; } $phutil_workflow->setArguments($specs); $synopses = $this->getCommandSynopses(); $phutil_workflow->setSynopsis($synopses); $help = $this->getCommandHelp(); if (strlen($help)) { $phutil_workflow->setHelp($help); } return $phutil_workflow; } final protected function newWorkflowArgument($key) { return id(new ArcanistWorkflowArgument()) ->setKey($key); } final protected function newWorkflowInformation() { return new ArcanistWorkflowInformation(); } final public function executeWorkflow(PhutilArgumentParser $args) { $runtime = $this->getRuntime(); $this->arguments = $args; $caught = null; $runtime->pushWorkflow($this); try { $err = $this->runWorkflow($args); } catch (Exception $ex) { $caught = $ex; } try { $this->runWorkflowCleanup(); } catch (Exception $ex) { phlog($ex); } $runtime->popWorkflow(); if ($caught) { throw $caught; } return $err; } - final protected function getLogEngine() { + final public function getLogEngine() { return $this->getRuntime()->getLogEngine(); } protected function runWorkflowCleanup() { // TOOLSETS: Do we need this? return; } public function __construct() {} public function run() { throw new PhutilMethodNotImplementedException(); } /** * Finalizes any cleanup operations that need to occur regardless of * whether the command succeeded or failed. */ public function finalize() { $this->finalizeWorkingCopy(); } /** * Return the command used to invoke this workflow from the command like, * e.g. "help" for @{class:ArcanistHelpWorkflow}. * * @return string The command a user types to invoke this workflow. */ abstract public function getWorkflowName(); /** * Return console formatted string with all command synopses. * * @return string 6-space indented list of available command synopses. */ public function getCommandSynopses() { return array(); } /** * Return console formatted string with command help printed in `arc help`. * * @return string 10-space indented help to use the command. */ public function getCommandHelp() { return null; } public function supportsToolset(ArcanistToolset $toolset) { return false; } /* -( Conduit )------------------------------------------------------------ */ /** * Set the URI which the workflow will open a conduit connection to when * @{method:establishConduit} is called. Arcanist makes an effort to set * this by default for all workflows (by reading ##.arcconfig## and/or the * value of ##--conduit-uri##) even if they don't need Conduit, so a workflow * can generally upgrade into a conduit workflow later by just calling * @{method:establishConduit}. * * You generally should not need to call this method unless you are * specifically overriding the default URI. It is normally sufficient to * just invoke @{method:establishConduit}. * * NOTE: You can not call this after a conduit has been established. * * @param string The URI to open a conduit to when @{method:establishConduit} * is called. * @return this * @task conduit */ final public function setConduitURI($conduit_uri) { if ($this->conduit) { throw new Exception( pht( 'You can not change the Conduit URI after a '. 'conduit is already open.')); } $this->conduitURI = $conduit_uri; return $this; } /** * Returns the URI the conduit connection within the workflow uses. * * @return string * @task conduit */ final public function getConduitURI() { return $this->conduitURI; } /** * Open a conduit channel to the server which was previously configured by * calling @{method:setConduitURI}. Arcanist will do this automatically if * the workflow returns ##true## from @{method:requiresConduit}, or you can * later upgrade a workflow and build a conduit by invoking it manually. * * You must establish a conduit before you can make conduit calls. * * NOTE: You must call @{method:setConduitURI} before you can call this * method. * * @return this * @task conduit */ final public function establishConduit() { if ($this->conduit) { return $this; } if (!$this->conduitURI) { throw new Exception( pht( 'You must specify a Conduit URI with %s before you can '. 'establish a conduit.', 'setConduitURI()')); } $this->conduit = new ConduitClient($this->conduitURI); if ($this->conduitTimeout) { $this->conduit->setTimeout($this->conduitTimeout); } return $this; } final public function getConfigFromAnySource($key) { $source_list = $this->getConfigurationSourceList(); if ($source_list) { $value_list = $source_list->getStorageValueList($key); if ($value_list) { return last($value_list)->getValue(); } return null; } return $this->configurationManager->getConfigFromAnySource($key); } /** * Set credentials which will be used to authenticate against Conduit. These * credentials can then be used to establish an authenticated connection to * conduit by calling @{method:authenticateConduit}. Arcanist sets some * defaults for all workflows regardless of whether or not they return true * from @{method:requireAuthentication}, based on the ##~/.arcrc## and * ##.arcconf## files if they are present. Thus, you can generally upgrade a * workflow which does not require authentication into an authenticated * workflow by later invoking @{method:requireAuthentication}. You should not * normally need to call this method unless you are specifically overriding * the defaults. * * NOTE: You can not call this method after calling * @{method:authenticateConduit}. * * @param dict A credential dictionary, see @{method:authenticateConduit}. * @return this * @task conduit */ final public function setConduitCredentials(array $credentials) { if ($this->isConduitAuthenticated()) { throw new Exception( pht('You may not set new credentials after authenticating conduit.')); } $this->conduitCredentials = $credentials; return $this; } /** * Get the protocol version the client should identify with. * * @return int Version the client should claim to be. * @task conduit */ final public function getConduitVersion() { return 6; } /** * Open and authenticate a conduit connection to a Phabricator server using * provided credentials. Normally, Arcanist does this for you automatically * when you return true from @{method:requiresAuthentication}, but you can * also upgrade an existing workflow to one with an authenticated conduit * by invoking this method manually. * * You must authenticate the conduit before you can make authenticated conduit * calls (almost all calls require authentication). * * This method uses credentials provided via @{method:setConduitCredentials} * to authenticate to the server: * * - ##user## (required) The username to authenticate with. * - ##certificate## (required) The Conduit certificate to use. * - ##description## (optional) Description of the invoking command. * * Successful authentication allows you to call @{method:getUserPHID} and * @{method:getUserName}, as well as use the client you access with * @{method:getConduit} to make authenticated calls. * * NOTE: You must call @{method:setConduitURI} and * @{method:setConduitCredentials} before you invoke this method. * * @return this * @task conduit */ final public function authenticateConduit() { if ($this->isConduitAuthenticated()) { return $this; } $this->establishConduit(); $credentials = $this->conduitCredentials; try { if (!$credentials) { throw new Exception( pht( 'Set conduit credentials with %s before authenticating conduit!', 'setConduitCredentials()')); } // If we have `token`, this server supports the simpler, new-style // token-based authentication. Use that instead of all the certificate // stuff. $token = idx($credentials, 'token'); if (strlen($token)) { $conduit = $this->getConduit(); $conduit->setConduitToken($token); try { $result = $this->getConduit()->callMethodSynchronous( 'user.whoami', array()); $this->userName = $result['userName']; $this->userPHID = $result['phid']; $this->conduitAuthenticated = true; return $this; } catch (Exception $ex) { $conduit->setConduitToken(null); throw $ex; } } if (empty($credentials['user'])) { throw new ConduitClientException( 'ERR-INVALID-USER', pht('Empty user in credentials.')); } if (empty($credentials['certificate'])) { throw new ConduitClientException( 'ERR-NO-CERTIFICATE', pht('Empty certificate in credentials.')); } $description = idx($credentials, 'description', ''); $user = $credentials['user']; $certificate = $credentials['certificate']; $connection = $this->getConduit()->callMethodSynchronous( 'conduit.connect', array( 'client' => 'arc', 'clientVersion' => $this->getConduitVersion(), 'clientDescription' => php_uname('n').':'.$description, 'user' => $user, 'certificate' => $certificate, 'host' => $this->conduitURI, )); } catch (ConduitClientException $ex) { if ($ex->getErrorCode() == 'ERR-NO-CERTIFICATE' || $ex->getErrorCode() == 'ERR-INVALID-USER' || $ex->getErrorCode() == 'ERR-INVALID-AUTH') { $conduit_uri = $this->conduitURI; $message = phutil_console_format( "\n%s\n\n %s\n\n%s\n%s", pht('YOU NEED TO __INSTALL A CERTIFICATE__ TO LOGIN TO PHABRICATOR'), pht('To do this, run: **%s**', 'arc install-certificate'), pht("The server '%s' rejected your request:", $conduit_uri), $ex->getMessage()); throw new ArcanistUsageException($message); } else if ($ex->getErrorCode() == 'NEW-ARC-VERSION') { // Cleverly disguise this as being AWESOME!!! echo phutil_console_format("**%s**\n\n", pht('New Version Available!')); echo phutil_console_wrap($ex->getMessage()); echo "\n\n"; echo pht('In most cases, arc can be upgraded automatically.')."\n"; $ok = phutil_console_confirm( pht('Upgrade arc now?'), $default_no = false); if (!$ok) { throw $ex; } $root = dirname(phutil_get_library_root('arcanist')); chdir($root); $err = phutil_passthru('%s upgrade', $root.'/bin/arc'); if (!$err) { echo "\n".pht('Try running your arc command again.')."\n"; } exit(1); } else { throw $ex; } } $this->userName = $user; $this->userPHID = $connection['userPHID']; $this->conduitAuthenticated = true; return $this; } /** * @return bool True if conduit is authenticated, false otherwise. * @task conduit */ final protected function isConduitAuthenticated() { return (bool)$this->conduitAuthenticated; } /** * Override this to return true if your workflow requires a conduit channel. * Arc will build the channel for you before your workflow executes. This * implies that you only need an unauthenticated channel; if you need * authentication, override @{method:requiresAuthentication}. * * @return bool True if arc should build a conduit channel before running * the workflow. * @task conduit */ public function requiresConduit() { return false; } /** * Override this to return true if your workflow requires an authenticated * conduit channel. This implies that it requires a conduit. Arc will build * and authenticate the channel for you before the workflow executes. * * @return bool True if arc should build an authenticated conduit channel * before running the workflow. * @task conduit */ public function requiresAuthentication() { return false; } /** * Returns the PHID for the user once they've authenticated via Conduit. * * @return phid Authenticated user PHID. * @task conduit */ final public function getUserPHID() { if (!$this->userPHID) { $workflow = get_class($this); throw new Exception( pht( "This workflow ('%s') requires authentication, override ". "%s to return true.", $workflow, 'requiresAuthentication()')); } return $this->userPHID; } /** * Return the username for the user once they've authenticated via Conduit. * * @return string Authenticated username. * @task conduit */ final public function getUserName() { return $this->userName; } /** * Get the established @{class@libphutil:ConduitClient} in order to make * Conduit method calls. Before the client is available it must be connected, * either implicitly by making @{method:requireConduit} or * @{method:requireAuthentication} return true, or explicitly by calling * @{method:establishConduit} or @{method:authenticateConduit}. * * @return @{class@libphutil:ConduitClient} Live conduit client. * @task conduit */ final public function getConduit() { if (!$this->conduit) { $workflow = get_class($this); throw new Exception( pht( "This workflow ('%s') requires a Conduit, override ". "%s to return true.", $workflow, 'requiresConduit()')); } return $this->conduit; } final public function setArcanistConfiguration( ArcanistConfiguration $arcanist_configuration) { $this->arcanistConfiguration = $arcanist_configuration; return $this; } final public function getArcanistConfiguration() { return $this->arcanistConfiguration; } final public function setConfigurationManager( ArcanistConfigurationManager $arcanist_configuration_manager) { $this->configurationManager = $arcanist_configuration_manager; return $this; } final public function getConfigurationManager() { return $this->configurationManager; } public function requiresWorkingCopy() { return false; } public function desiresWorkingCopy() { return false; } public function requiresRepositoryAPI() { return false; } public function desiresRepositoryAPI() { return false; } final public function setCommand($command) { $this->command = $command; return $this; } final public function getCommand() { return $this->command; } public function getArguments() { return array(); } final public function setWorkingDirectory($working_directory) { $this->workingDirectory = $working_directory; return $this; } final public function getWorkingDirectory() { return $this->workingDirectory; } final private function setParentWorkflow($parent_workflow) { $this->parentWorkflow = $parent_workflow; return $this; } final protected function getParentWorkflow() { return $this->parentWorkflow; } final public function buildChildWorkflow($command, array $argv) { $arc_config = $this->getArcanistConfiguration(); $workflow = $arc_config->buildWorkflow($command); $workflow->setParentWorkflow($this); $workflow->setConduitEngine($this->getConduitEngine()); $workflow->setCommand($command); $workflow->setConfigurationManager($this->getConfigurationManager()); if ($this->repositoryAPI) { $workflow->setRepositoryAPI($this->repositoryAPI); } if ($this->userPHID) { $workflow->userPHID = $this->getUserPHID(); $workflow->userName = $this->getUserName(); } if ($this->conduit) { $workflow->conduit = $this->conduit; $workflow->setConduitCredentials($this->conduitCredentials); $workflow->conduitAuthenticated = $this->conduitAuthenticated; } $workflow->setArcanistConfiguration($arc_config); $workflow->parseArguments(array_values($argv)); return $workflow; } final public function getArgument($key, $default = null) { // TOOLSETS: Remove this legacy code. if (is_array($this->arguments)) { return idx($this->arguments, $key, $default); } return $this->arguments->getArg($key); } final public function getCompleteArgumentSpecification() { $spec = $this->getArguments(); $arc_config = $this->getArcanistConfiguration(); $command = $this->getCommand(); $spec += $arc_config->getCustomArgumentsForCommand($command); return $spec; } final public function parseArguments(array $args) { $spec = $this->getCompleteArgumentSpecification(); $dict = array(); $more_key = null; if (!empty($spec['*'])) { $more_key = $spec['*']; unset($spec['*']); $dict[$more_key] = array(); } $short_to_long_map = array(); foreach ($spec as $long => $options) { if (!empty($options['short'])) { $short_to_long_map[$options['short']] = $long; } } foreach ($spec as $long => $options) { if (!empty($options['repeat'])) { $dict[$long] = array(); } } $more = array(); $size = count($args); for ($ii = 0; $ii < $size; $ii++) { $arg = $args[$ii]; $arg_name = null; $arg_key = null; if ($arg == '--') { $more = array_merge( $more, array_slice($args, $ii + 1)); break; } else if (!strncmp($arg, '--', 2)) { $arg_key = substr($arg, 2); $parts = explode('=', $arg_key, 2); if (count($parts) == 2) { list($arg_key, $val) = $parts; array_splice($args, $ii, 1, array('--'.$arg_key, $val)); $size++; } if (!array_key_exists($arg_key, $spec)) { $corrected = PhutilArgumentSpellingCorrector::newFlagCorrector() ->correctSpelling($arg_key, array_keys($spec)); if (count($corrected) == 1) { PhutilConsole::getConsole()->writeErr( pht( "(Assuming '%s' is the British spelling of '%s'.)", '--'.$arg_key, '--'.head($corrected))."\n"); $arg_key = head($corrected); } else { throw new ArcanistUsageException( pht( "Unknown argument '%s'. Try '%s'.", $arg_key, 'arc help')); } } } else if (!strncmp($arg, '-', 1)) { $arg_key = substr($arg, 1); if (empty($short_to_long_map[$arg_key])) { throw new ArcanistUsageException( pht( "Unknown argument '%s'. Try '%s'.", $arg_key, 'arc help')); } $arg_key = $short_to_long_map[$arg_key]; } else { $more[] = $arg; continue; } $options = $spec[$arg_key]; if (empty($options['param'])) { $dict[$arg_key] = true; } else { if ($ii == $size - 1) { throw new ArcanistUsageException( pht( "Option '%s' requires a parameter.", $arg)); } if (!empty($options['repeat'])) { $dict[$arg_key][] = $args[$ii + 1]; } else { $dict[$arg_key] = $args[$ii + 1]; } $ii++; } } if ($more) { if ($more_key) { $dict[$more_key] = $more; } else { $example = reset($more); throw new ArcanistUsageException( pht( "Unrecognized argument '%s'. Try '%s'.", $example, 'arc help')); } } foreach ($dict as $key => $value) { if (empty($spec[$key]['conflicts'])) { continue; } foreach ($spec[$key]['conflicts'] as $conflict => $more) { if (isset($dict[$conflict])) { if ($more) { $more = ': '.$more; } else { $more = '.'; } // TODO: We'll always display these as long-form, when the user might // have typed them as short form. throw new ArcanistUsageException( pht( "Arguments '%s' and '%s' are mutually exclusive", "--{$key}", "--{$conflict}").$more); } } } $this->arguments = $dict; $this->didParseArguments(); return $this; } protected function didParseArguments() { // Override this to customize workflow argument behavior. } final public function getWorkingCopy() { $configuration_engine = $this->getConfigurationEngine(); // TOOLSETS: Remove this once all workflows are toolset workflows. if (!$configuration_engine) { throw new Exception( pht( 'This workflow has not yet been updated to Toolsets and can '. 'not retrieve a modern WorkingCopy object. Use '. '"getWorkingCopyIdentity()" to retrieve a previous-generation '. 'object.')); } return $configuration_engine->getWorkingCopy(); } final public function getWorkingCopyIdentity() { $configuration_engine = $this->getConfigurationEngine(); if ($configuration_engine) { $working_copy = $configuration_engine->getWorkingCopy(); $working_path = $working_copy->getWorkingDirectory(); return ArcanistWorkingCopyIdentity::newFromPath($working_path); } $working_copy = $this->getConfigurationManager()->getWorkingCopyIdentity(); if (!$working_copy) { $workflow = get_class($this); throw new Exception( pht( "This workflow ('%s') requires a working copy, override ". "%s to return true.", $workflow, 'requiresWorkingCopy()')); } return $working_copy; } final public function setRepositoryAPI($api) { $this->repositoryAPI = $api; return $this; } final public function hasRepositoryAPI() { try { return (bool)$this->getRepositoryAPI(); } catch (Exception $ex) { return false; } } final public function getRepositoryAPI() { $configuration_engine = $this->getConfigurationEngine(); if ($configuration_engine) { $working_copy = $configuration_engine->getWorkingCopy(); return $working_copy->getRepositoryAPI(); } if (!$this->repositoryAPI) { $workflow = get_class($this); throw new Exception( pht( "This workflow ('%s') requires a Repository API, override ". "%s to return true.", $workflow, 'requiresRepositoryAPI()')); } return $this->repositoryAPI; } final protected function shouldRequireCleanUntrackedFiles() { return empty($this->arguments['allow-untracked']); } final public function setCommitMode($mode) { $this->commitMode = $mode; return $this; } final public function finalizeWorkingCopy() { if ($this->stashed) { $api = $this->getRepositoryAPI(); $api->unstashChanges(); echo pht('Restored stashed changes to the working directory.')."\n"; } } final public function requireCleanWorkingCopy() { $api = $this->getRepositoryAPI(); $must_commit = array(); $working_copy_desc = phutil_console_format( " %s: __%s__\n\n", pht('Working copy'), $api->getPath()); // NOTE: this is a subversion-only concept. $incomplete = $api->getIncompleteChanges(); if ($incomplete) { throw new ArcanistUsageException( sprintf( "%s\n\n%s %s\n %s\n\n%s", pht( "You have incompletely checked out directories in this working ". "copy. Fix them before proceeding.'"), $working_copy_desc, pht('Incomplete directories in working copy:'), implode("\n ", $incomplete), pht( "You can fix these paths by running '%s' on them.", 'svn update'))); } $conflicts = $api->getMergeConflicts(); if ($conflicts) { throw new ArcanistUsageException( sprintf( "%s\n\n%s %s\n %s", pht( 'You have merge conflicts in this working copy. Resolve merge '. 'conflicts before proceeding.'), $working_copy_desc, pht('Conflicts in working copy:'), implode("\n ", $conflicts))); } $missing = $api->getMissingChanges(); if ($missing) { throw new ArcanistUsageException( sprintf( "%s\n\n%s %s\n %s\n", pht( 'You have missing files in this working copy. Revert or formally '. 'remove them (with `%s`) before proceeding.', 'svn rm'), $working_copy_desc, pht('Missing files in working copy:'), implode("\n ", $missing))); } $externals = $api->getDirtyExternalChanges(); // TODO: This state can exist in Subversion, but it is currently handled // elsewhere. It should probably be handled here, eventually. if ($api instanceof ArcanistSubversionAPI) { $externals = array(); } if ($externals) { $message = pht( '%s submodule(s) have uncommitted or untracked changes:', new PhutilNumber(count($externals))); $prompt = pht( 'Ignore the changes to these %s submodule(s) and continue?', new PhutilNumber(count($externals))); $list = id(new PhutilConsoleList()) ->setWrap(false) ->addItems($externals); id(new PhutilConsoleBlock()) ->addParagraph($message) ->addList($list) ->draw(); $ok = phutil_console_confirm($prompt, $default_no = false); if (!$ok) { throw new ArcanistUserAbortException(); } } $uncommitted = $api->getUncommittedChanges(); $unstaged = $api->getUnstagedChanges(); // We already dealt with externals. $unstaged = array_diff($unstaged, $externals); // We only want files which are purely uncommitted. $uncommitted = array_diff($uncommitted, $unstaged); $uncommitted = array_diff($uncommitted, $externals); $untracked = $api->getUntrackedChanges(); if (!$this->shouldRequireCleanUntrackedFiles()) { $untracked = array(); } if ($untracked) { echo sprintf( "%s\n\n%s", pht('You have untracked files in this working copy.'), $working_copy_desc); if ($api instanceof ArcanistGitAPI) { $hint = pht( '(To ignore these %s change(s), add them to "%s".)', phutil_count($untracked), '.git/info/exclude'); } else if ($api instanceof ArcanistSubversionAPI) { $hint = pht( '(To ignore these %s change(s), add them to "%s".)', phutil_count($untracked), 'svn:ignore'); } else if ($api instanceof ArcanistMercurialAPI) { $hint = pht( '(To ignore these %s change(s), add them to "%s".)', phutil_count($untracked), '.hgignore'); } $untracked_list = " ".implode("\n ", $untracked); echo sprintf( " %s\n %s\n%s", pht('Untracked changes in working copy:'), $hint, $untracked_list); $prompt = pht( 'Ignore these %s untracked file(s) and continue?', phutil_count($untracked)); if (!phutil_console_confirm($prompt)) { throw new ArcanistUserAbortException(); } } $should_commit = false; if ($unstaged || $uncommitted) { // NOTE: We're running this because it builds a cache and can take a // perceptible amount of time to arrive at an answer, but we don't want // to pause in the middle of printing the output below. $this->getShouldAmend(); echo sprintf( "%s\n\n%s", pht('You have uncommitted changes in this working copy.'), $working_copy_desc); $lists = array(); if ($unstaged) { $unstaged_list = " ".implode("\n ", $unstaged); $lists[] = sprintf( " %s\n%s", pht('Unstaged changes in working copy:'), $unstaged_list); } if ($uncommitted) { $uncommitted_list = " ".implode("\n ", $uncommitted); $lists[] = sprintf( "%s\n%s", pht('Uncommitted changes in working copy:'), $uncommitted_list); } echo implode("\n\n", $lists)."\n"; $all_uncommitted = array_merge($unstaged, $uncommitted); if ($this->askForAdd($all_uncommitted)) { if ($unstaged) { $api->addToCommit($unstaged); } $should_commit = true; } else { $permit_autostash = $this->getConfigFromAnySource('arc.autostash'); if ($permit_autostash && $api->canStashChanges()) { echo pht( 'Stashing uncommitted changes. (You can restore them with `%s`).', 'git stash pop')."\n"; $api->stashChanges(); $this->stashed = true; } else { throw new ArcanistUsageException( pht( 'You can not continue with uncommitted changes. '. 'Commit or discard them before proceeding.')); } } } if ($should_commit) { if ($this->getShouldAmend()) { $commit = head($api->getLocalCommitInformation()); $api->amendCommit($commit['message']); } else if ($api->supportsLocalCommits()) { $template = sprintf( "\n\n# %s\n#\n# %s\n#\n", pht('Enter a commit message.'), pht('Changes:')); $paths = array_merge($uncommitted, $unstaged); $paths = array_unique($paths); sort($paths); foreach ($paths as $path) { $template .= "# ".$path."\n"; } $commit_message = $this->newInteractiveEditor($template) ->setName(pht('commit-message')) ->editInteractively(); if ($commit_message === $template) { throw new ArcanistUsageException( pht('You must provide a commit message.')); } $commit_message = ArcanistCommentRemover::removeComments( $commit_message); if (!strlen($commit_message)) { throw new ArcanistUsageException( pht('You must provide a nonempty commit message.')); } $api->doCommit($commit_message); } } } private function getShouldAmend() { if ($this->shouldAmend === null) { $this->shouldAmend = $this->calculateShouldAmend(); } return $this->shouldAmend; } private function calculateShouldAmend() { $api = $this->getRepositoryAPI(); if ($this->isHistoryImmutable() || !$api->supportsAmend()) { return false; } $commits = $api->getLocalCommitInformation(); if (!$commits) { return false; } $commit = reset($commits); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( $commit['message']); if ($message->getGitSVNBaseRevision()) { return false; } if ($api->getAuthor() != $commit['author']) { return false; } if ($message->getRevisionID() && $this->getArgument('create')) { return false; } // TODO: Check commits since tracking branch. If empty then return false. // Don't amend the current commit if it has already been published. $repository = $this->loadProjectRepository(); if ($repository) { $repo_id = $repository['id']; $commit_hash = $commit['commit']; $callsign = idx($repository, 'callsign'); if ($callsign) { // The server might be too old to support the new style commit names, // so prefer the old way $commit_name = "r{$callsign}{$commit_hash}"; } else { $commit_name = "R{$repo_id}:{$commit_hash}"; } $result = $this->getConduit()->callMethodSynchronous( 'diffusion.querycommits', array('names' => array($commit_name))); $known_commit = idx($result['identifierMap'], $commit_name); if ($known_commit) { return false; } } if (!$message->getRevisionID()) { return true; } $in_working_copy = $api->loadWorkingCopyDifferentialRevisions( $this->getConduit(), array( 'authors' => array($this->getUserPHID()), 'status' => 'status-open', )); if ($in_working_copy) { return true; } return false; } private function askForAdd(array $files) { if ($this->commitMode == self::COMMIT_DISABLE) { return false; } if ($this->commitMode == self::COMMIT_ENABLE) { return true; } $prompt = $this->getAskForAddPrompt($files); return phutil_console_confirm($prompt); } private function getAskForAddPrompt(array $files) { if ($this->getShouldAmend()) { $prompt = pht( 'Do you want to amend these %s change(s) to the current commit?', phutil_count($files)); } else { $prompt = pht( 'Do you want to create a new commit with these %s change(s)?', phutil_count($files)); } return $prompt; } final protected function loadDiffBundleFromConduit( ConduitClient $conduit, $diff_id) { return $this->loadBundleFromConduit( $conduit, array( 'ids' => array($diff_id), )); } final protected function loadRevisionBundleFromConduit( ConduitClient $conduit, $revision_id) { return $this->loadBundleFromConduit( $conduit, array( 'revisionIDs' => array($revision_id), )); } final private function loadBundleFromConduit( ConduitClient $conduit, $params) { $future = $conduit->callMethod('differential.querydiffs', $params); $diff = head($future->resolve()); if ($diff == null) { throw new Exception( phutil_console_wrap( pht("The diff or revision you specified is either invalid or you ". "don't have permission to view it.")) ); } $changes = array(); foreach ($diff['changes'] as $changedict) { $changes[] = ArcanistDiffChange::newFromDictionary($changedict); } $bundle = ArcanistBundle::newFromChanges($changes); $bundle->setConduit($conduit); // since the conduit method has changes, assume that these fields // could be unset $bundle->setBaseRevision(idx($diff, 'sourceControlBaseRevision')); $bundle->setRevisionID(idx($diff, 'revisionID')); $bundle->setAuthorName(idx($diff, 'authorName')); $bundle->setAuthorEmail(idx($diff, 'authorEmail')); return $bundle; } /** * Return a list of lines changed by the current diff, or ##null## if the * change list is meaningless (for example, because the path is a directory * or binary file). * * @param string Path within the repository. * @param string Change selection mode (see ArcanistDiffHunk). * @return list|null List of changed line numbers, or null to indicate that * the path is not a line-oriented text file. */ final protected function getChangedLines($path, $mode) { $repository_api = $this->getRepositoryAPI(); $full_path = $repository_api->getPath($path); if (is_dir($full_path)) { return null; } if (!file_exists($full_path)) { return null; } $change = $this->getChange($path); if ($change->getFileType() !== ArcanistDiffChangeType::FILE_TEXT) { return null; } $lines = $change->getChangedLines($mode); return array_keys($lines); } final protected function getChange($path) { $repository_api = $this->getRepositoryAPI(); // TODO: Very gross $is_git = ($repository_api instanceof ArcanistGitAPI); $is_hg = ($repository_api instanceof ArcanistMercurialAPI); $is_svn = ($repository_api instanceof ArcanistSubversionAPI); if ($is_svn) { // NOTE: In SVN, we don't currently support a "get all local changes" // operation, so special case it. if (empty($this->changeCache[$path])) { $diff = $repository_api->getRawDiffText($path); $parser = $this->newDiffParser(); $changes = $parser->parseDiff($diff); if (count($changes) != 1) { throw new Exception(pht('Expected exactly one change.')); } $this->changeCache[$path] = reset($changes); } } else if ($is_git || $is_hg) { if (empty($this->changeCache)) { $changes = $repository_api->getAllLocalChanges(); foreach ($changes as $change) { $this->changeCache[$change->getCurrentPath()] = $change; } } } else { throw new Exception(pht('Missing VCS support.')); } if (empty($this->changeCache[$path])) { if ($is_git || $is_hg) { // This can legitimately occur under git/hg if you make a change, // "git/hg commit" it, and then revert the change in the working copy // and run "arc lint". $change = new ArcanistDiffChange(); $change->setCurrentPath($path); return $change; } else { throw new Exception( pht( "Trying to get change for unchanged path '%s'!", $path)); } } return $this->changeCache[$path]; } final public function willRunWorkflow() { $spec = $this->getCompleteArgumentSpecification(); foreach ($this->arguments as $arg => $value) { if (empty($spec[$arg])) { continue; } $options = $spec[$arg]; if (!empty($options['supports'])) { $system_name = $this->getRepositoryAPI()->getSourceControlSystemName(); if (!in_array($system_name, $options['supports'])) { $extended_info = null; if (!empty($options['nosupport'][$system_name])) { $extended_info = ' '.$options['nosupport'][$system_name]; } throw new ArcanistUsageException( pht( "Option '%s' is not supported under %s.", "--{$arg}", $system_name). $extended_info); } } } } final protected function normalizeRevisionID($revision_id) { return preg_replace('/^D/i', '', $revision_id); } protected function shouldShellComplete() { return true; } protected function getShellCompletions(array $argv) { return array(); } public function getSupportedRevisionControlSystems() { return array('git', 'hg', 'svn'); } final protected function getPassthruArgumentsAsMap($command) { $map = array(); foreach ($this->getCompleteArgumentSpecification() as $key => $spec) { if (!empty($spec['passthru'][$command])) { if (isset($this->arguments[$key])) { $map[$key] = $this->arguments[$key]; } } } return $map; } final protected function getPassthruArgumentsAsArgv($command) { $spec = $this->getCompleteArgumentSpecification(); $map = $this->getPassthruArgumentsAsMap($command); $argv = array(); foreach ($map as $key => $value) { $argv[] = '--'.$key; if (!empty($spec[$key]['param'])) { $argv[] = $value; } } return $argv; } /** * Write a message to stderr so that '--json' flags or stdout which is meant * to be piped somewhere aren't disrupted. * * @param string Message to write to stderr. * @return void */ final protected function writeStatusMessage($msg) { fwrite(STDERR, $msg); } final public function writeInfo($title, $message) { $this->writeStatusMessage( phutil_console_format( "** %s ** %s\n", $title, $message)); } final public function writeWarn($title, $message) { $this->writeStatusMessage( phutil_console_format( "** %s ** %s\n", $title, $message)); } final public function writeOkay($title, $message) { $this->writeStatusMessage( phutil_console_format( "** %s ** %s\n", $title, $message)); } final protected function isHistoryImmutable() { $repository_api = $this->getRepositoryAPI(); $config = $this->getConfigFromAnySource('history.immutable'); if ($config !== null) { return $config; } return $repository_api->isHistoryDefaultImmutable(); } /** * Workflows like 'lint' and 'unit' operate on a list of working copy paths. * The user can either specify the paths explicitly ("a.js b.php"), or by * specifying a revision ("--rev a3f10f1f") to select all paths modified * since that revision, or by omitting both and letting arc choose the * default relative revision. * * This method takes the user's selections and returns the paths that the * workflow should act upon. * * @param list List of explicitly provided paths. * @param string|null Revision name, if provided. * @param mask Mask of ArcanistRepositoryAPI flags to exclude. * Defaults to ArcanistRepositoryAPI::FLAG_UNTRACKED. * @return list List of paths the workflow should act on. */ final protected function selectPathsForWorkflow( array $paths, $rev, $omit_mask = null) { if ($omit_mask === null) { $omit_mask = ArcanistRepositoryAPI::FLAG_UNTRACKED; } if ($paths) { $working_copy = $this->getWorkingCopyIdentity(); foreach ($paths as $key => $path) { $full_path = Filesystem::resolvePath($path); if (!Filesystem::pathExists($full_path)) { throw new ArcanistUsageException( pht( "Path '%s' does not exist!", $path)); } $relative_path = Filesystem::readablePath( $full_path, $working_copy->getProjectRoot()); $paths[$key] = $relative_path; } } else { $repository_api = $this->getRepositoryAPI(); if ($rev) { $this->parseBaseCommitArgument(array($rev)); } $paths = $repository_api->getWorkingCopyStatus(); foreach ($paths as $path => $flags) { if ($flags & $omit_mask) { unset($paths[$path]); } } $paths = array_keys($paths); } return array_values($paths); } final protected function renderRevisionList(array $revisions) { $list = array(); foreach ($revisions as $revision) { $list[] = ' - D'.$revision['id'].': '.$revision['title']."\n"; } return implode('', $list); } /* -( Scratch Files )------------------------------------------------------ */ /** * Try to read a scratch file, if it exists and is readable. * * @param string Scratch file name. * @return mixed String for file contents, or false for failure. * @task scratch */ final protected function readScratchFile($path) { if (!$this->repositoryAPI) { return false; } return $this->getRepositoryAPI()->readScratchFile($path); } /** * Try to read a scratch JSON file, if it exists and is readable. * * @param string Scratch file name. * @return array Empty array for failure. * @task scratch */ final protected function readScratchJSONFile($path) { $file = $this->readScratchFile($path); if (!$file) { return array(); } return phutil_json_decode($file); } /** * Try to write a scratch file, if there's somewhere to put it and we can * write there. * * @param string Scratch file name to write. * @param string Data to write. * @return bool True on success, false on failure. * @task scratch */ final protected function writeScratchFile($path, $data) { if (!$this->repositoryAPI) { return false; } return $this->getRepositoryAPI()->writeScratchFile($path, $data); } /** * Try to write a scratch JSON file, if there's somewhere to put it and we can * write there. * * @param string Scratch file name to write. * @param array Data to write. * @return bool True on success, false on failure. * @task scratch */ final protected function writeScratchJSONFile($path, array $data) { return $this->writeScratchFile($path, json_encode($data)); } /** * Try to remove a scratch file. * * @param string Scratch file name to remove. * @return bool True if the file was removed successfully. * @task scratch */ final protected function removeScratchFile($path) { if (!$this->repositoryAPI) { return false; } return $this->getRepositoryAPI()->removeScratchFile($path); } /** * Get a human-readable description of the scratch file location. * * @param string Scratch file name. * @return mixed String, or false on failure. * @task scratch */ final protected function getReadableScratchFilePath($path) { if (!$this->repositoryAPI) { return false; } return $this->getRepositoryAPI()->getReadableScratchFilePath($path); } /** * Get the path to a scratch file, if possible. * * @param string Scratch file name. * @return mixed File path, or false on failure. * @task scratch */ final protected function getScratchFilePath($path) { if (!$this->repositoryAPI) { return false; } return $this->getRepositoryAPI()->getScratchFilePath($path); } final protected function getRepositoryEncoding() { return nonempty( idx($this->loadProjectRepository(), 'encoding'), 'UTF-8'); } final protected function loadProjectRepository() { list($info, $reasons) = $this->loadRepositoryInformation(); return coalesce($info, array()); } final protected function newInteractiveEditor($text) { $editor = new PhutilInteractiveEditor($text); $preferred = $this->getConfigFromAnySource('editor'); if ($preferred) { $editor->setPreferredEditor($preferred); } return $editor; } final protected function newDiffParser() { $parser = new ArcanistDiffParser(); if ($this->repositoryAPI) { $parser->setRepositoryAPI($this->getRepositoryAPI()); } $parser->setWriteDiffOnFailure(true); return $parser; } final protected function resolveCall(ConduitFuture $method) { try { return $method->resolve(); } catch (ConduitClientException $ex) { if ($ex->getErrorCode() == 'ERR-CONDUIT-CALL') { echo phutil_console_wrap( pht( 'This feature requires a newer version of Phabricator. Please '. 'update it using these instructions: %s', 'https://secure.phabricator.com/book/phabricator/article/'. 'upgrading/')."\n\n"); } throw $ex; } } final protected function dispatchEvent($type, array $data) { $data += array( 'workflow' => $this, ); $event = new PhutilEvent($type, $data); PhutilEventEngine::dispatchEvent($event); return $event; } final public function parseBaseCommitArgument(array $argv) { if (!count($argv)) { return; } $api = $this->getRepositoryAPI(); if (!$api->supportsCommitRanges()) { throw new ArcanistUsageException( pht('This version control system does not support commit ranges.')); } if (count($argv) > 1) { throw new ArcanistUsageException( pht( 'Specify exactly one base commit. The end of the commit range is '. 'always the working copy state.')); } $api->setBaseCommit(head($argv)); return $this; } final protected function getRepositoryVersion() { if (!$this->repositoryVersion) { $api = $this->getRepositoryAPI(); $commit = $api->getSourceControlBaseRevision(); $versions = array('' => $commit); foreach ($api->getChangedFiles($commit) as $path => $mask) { $versions[$path] = (Filesystem::pathExists($path) ? md5_file($path) : ''); } $this->repositoryVersion = md5(json_encode($versions)); } return $this->repositoryVersion; } /* -( Phabricator Repositories )------------------------------------------- */ /** * Get the PHID of the Phabricator repository this working copy corresponds * to. Returns `null` if no repository can be identified. * * @return phid|null Repository PHID, or null if no repository can be * identified. * * @task phabrep */ final protected function getRepositoryPHID() { return idx($this->getRepositoryInformation(), 'phid'); } /** * Get the name of the Phabricator repository this working copy * corresponds to. Returns `null` if no repository can be identified. * * @return string|null Repository name, or null if no repository can be * identified. * * @task phabrep */ final protected function getRepositoryName() { return idx($this->getRepositoryInformation(), 'name'); } /** * Get the URI of the Phabricator repository this working copy * corresponds to. Returns `null` if no repository can be identified. * * @return string|null Repository URI, or null if no repository can be * identified. * * @task phabrep */ final protected function getRepositoryURI() { return idx($this->getRepositoryInformation(), 'uri'); } final protected function getRepositoryStagingConfiguration() { return idx($this->getRepositoryInformation(), 'staging'); } /** * Get human-readable reasoning explaining how `arc` evaluated which * Phabricator repository corresponds to this working copy. Used by * `arc which` to explain the process to users. * * @return list Human-readable explanation of the repository * association process. * * @task phabrep */ final protected function getRepositoryReasons() { $this->getRepositoryInformation(); return $this->repositoryReasons; } /** * @task phabrep */ private function getRepositoryInformation() { if ($this->repositoryInfo === null) { list($info, $reasons) = $this->loadRepositoryInformation(); $this->repositoryInfo = nonempty($info, array()); $this->repositoryReasons = $reasons; } return $this->repositoryInfo; } /** * @task phabrep */ private function loadRepositoryInformation() { list($query, $reasons) = $this->getRepositoryQuery(); if (!$query) { return array(null, $reasons); } try { $method = 'repository.query'; $results = $this->getConduitEngine()->newCall($method, $query) ->resolve(); } catch (ConduitClientException $ex) { if ($ex->getErrorCode() == 'ERR-CONDUIT-CALL') { $reasons[] = pht( 'This version of Arcanist is more recent than the version of '. 'Phabricator you are connecting to: the Phabricator install is '. 'out of date and does not have support for identifying '. 'repositories by callsign or URI. Update Phabricator to enable '. 'these features.'); return array(null, $reasons); } throw $ex; } $result = null; if (!$results) { $reasons[] = pht( 'No repositories matched the query. Check that your configuration '. 'is correct, or use "%s" to select a repository explicitly.', 'repository.callsign'); } else if (count($results) > 1) { $reasons[] = pht( 'Multiple repostories (%s) matched the query. You can use the '. '"%s" configuration to select the one you want.', implode(', ', ipull($results, 'callsign')), 'repository.callsign'); } else { $result = head($results); $reasons[] = pht('Found a unique matching repository.'); } return array($result, $reasons); } /** * @task phabrep */ private function getRepositoryQuery() { $reasons = array(); $callsign = $this->getConfigFromAnySource('repository.callsign'); if ($callsign) { $query = array( 'callsigns' => array($callsign), ); $reasons[] = pht( 'Configuration value "%s" is set to "%s".', 'repository.callsign', $callsign); return array($query, $reasons); } else { $reasons[] = pht( 'Configuration value "%s" is empty.', 'repository.callsign'); } $uuid = $this->getRepositoryAPI()->getRepositoryUUID(); if ($uuid !== null) { $query = array( 'uuids' => array($uuid), ); $reasons[] = pht( 'The UUID for this working copy is "%s".', $uuid); return array($query, $reasons); } else { $reasons[] = pht( 'This repository has no VCS UUID (this is normal for git/hg).'); } $remote_uri = $this->getRepositoryAPI()->getRemoteURI(); if ($remote_uri !== null) { $query = array( 'remoteURIs' => array($remote_uri), ); $reasons[] = pht( 'The remote URI for this working copy is "%s".', $remote_uri); return array($query, $reasons); } else { $reasons[] = pht( 'Unable to determine the remote URI for this repository.'); } return array(null, $reasons); } /** * Build a new lint engine for the current working copy. * * Optionally, you can pass an explicit engine class name to build an engine * of a particular class. Normally this is used to implement an `--engine` * flag from the CLI. * * @param string Optional explicit engine class name. * @return ArcanistLintEngine Constructed engine. */ protected function newLintEngine($engine_class = null) { $working_copy = $this->getWorkingCopyIdentity(); $config = $this->getConfigurationManager(); if (!$engine_class) { $engine_class = $config->getConfigFromAnySource('lint.engine'); } if (!$engine_class) { if (Filesystem::pathExists($working_copy->getProjectPath('.arclint'))) { $engine_class = 'ArcanistConfigurationDrivenLintEngine'; } } if (!$engine_class) { throw new ArcanistNoEngineException( pht( "No lint engine is configured for this project. Create an '%s' ". "file, or configure an advanced engine with '%s' in '%s'.", '.arclint', 'lint.engine', '.arcconfig')); } $base_class = 'ArcanistLintEngine'; if (!class_exists($engine_class) || !is_subclass_of($engine_class, $base_class)) { throw new ArcanistUsageException( pht( 'Configured lint engine "%s" is not a subclass of "%s", but must be.', $engine_class, $base_class)); } $engine = newv($engine_class, array()) ->setWorkingCopy($working_copy) ->setConfigurationManager($config); return $engine; } /** * Build a new unit test engine for the current working copy. * * Optionally, you can pass an explicit engine class name to build an engine * of a particular class. Normally this is used to implement an `--engine` * flag from the CLI. * * @param string Optional explicit engine class name. * @return ArcanistUnitTestEngine Constructed engine. */ protected function newUnitTestEngine($engine_class = null) { $working_copy = $this->getWorkingCopyIdentity(); $config = $this->getConfigurationManager(); if (!$engine_class) { $engine_class = $config->getConfigFromAnySource('unit.engine'); } if (!$engine_class) { if (Filesystem::pathExists($working_copy->getProjectPath('.arcunit'))) { $engine_class = 'ArcanistConfigurationDrivenUnitTestEngine'; } } if (!$engine_class) { throw new ArcanistNoEngineException( pht( "No unit test engine is configured for this project. Create an ". "'%s' file, or configure an advanced engine with '%s' in '%s'.", '.arcunit', 'unit.engine', '.arcconfig')); } $base_class = 'ArcanistUnitTestEngine'; if (!class_exists($engine_class) || !is_subclass_of($engine_class, $base_class)) { throw new ArcanistUsageException( pht( 'Configured unit test engine "%s" is not a subclass of "%s", '. 'but must be.', $engine_class, $base_class)); } $engine = newv($engine_class, array()) ->setWorkingCopy($working_copy) ->setConfigurationManager($config); return $engine; } protected function openURIsInBrowser(array $uris) { $browser = $this->getBrowserCommand(); // The "browser" may actually be a list of arguments. if (!is_array($browser)) { $browser = array($browser); } foreach ($uris as $uri) { $err = phutil_passthru('%LR %R', $browser, $uri); if ($err) { throw new ArcanistUsageException( pht( 'Failed to open URI "%s" in browser ("%s"). '. 'Check your "browser" config option.', $uri, implode(' ', $browser))); } } } private function getBrowserCommand() { $config = $this->getConfigFromAnySource('browser'); if ($config) { return $config; } if (phutil_is_windows()) { // See T13504. We now use "bypass_shell", so "start" alone is no longer // a valid binary to invoke directly. return array( 'cmd', '/c', 'start', ); } $candidates = array( 'sensible-browser' => array('sensible-browser'), 'xdg-open' => array('xdg-open'), 'open' => array('open', '--'), ); // NOTE: The "open" command works well on OS X, but on many Linuxes "open" // exists and is not a browser. For now, we're just looking for other // commands first, but we might want to be smarter about selecting "open" // only on OS X. foreach ($candidates as $cmd => $argv) { if (Filesystem::binaryExists($cmd)) { return $argv; } } throw new ArcanistUsageException( pht( "Unable to find a browser command to run. Set '%s' in your ". "Arcanist config to specify a command to use.", 'browser')); } /** * Ask Phabricator to update the current repository as soon as possible. * * Calling this method after pushing commits allows Phabricator to discover * the commits more quickly, so the system overall is more responsive. * * @return void */ protected function askForRepositoryUpdate() { // If we know which repository we're in, try to tell Phabricator that we // pushed commits to it so it can update. This hint can help pull updates // more quickly, especially in rarely-used repositories. if ($this->getRepositoryPHID()) { try { $this->getConduit()->callMethodSynchronous( 'diffusion.looksoon', array( 'repositories' => array($this->getRepositoryPHID()), )); } catch (ConduitClientException $ex) { // If we hit an exception, just ignore it. Likely, we are running // against a Phabricator which is too old to support this method. // Since this hint is purely advisory, it doesn't matter if it has // no effect. } } } protected function getModernLintDictionary(array $map) { $map = $this->getModernCommonDictionary($map); return $map; } protected function getModernUnitDictionary(array $map) { $map = $this->getModernCommonDictionary($map); $details = idx($map, 'userData'); if (strlen($details)) { $map['details'] = (string)$details; } unset($map['userData']); return $map; } private function getModernCommonDictionary(array $map) { foreach ($map as $key => $value) { if ($value === null) { unset($map[$key]); } } return $map; } final public function setConduitEngine( ArcanistConduitEngine $conduit_engine) { $this->conduitEngine = $conduit_engine; return $this; } final public function getConduitEngine() { return $this->conduitEngine; } final public function getRepositoryRef() { $configuration_engine = $this->getConfigurationEngine(); if ($configuration_engine) { // This is a toolset workflow and can always build a repository ref. } else { if (!$this->getConfigurationManager()->getWorkingCopyIdentity()) { return null; } if (!$this->repositoryAPI) { return null; } } if (!$this->repositoryRef) { $ref = id(new ArcanistRepositoryRef()) ->setPHID($this->getRepositoryPHID()) ->setBrowseURI($this->getRepositoryURI()); $this->repositoryRef = $ref; } return $this->repositoryRef; } final public function getToolsetKey() { return $this->getToolset()->getToolsetKey(); } final public function getConfig($key) { return $this->getConfigurationSourceList()->getConfig($key); } public function canHandleSignal($signo) { return false; } public function handleSignal($signo) { return; } final public function newCommand(PhutilExecutableFuture $future) { return id(new ArcanistCommand()) ->setLogEngine($this->getLogEngine()) ->setExecutableFuture($future); } final public function loadHardpoints( $objects, $requests) { return $this->getRuntime()->loadHardpoints($objects, $requests); } protected function newPrompts() { return array(); } protected function newPrompt($key) { return id(new ArcanistPrompt()) ->setWorkflow($this) ->setKey($key); } public function hasPrompt($key) { $map = $this->getPromptMap(); return isset($map[$key]); } public function getPromptMap() { if ($this->promptMap === null) { $prompts = $this->newPrompts(); assert_instances_of($prompts, 'ArcanistPrompt'); + $prompts[] = $this->newPrompt('arc.state.stash') + ->setDescription( + pht( + 'Prompts the user to stash changes and continue when the '. + 'working copy has untracked, uncommitted. or unstaged '. + 'changes.')); + + // TODO: Swap to ArrayCheck? + $map = array(); foreach ($prompts as $prompt) { $key = $prompt->getKey(); if (isset($map[$key])) { throw new Exception( pht( 'Workflow ("%s") generates two prompts with the same '. 'key ("%s"). Each prompt a workflow generates must have a '. 'unique key.', get_class($this), $key)); } $map[$key] = $prompt; } $this->promptMap = $map; } return $this->promptMap; } - protected function getPrompt($key) { + final public function getPrompt($key) { $map = $this->getPromptMap(); $prompt = idx($map, $key); if (!$prompt) { throw new Exception( pht( 'Workflow ("%s") is requesting a prompt ("%s") but it did not '. 'generate any prompt with that name in "newPrompts()".', get_class($this), $key)); } return clone $prompt; } final protected function getSymbolEngine() { return $this->getRuntime()->getSymbolEngine(); } final protected function getViewer() { return $this->getRuntime()->getViewer(); } final protected function readStdin() { $log = $this->getLogEngine(); $log->writeWaitingForInput(); // NOTE: We can't just "file_get_contents()" here because signals don't // interrupt it. If the user types "^C", we want to interrupt the read. $raw_handle = fopen('php://stdin', 'rb'); $stdin = new PhutilSocketChannel($raw_handle); while ($stdin->update()) { PhutilChannel::waitForAny(array($stdin)); } return $stdin->read(); } protected function getAbsoluteURI($raw_uri) { // TODO: "ArcanistRevisionRef", at least, may return a relative URI. // If we get a relative URI, guess the correct absolute URI based on // the Conduit URI. This might not be correct for Conduit over SSH. $raw_uri = new PhutilURI($raw_uri); if (!strlen($raw_uri->getDomain())) { $base_uri = $this->getConduitEngine() ->getConduitURI(); $raw_uri = id(new PhutilURI($base_uri)) ->setPath($raw_uri->getPath()); } $raw_uri = phutil_string_cast($raw_uri); return $raw_uri; } }