diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a9d9e0ea..6b04b743 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,391 +1,388 @@ 2, 'class' => array( 'ArcanistAliasWorkflow' => 'workflow/ArcanistAliasWorkflow.php', 'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php', 'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php', 'ArcanistBackoutWorkflow' => 'workflow/ArcanistBackoutWorkflow.php', 'ArcanistBaseCommitParser' => 'parser/ArcanistBaseCommitParser.php', 'ArcanistBaseCommitParserTestCase' => 'parser/__tests__/ArcanistBaseCommitParserTestCase.php', 'ArcanistBaseUnitTestEngine' => 'unit/engine/ArcanistBaseUnitTestEngine.php', 'ArcanistBaseWorkflow' => 'workflow/ArcanistBaseWorkflow.php', 'ArcanistBaseXHPASTLinter' => 'lint/linter/ArcanistBaseXHPASTLinter.php', 'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php', 'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php', 'ArcanistBritishTestCase' => 'configuration/__tests__/ArcanistBritishTestCase.php', 'ArcanistBrowseWorkflow' => 'workflow/ArcanistBrowseWorkflow.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', 'ArcanistCapabilityNotSupportedException' => 'workflow/exception/ArcanistCapabilityNotSupportedException.php', 'ArcanistCheckstyleXMLLintRenderer' => 'lint/renderer/ArcanistCheckstyleXMLLintRenderer.php', 'ArcanistChmodLinter' => 'lint/linter/ArcanistChmodLinter.php', 'ArcanistChmodLinterTestCase' => 'lint/linter/__tests__/ArcanistChmodLinterTestCase.php', 'ArcanistCloseRevisionWorkflow' => 'workflow/ArcanistCloseRevisionWorkflow.php', 'ArcanistCloseWorkflow' => 'workflow/ArcanistCloseWorkflow.php', 'ArcanistClosureLinter' => 'lint/linter/ArcanistClosureLinter.php', 'ArcanistClosureLinterTestCase' => 'lint/linter/__tests__/ArcanistClosureLinterTestCase.php', 'ArcanistCoffeeLintLinter' => 'lint/linter/ArcanistCoffeeLintLinter.php', 'ArcanistCoffeeLintLinterTestCase' => 'lint/linter/__tests__/ArcanistCoffeeLintLinterTestCase.php', 'ArcanistCommentRemover' => 'parser/ArcanistCommentRemover.php', 'ArcanistCommentRemoverTestCase' => 'parser/__tests__/ArcanistCommentRemoverTestCase.php', 'ArcanistCommitWorkflow' => 'workflow/ArcanistCommitWorkflow.php', 'ArcanistCompilerLintRenderer' => 'lint/renderer/ArcanistCompilerLintRenderer.php', 'ArcanistComprehensiveLintEngine' => 'lint/engine/ArcanistComprehensiveLintEngine.php', 'ArcanistConduitLinter' => 'lint/linter/ArcanistConduitLinter.php', 'ArcanistConfiguration' => 'configuration/ArcanistConfiguration.php', 'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php', 'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php', 'ArcanistConsoleLintRenderer' => 'lint/renderer/ArcanistConsoleLintRenderer.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', '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', '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', 'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php', 'ArcanistEventType' => 'events/constant/ArcanistEventType.php', 'ArcanistExportWorkflow' => 'workflow/ArcanistExportWorkflow.php', 'ArcanistExternalLinter' => 'lint/linter/ArcanistExternalLinter.php', 'ArcanistExternalLinterTestCase' => 'lint/linter/__tests__/ArcanistExternalLinterTestCase.php', 'ArcanistFeatureWorkflow' => 'workflow/ArcanistFeatureWorkflow.php', 'ArcanistFilenameLinter' => 'lint/linter/ArcanistFilenameLinter.php', 'ArcanistFilenameLinterTestCase' => 'lint/linter/__tests__/ArcanistFilenameLinterTestCase.php', 'ArcanistFlagWorkflow' => 'workflow/ArcanistFlagWorkflow.php', 'ArcanistFlake8Linter' => 'lint/linter/ArcanistFlake8Linter.php', 'ArcanistFlake8LinterTestCase' => 'lint/linter/__tests__/ArcanistFlake8LinterTestCase.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', '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', 'ArcanistHelpWorkflow' => 'workflow/ArcanistHelpWorkflow.php', 'ArcanistHgClientChannel' => 'hgdaemon/ArcanistHgClientChannel.php', 'ArcanistHgProxyClient' => 'hgdaemon/ArcanistHgProxyClient.php', 'ArcanistHgProxyServer' => 'hgdaemon/ArcanistHgProxyServer.php', 'ArcanistHgServerChannel' => 'hgdaemon/ArcanistHgServerChannel.php', - 'ArcanistHookAPI' => 'repository/hookapi/ArcanistHookAPI.php', 'ArcanistInstallCertificateWorkflow' => 'workflow/ArcanistInstallCertificateWorkflow.php', 'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php', 'ArcanistJSHintLinterTestCase' => 'lint/linter/__tests__/ArcanistJSHintLinterTestCase.php', 'ArcanistJSONLintLinter' => 'lint/linter/ArcanistJSONLintLinter.php', 'ArcanistJSONLintLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLintLinterTestCase.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', 'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php', 'ArcanistLesscLinter' => 'lint/linter/ArcanistLesscLinter.php', 'ArcanistLesscLinterTestCase' => 'lint/linter/__tests__/ArcanistLesscLinterTestCase.php', 'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php', 'ArcanistLibraryTestCase' => '__tests__/ArcanistLibraryTestCase.php', 'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php', 'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php', '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', 'ArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistLinterTestCase.php', 'ArcanistLintersWorkflow' => 'workflow/ArcanistLintersWorkflow.php', 'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php', 'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php', 'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php', 'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php', 'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php', 'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php', 'ArcanistMissingLinterException' => 'lint/linter/exception/ArcanistMissingLinterException.php', 'ArcanistNoEffectException' => 'exception/usage/ArcanistNoEffectException.php', 'ArcanistNoEngineException' => 'exception/usage/ArcanistNoEngineException.php', 'ArcanistNoLintLinter' => 'lint/linter/ArcanistNoLintLinter.php', 'ArcanistNoLintLinterTestCase' => 'lint/linter/__tests__/ArcanistNoLintLinterTestCase.php', 'ArcanistNoneLintRenderer' => 'lint/renderer/ArcanistNoneLintRenderer.php', 'ArcanistPEP8Linter' => 'lint/linter/ArcanistPEP8Linter.php', 'ArcanistPEP8LinterTestCase' => 'lint/linter/__tests__/ArcanistPEP8LinterTestCase.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', 'ArcanistPhrequentWorkflow' => 'workflow/ArcanistPhrequentWorkflow.php', 'ArcanistPhutilLibraryLinter' => 'lint/linter/ArcanistPhutilLibraryLinter.php', 'ArcanistPhutilTestCase' => 'unit/engine/phutil/ArcanistPhutilTestCase.php', 'ArcanistPhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php', 'ArcanistPhutilTestSkippedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php', 'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestTerminatedException.php', 'ArcanistPhutilXHPASTLinter' => 'lint/linter/ArcanistPhutilXHPASTLinter.php', 'ArcanistPhutilXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistPhutilXHPASTLinterTestCase.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', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', 'ArcanistRevertWorkflow' => 'workflow/ArcanistRevertWorkflow.php', 'ArcanistRuboCopLinter' => 'lint/linter/ArcanistRuboCopLinter.php', 'ArcanistRuboCopLinterTestCase' => 'lint/linter/__tests__/ArcanistRuboCopLinterTestCase.php', 'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php', 'ArcanistRubyLinterTestCase' => 'lint/linter/__tests__/ArcanistRubyLinterTestCase.php', 'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php', 'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php', 'ArcanistSettings' => 'configuration/ArcanistSettings.php', 'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php', 'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php', 'ArcanistSpellingLinter' => 'lint/linter/ArcanistSpellingLinter.php', 'ArcanistSpellingLinterTestCase' => 'lint/linter/__tests__/ArcanistSpellingLinterTestCase.php', 'ArcanistStartWorkflow' => 'workflow/ArcanistStartWorkflow.php', 'ArcanistStopWorkflow' => 'workflow/ArcanistStopWorkflow.php', 'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php', - 'ArcanistSubversionHookAPI' => 'repository/hookapi/ArcanistSubversionHookAPI.php', 'ArcanistSummaryLintRenderer' => 'lint/renderer/ArcanistSummaryLintRenderer.php', 'ArcanistTasksWorkflow' => 'workflow/ArcanistTasksWorkflow.php', 'ArcanistTestCase' => 'infrastructure/testing/ArcanistTestCase.php', 'ArcanistTestResultParser' => 'unit/parser/ArcanistTestResultParser.php', 'ArcanistTestXHPASTLintSwitchHook' => 'lint/linter/__tests__/ArcanistTestXHPASTLintSwitchHook.php', 'ArcanistTextLinter' => 'lint/linter/ArcanistTextLinter.php', 'ArcanistTextLinterTestCase' => 'lint/linter/__tests__/ArcanistTextLinterTestCase.php', 'ArcanistTimeWorkflow' => 'workflow/ArcanistTimeWorkflow.php', 'ArcanistTodoWorkflow' => 'workflow/ArcanistTodoWorkflow.php', 'ArcanistUSEnglishTranslation' => 'internationalization/ArcanistUSEnglishTranslation.php', 'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.php', 'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php', 'ArcanistUnitTestEngine' => 'unit/engine/ArcanistUnitTestEngine.php', 'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php', 'ArcanistUnitTestableLintEngine' => 'lint/engine/ArcanistUnitTestableLintEngine.php', 'ArcanistUnitWorkflow' => 'workflow/ArcanistUnitWorkflow.php', 'ArcanistUpgradeWorkflow' => 'workflow/ArcanistUpgradeWorkflow.php', 'ArcanistUploadWorkflow' => 'workflow/ArcanistUploadWorkflow.php', 'ArcanistUsageException' => 'exception/ArcanistUsageException.php', 'ArcanistUserAbortException' => 'exception/usage/ArcanistUserAbortException.php', 'ArcanistVersionWorkflow' => 'workflow/ArcanistVersionWorkflow.php', 'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php', 'ArcanistWorkflow' => 'workflow/ArcanistWorkflow.php', 'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.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', 'ArcanistXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php', 'ArcanistXMLLinter' => 'lint/linter/ArcanistXMLLinter.php', 'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php', 'ArcanistXUnitTestResultParser' => 'unit/parser/ArcanistXUnitTestResultParser.php', 'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php', 'NoseTestEngine' => 'unit/engine/NoseTestEngine.php', 'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php', 'PhpunitTestEngineTestCase' => 'unit/engine/__tests__/PhpunitTestEngineTestCase.php', 'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php', 'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php', 'PytestTestEngine' => 'unit/engine/PytestTestEngine.php', 'XUnitTestEngine' => 'unit/engine/XUnitTestEngine.php', 'XUnitTestResultParserTestCase' => 'unit/parser/__tests__/XUnitTestResultParserTestCase.php', ), 'function' => array(), 'xmap' => array( 'ArcanistAliasWorkflow' => 'ArcanistWorkflow', 'ArcanistAmendWorkflow' => 'ArcanistWorkflow', 'ArcanistAnoidWorkflow' => 'ArcanistWorkflow', 'ArcanistBackoutWorkflow' => 'ArcanistWorkflow', 'ArcanistBaseCommitParserTestCase' => 'ArcanistTestCase', 'ArcanistBaseUnitTestEngine' => 'ArcanistUnitTestEngine', 'ArcanistBaseWorkflow' => 'ArcanistWorkflow', 'ArcanistBaseXHPASTLinter' => 'ArcanistFutureLinter', 'ArcanistBookmarkWorkflow' => 'ArcanistFeatureWorkflow', 'ArcanistBranchWorkflow' => 'ArcanistFeatureWorkflow', 'ArcanistBritishTestCase' => 'ArcanistTestCase', 'ArcanistBrowseWorkflow' => 'ArcanistWorkflow', 'ArcanistBundleTestCase' => 'ArcanistTestCase', 'ArcanistCSSLintLinter' => 'ArcanistExternalLinter', 'ArcanistCSSLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCSharpLinter' => 'ArcanistLinter', 'ArcanistCallConduitWorkflow' => 'ArcanistWorkflow', 'ArcanistCapabilityNotSupportedException' => 'Exception', 'ArcanistCheckstyleXMLLintRenderer' => 'ArcanistLintRenderer', 'ArcanistChmodLinter' => 'ArcanistLinter', 'ArcanistChmodLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistCloseRevisionWorkflow' => 'ArcanistWorkflow', 'ArcanistCloseWorkflow' => 'ArcanistWorkflow', 'ArcanistClosureLinter' => 'ArcanistExternalLinter', 'ArcanistClosureLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCoffeeLintLinter' => 'ArcanistExternalLinter', 'ArcanistCoffeeLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCommentRemoverTestCase' => 'ArcanistTestCase', 'ArcanistCommitWorkflow' => 'ArcanistWorkflow', 'ArcanistCompilerLintRenderer' => 'ArcanistLintRenderer', 'ArcanistComprehensiveLintEngine' => 'ArcanistLintEngine', 'ArcanistConduitLinter' => 'ArcanistLinter', 'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine', 'ArcanistConsoleLintRenderer' => 'ArcanistLintRenderer', 'ArcanistCoverWorkflow' => 'ArcanistWorkflow', 'ArcanistCppcheckLinter' => 'ArcanistExternalLinter', 'ArcanistCppcheckLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCpplintLinter' => 'ArcanistExternalLinter', 'ArcanistCpplintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistDiffParserTestCase' => 'ArcanistTestCase', 'ArcanistDiffUtilsTestCase' => 'ArcanistTestCase', 'ArcanistDiffWorkflow' => 'ArcanistWorkflow', 'ArcanistDifferentialCommitMessageParserException' => 'Exception', 'ArcanistDifferentialDependencyGraph' => 'AbstractDirectedGraph', 'ArcanistDownloadWorkflow' => 'ArcanistWorkflow', 'ArcanistEventType' => 'PhutilEventType', 'ArcanistExportWorkflow' => 'ArcanistWorkflow', 'ArcanistExternalLinter' => 'ArcanistFutureLinter', 'ArcanistExternalLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistFeatureWorkflow' => 'ArcanistWorkflow', 'ArcanistFilenameLinter' => 'ArcanistLinter', 'ArcanistFilenameLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistFlagWorkflow' => 'ArcanistWorkflow', 'ArcanistFlake8Linter' => 'ArcanistExternalLinter', 'ArcanistFlake8LinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistFutureLinter' => 'ArcanistLinter', 'ArcanistGeneratedLinter' => 'ArcanistLinter', 'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistGetConfigWorkflow' => 'ArcanistWorkflow', 'ArcanistGitAPI' => 'ArcanistRepositoryAPI', 'ArcanistGoLintLinter' => 'ArcanistExternalLinter', 'ArcanistGoLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistGoTestResultParser' => 'ArcanistTestResultParser', 'ArcanistGoTestResultParserTestCase' => 'ArcanistTestCase', 'ArcanistHLintLinter' => 'ArcanistExternalLinter', 'ArcanistHLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistHelpWorkflow' => 'ArcanistWorkflow', 'ArcanistHgClientChannel' => 'PhutilProtocolChannel', 'ArcanistHgServerChannel' => 'PhutilProtocolChannel', 'ArcanistInstallCertificateWorkflow' => 'ArcanistWorkflow', 'ArcanistJSHintLinter' => 'ArcanistExternalLinter', 'ArcanistJSHintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistJSONLintLinter' => 'ArcanistExternalLinter', 'ArcanistJSONLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistJSONLintRenderer' => 'ArcanistLintRenderer', 'ArcanistJSONLinter' => 'ArcanistLinter', 'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistJscsLinter' => 'ArcanistExternalLinter', 'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistLandWorkflow' => 'ArcanistWorkflow', 'ArcanistLesscLinter' => 'ArcanistExternalLinter', 'ArcanistLesscLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistLiberateWorkflow' => 'ArcanistWorkflow', 'ArcanistLibraryTestCase' => 'PhutilLibraryTestCase', 'ArcanistLintWorkflow' => 'ArcanistWorkflow', 'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase', 'ArcanistLintersWorkflow' => 'ArcanistWorkflow', 'ArcanistListWorkflow' => 'ArcanistWorkflow', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', 'ArcanistMercurialParserTestCase' => 'ArcanistTestCase', 'ArcanistMergeConflictLinter' => 'ArcanistLinter', 'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistMissingLinterException' => 'Exception', 'ArcanistNoEffectException' => 'ArcanistUsageException', 'ArcanistNoEngineException' => 'ArcanistUsageException', 'ArcanistNoLintLinter' => 'ArcanistLinter', 'ArcanistNoLintLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistNoneLintRenderer' => 'ArcanistLintRenderer', 'ArcanistPEP8Linter' => 'ArcanistExternalLinter', 'ArcanistPEP8LinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPasteWorkflow' => 'ArcanistWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistWorkflow', 'ArcanistPhpLinter' => 'ArcanistExternalLinter', 'ArcanistPhpLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPhpcsLinter' => 'ArcanistExternalLinter', 'ArcanistPhpcsLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPhpunitTestResultParser' => 'ArcanistTestResultParser', 'ArcanistPhrequentWorkflow' => 'ArcanistWorkflow', 'ArcanistPhutilLibraryLinter' => 'ArcanistLinter', 'ArcanistPhutilTestCaseTestCase' => 'ArcanistPhutilTestCase', 'ArcanistPhutilTestSkippedException' => 'Exception', 'ArcanistPhutilTestTerminatedException' => 'Exception', 'ArcanistPhutilXHPASTLinter' => 'ArcanistBaseXHPASTLinter', 'ArcanistPhutilXHPASTLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistPuppetLintLinter' => 'ArcanistExternalLinter', 'ArcanistPuppetLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPyFlakesLinter' => 'ArcanistExternalLinter', 'ArcanistPyFlakesLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPyLintLinter' => 'ArcanistLinter', 'ArcanistPyLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRepositoryAPIMiscTestCase' => 'ArcanistTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'ArcanistTestCase', 'ArcanistRevertWorkflow' => 'ArcanistWorkflow', 'ArcanistRuboCopLinter' => 'ArcanistExternalLinter', 'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRubyLinter' => 'ArcanistExternalLinter', 'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistScriptAndRegexLinter' => 'ArcanistLinter', 'ArcanistSetConfigWorkflow' => 'ArcanistWorkflow', 'ArcanistShellCompleteWorkflow' => 'ArcanistWorkflow', 'ArcanistSingleLintEngine' => 'ArcanistLintEngine', 'ArcanistSpellingLinter' => 'ArcanistLinter', 'ArcanistSpellingLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistStartWorkflow' => 'ArcanistPhrequentWorkflow', 'ArcanistStopWorkflow' => 'ArcanistPhrequentWorkflow', 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', - 'ArcanistSubversionHookAPI' => 'ArcanistHookAPI', 'ArcanistSummaryLintRenderer' => 'ArcanistLintRenderer', 'ArcanistTasksWorkflow' => 'ArcanistWorkflow', 'ArcanistTestCase' => 'ArcanistPhutilTestCase', 'ArcanistTestXHPASTLintSwitchHook' => 'ArcanistXHPASTLintSwitchHook', 'ArcanistTextLinter' => 'ArcanistLinter', 'ArcanistTextLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistTimeWorkflow' => 'ArcanistPhrequentWorkflow', 'ArcanistTodoWorkflow' => 'ArcanistWorkflow', 'ArcanistUSEnglishTranslation' => 'PhutilTranslation', 'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer', 'ArcanistUnitTestableLintEngine' => 'ArcanistLintEngine', 'ArcanistUnitWorkflow' => 'ArcanistWorkflow', 'ArcanistUpgradeWorkflow' => 'ArcanistWorkflow', 'ArcanistUploadWorkflow' => 'ArcanistWorkflow', 'ArcanistUsageException' => 'Exception', 'ArcanistUserAbortException' => 'ArcanistUsageException', 'ArcanistVersionWorkflow' => 'ArcanistWorkflow', 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', 'ArcanistWorkflow' => 'Phobject', 'ArcanistXHPASTLintNamingHookTestCase' => 'ArcanistTestCase', 'ArcanistXHPASTLinter' => 'ArcanistBaseXHPASTLinter', 'ArcanistXHPASTLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistXMLLinter' => 'ArcanistLinter', 'ArcanistXMLLinterTestCase' => 'ArcanistLinterTestCase', 'CSharpToolsTestEngine' => 'XUnitTestEngine', 'NoseTestEngine' => 'ArcanistUnitTestEngine', 'PhpunitTestEngine' => 'ArcanistUnitTestEngine', 'PhpunitTestEngineTestCase' => 'ArcanistTestCase', 'PhutilUnitTestEngine' => 'ArcanistUnitTestEngine', 'PhutilUnitTestEngineTestCase' => 'ArcanistTestCase', 'PytestTestEngine' => 'ArcanistUnitTestEngine', 'XUnitTestEngine' => 'ArcanistUnitTestEngine', 'XUnitTestResultParserTestCase' => 'ArcanistTestCase', ), )); diff --git a/src/lint/engine/ArcanistLintEngine.php b/src/lint/engine/ArcanistLintEngine.php index 8a4d1212..29cf13d6 100644 --- a/src/lint/engine/ArcanistLintEngine.php +++ b/src/lint/engine/ArcanistLintEngine.php @@ -1,654 +1,610 @@ configurationManager = $configuration_manager; return $this; } final public function getConfigurationManager() { return $this->configurationManager; } final public function setWorkingCopy( ArcanistWorkingCopyIdentity $working_copy) { $this->workingCopy = $working_copy; return $this; } final public function getWorkingCopy() { return $this->workingCopy; } final public function setPaths($paths) { $this->paths = $paths; return $this; } public function getPaths() { return $this->paths; } final public function setPathChangedLines($path, $changed) { if ($changed === null) { $this->changedLines[$path] = null; } else { $this->changedLines[$path] = array_fill_keys($changed, true); } return $this; } final public function getPathChangedLines($path) { return idx($this->changedLines, $path); } final public function setFileData($data) { $this->fileData = $data + $this->fileData; return $this; } - final public function setCommitHookMode($mode) { - $this->commitHookMode = $mode; - return $this; - } - - final public function setHookAPI(ArcanistHookAPI $hook_api) { - $this->hookAPI = $hook_api; - return $this; - } - - final public function getHookAPI() { - return $this->hookAPI; - } - final public function setEnableAsyncLint($enable_async_lint) { $this->enableAsyncLint = $enable_async_lint; return $this; } final public function getEnableAsyncLint() { return $this->enableAsyncLint; } final public function loadData($path) { if (!isset($this->fileData[$path])) { - if ($this->getCommitHookMode()) { - $this->fileData[$path] = $this->getHookAPI() - ->getCurrentFileData($path); - } else { $disk_path = $this->getFilePathOnDisk($path); $this->fileData[$path] = Filesystem::readFile($disk_path); - } } return $this->fileData[$path]; } public function pathExists($path) { - if ($this->getCommitHookMode()) { - $file_data = $this->loadData($path); - return ($file_data !== null); - } else { - $disk_path = $this->getFilePathOnDisk($path); - return Filesystem::pathExists($disk_path); - } + $disk_path = $this->getFilePathOnDisk($path); + return Filesystem::pathExists($disk_path); } final public function isDirectory($path) { - if ($this->getCommitHookMode()) { - // TODO: This won't get the right result in every case (we need more - // metadata) but should almost always be correct. - try { - $this->loadData($path); - return false; - } catch (Exception $ex) { - return true; - } - } else { - $disk_path = $this->getFilePathOnDisk($path); - return is_dir($disk_path); - } + $disk_path = $this->getFilePathOnDisk($path); + return is_dir($disk_path); } final public function isBinaryFile($path) { try { $data = $this->loadData($path); } catch (Exception $ex) { return false; } return ArcanistDiffUtils::isHeuristicBinaryFile($data); } final public function isSymbolicLink($path) { return is_link($this->getFilePathOnDisk($path)); } final public function getFilePathOnDisk($path) { return Filesystem::resolvePath( $path, $this->getWorkingCopy()->getProjectRoot()); } final public function setMinimumSeverity($severity) { $this->minimumSeverity = $severity; return $this; } - final public function getCommitHookMode() { - return $this->commitHookMode; - } - final public function run() { $linters = $this->buildLinters(); if (!$linters) { throw new ArcanistNoEffectException('No linters to run.'); } foreach ($linters as $key => $linter) { $linter->setLinterID($key); } $linters = msort($linters, 'getLinterPriority'); foreach ($linters as $linter) { $linter->setEngine($this); } $have_paths = false; foreach ($linters as $linter) { if ($linter->getPaths()) { $have_paths = true; break; } } if (!$have_paths) { throw new ArcanistNoEffectException('No paths are lintable.'); } $versions = array($this->getCacheVersion()); foreach ($linters as $linter) { $version = get_class($linter).':'.$linter->getCacheVersion(); $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setName(get_class($linter)) ->selectSymbolsWithoutLoading(); $symbol = idx($symbols, 'class$'.get_class($linter)); if ($symbol) { $version .= ':'.md5_file( phutil_get_library_root($symbol['library']).'/'.$symbol['where']); } $versions[] = $version; } $this->cacheVersion = crc32(implode("\n", $versions)); $runnable = $this->getRunnableLinters($linters); $this->stopped = array(); $exceptions = $this->executeLinters($runnable); foreach ($runnable as $linter) { foreach ($linter->getLintMessages() as $message) { if (!$this->isSeverityEnabled($message->getSeverity())) { continue; } if (!$this->isRelevantMessage($message)) { continue; } $message->setGranularity($linter->getCacheGranularity()); $result = $this->getResultForPath($message->getPath()); $result->addMessage($message); } } if ($this->cachedResults) { foreach ($this->cachedResults as $path => $messages) { $messages = idx($messages, $this->cacheVersion, array()); $repository_version = idx($messages, 'repository_version'); unset($messages['stopped']); unset($messages['repository_version']); foreach ($messages as $message) { $use_cache = $this->shouldUseCache( idx($message, 'granularity'), $repository_version); if ($use_cache) { $this->getResultForPath($path)->addMessage( ArcanistLintMessage::newFromDictionary($message)); } } } } foreach ($this->results as $path => $result) { $disk_path = $this->getFilePathOnDisk($path); $result->setFilePathOnDisk($disk_path); if (isset($this->fileData[$path])) { $result->setData($this->fileData[$path]); } else if ($disk_path && Filesystem::pathExists($disk_path)) { // TODO: this may cause us to, e.g., load a large binary when we only // raised an error about its filename. We could refine this by looking // through the lint messages and doing this load only if any of them // have original/replacement text or something like that. try { $this->fileData[$path] = Filesystem::readFile($disk_path); $result->setData($this->fileData[$path]); } catch (FilesystemException $ex) { // Ignore this, it's noncritical that we access this data and it // might be unreadable or a directory or whatever else for plenty // of legitimate reasons. } } } if ($exceptions) { throw new PhutilAggregateException('Some linters failed:', $exceptions); } return $this->results; } final public function isSeverityEnabled($severity) { $minimum = $this->minimumSeverity; return ArcanistLintSeverity::isAtLeastAsSevere($severity, $minimum); } final private function shouldUseCache( $cache_granularity, $repository_version) { - if ($this->commitHookMode) { - return false; - } switch ($cache_granularity) { case ArcanistLinter::GRANULARITY_FILE: return true; case ArcanistLinter::GRANULARITY_DIRECTORY: case ArcanistLinter::GRANULARITY_REPOSITORY: return ($this->repositoryVersion == $repository_version); default: return false; } } /** * @param dict>> * @return this */ final public function setCachedResults(array $results) { $this->cachedResults = $results; return $this; } final public function getResults() { return $this->results; } final public function getStoppedPaths() { return $this->stopped; } abstract public function buildLinters(); final public function setRepositoryVersion($version) { $this->repositoryVersion = $version; return $this; } final private function isRelevantMessage(ArcanistLintMessage $message) { // When a user runs "arc lint", we default to raising only warnings on // lines they have changed (errors are still raised anywhere in the // file). The list of $changed lines may be null, to indicate that the // path is a directory or a binary file so we should not exclude // warnings. if (!$this->changedLines || $message->isError() || $message->shouldBypassChangedLineFiltering()) { return true; } $locations = $message->getOtherLocations(); $locations[] = $message->toDictionary(); foreach ($locations as $location) { $path = idx($location, 'path', $message->getPath()); if (!array_key_exists($path, $this->changedLines)) { continue; } $changed = $this->getPathChangedLines($path); if ($changed === null || !$location['line']) { return true; } $last_line = $location['line']; if (isset($location['original'])) { $last_line += substr_count($location['original'], "\n"); } for ($l = $location['line']; $l <= $last_line; $l++) { if (!empty($changed[$l])) { return true; } } } return false; } final protected function getResultForPath($path) { if (empty($this->results[$path])) { $result = new ArcanistLintResult(); $result->setPath($path); $result->setCacheVersion($this->cacheVersion); $this->results[$path] = $result; } return $this->results[$path]; } final public function getLineAndCharFromOffset($path, $offset) { if (!isset($this->charToLine[$path])) { $char_to_line = array(); $line_to_first_char = array(); $lines = explode("\n", $this->loadData($path)); $line_number = 0; $line_start = 0; foreach ($lines as $line) { $len = strlen($line) + 1; // Account for "\n". $line_to_first_char[] = $line_start; $line_start += $len; for ($ii = 0; $ii < $len; $ii++) { $char_to_line[] = $line_number; } $line_number++; } $this->charToLine[$path] = $char_to_line; $this->lineToFirstChar[$path] = $line_to_first_char; } $line = $this->charToLine[$path][$offset]; $char = $offset - $this->lineToFirstChar[$path][$line]; return array($line, $char); } final public function getPostponedLinters() { return $this->postponedLinters; } final public function setPostponedLinters(array $linters) { $this->postponedLinters = $linters; return $this; } protected function getCacheVersion() { return 1; } /** * Get a named linter resource shared by another linter. * * This mechanism allows linters to share arbitrary resources, like the * results of computation. If several linters need to perform the same * expensive computation step, they can use a named resource to synchronize * construction of the result so it doesn't need to be built multiple * times. * * @param string Resource identifier. * @param wild Optionally, default value to return if resource does not * exist. * @return wild Resource, or default value if not present. */ public function getLinterResource($key, $default = null) { return idx($this->linterResources, $key, $default); } /** * Set a linter resource that other linters can access. * * See @{method:getLinterResource} for a description of this mechanism. * * @param string Resource identifier. * @param wild Resource. * @return this */ public function setLinterResource($key, $value) { $this->linterResources[$key] = $value; return $this; } private function getRunnableLinters(array $linters) { assert_instances_of($linters, 'ArcanistLinter'); // TODO: The canRun() mechanism is only used by one linter, and just // silently disables the linter. Almost every other linter handles this // by throwing `ArcanistMissingLinterException`. Both mechanisms are not // ideal; linters which can not run should emit a message, get marked as // "skipped", and allow execution to continue. See T7045. $runnable = array(); foreach ($linters as $key => $linter) { if ($linter->canRun()) { $runnable[$key] = $linter; } } return $runnable; } private function executeLinters(array $runnable) { $all_paths = $this->getPaths(); $path_chunks = array_chunk($all_paths, 32, $preserve_keys = true); $exception_lists = array(); foreach ($path_chunks as $chunk) { $exception_lists[] = $this->executeLintersOnChunk($runnable, $chunk); } return array_mergev($exception_lists); } private function executeLintersOnChunk(array $runnable, array $path_list) { assert_instances_of($runnable, 'ArcanistLinter'); $path_map = array_fuse($path_list); $exceptions = array(); $did_lint = array(); foreach ($runnable as $linter) { $linter_id = $linter->getLinterID(); $paths = $linter->getPaths(); foreach ($paths as $key => $path) { // If we aren't running this path in the current chunk of paths, // skip it completely. if (empty($path_map[$path])) { unset($paths[$key]); continue; } // Make sure each path has a result generated, even if it is empty // (i.e., the file has no lint messages). $result = $this->getResultForPath($path); // If a linter has stopped all other linters for this path, don't // actually run the linter. if (isset($this->stopped[$path])) { unset($paths[$key]); continue; } // If we have a cached result for this path, don't actually run the // linter. if (isset($this->cachedResults[$path][$this->cacheVersion])) { $cached_result = $this->cachedResults[$path][$this->cacheVersion]; $use_cache = $this->shouldUseCache( $linter->getCacheGranularity(), idx($cached_result, 'repository_version')); if ($use_cache) { unset($paths[$key]); if (idx($cached_result, 'stopped') == $linter_id) { $this->stopped[$path] = $linter_id; } } } } $paths = array_values($paths); if (!$paths) { continue; } try { $this->executeLinterOnPaths($linter, $paths); $did_lint[] = array($linter, $paths); } catch (Exception $ex) { $exceptions[] = $ex; } } foreach ($did_lint as $info) { list($linter, $paths) = $info; try { $this->executeDidLintOnPaths($linter, $paths); } catch (Exception $ex) { $exceptions[] = $ex; } } return $exceptions; } private function beginLintServiceCall(ArcanistLinter $linter, array $paths) { $profiler = PhutilServiceProfiler::getInstance(); return $profiler->beginServiceCall( array( 'type' => 'lint', 'linter' => $linter->getInfoName(), 'paths' => $paths, )); } private function endLintServiceCall($call_id) { $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall($call_id, array()); } private function executeLinterOnPaths(ArcanistLinter $linter, array $paths) { $call_id = $this->beginLintServiceCall($linter, $paths); try { $linter->willLintPaths($paths); foreach ($paths as $path) { $linter->setActivePath($path); $linter->lintPath($path); if ($linter->didStopAllLinters()) { $this->stopped[$path] = $linter->getLinterID(); } } } catch (Exception $ex) { $this->endLintServiceCall($call_id); throw $ex; } $this->endLintServiceCall($call_id); } private function executeDidLintOnPaths(ArcanistLinter $linter, array $paths) { $call_id = $this->beginLintServiceCall($linter, $paths); try { $linter->didLintPaths($paths); } catch (Exception $ex) { $this->endLintServiceCall($call_id); throw $ex; } $this->endLintServiceCall($call_id); } } diff --git a/src/lint/linter/ArcanistLinter.php b/src/lint/linter/ArcanistLinter.php index 23a7f7a6..41e27c1c 100644 --- a/src/lint/linter/ArcanistLinter.php +++ b/src/lint/linter/ArcanistLinter.php @@ -1,650 +1,649 @@ getLinterName(), $this->getLinterConfigurationName(), get_class($this)); } /* -( Runtime State )------------------------------------------------------ */ /** * @task state */ final public function getActivePath() { return $this->activePath; } /** * @task state */ final public function setActivePath($path) { $this->stopAllLinters = false; $this->activePath = $path; return $this; } /** * @task state */ final public function setEngine(ArcanistLintEngine $engine) { $this->engine = $engine; return $this; } /** * @task state */ final protected function getEngine() { return $this->engine; } /** * Set the internal ID for this linter. * * This ID is assigned automatically by the @{class:ArcanistLintEngine}. * * @param string Unique linter ID. * @return this * @task state */ final public function setLinterID($id) { $this->id = $id; return $this; } /** * Get the internal ID for this linter. * * Retrieves an internal linter ID managed by the @{class:ArcanistLintEngine}. * This ID is a unique scalar which distinguishes linters in a list. * * @return string Unique linter ID. * @task state */ final public function getLinterID() { return $this->id; } /* -( Executing Linters )-------------------------------------------------- */ /** * Hook called before a list of paths are linted. * * Parallelizable linters can start multiple requests in parallel here, * to improve performance. They can implement @{method:didLintPaths} to * collect results. * * Linters which are not parallelizable should normally ignore this callback * and implement @{method:lintPath} instead. * * @param list A list of paths to be linted * @return void * @task exec */ public function willLintPaths(array $paths) { return; } /** * Hook called for each path to be linted. * * Linters which are not parallelizable can do work here. * * Linters which are parallelizable may want to ignore this callback and * implement @{method:willLintPaths} and @{method:didLintPaths} instead. * * @param string Path to lint. * @return void * @task exec */ public function lintPath($path) { return; } /** * Hook called after a list of paths are linted. * * Parallelizable linters can collect results here. * * Linters which are not paralleizable should normally ignore this callback * and implement @{method:lintPath} instead. * * @param list A list of paths which were linted. * @return void * @task exec */ public function didLintPaths(array $paths) { return; } /** * Obsolete hook which was invoked before a path was linted. * * WARNING: This is an obsolete hook which is not called. If you maintain * a linter which relies on it, update to use @{method:lintPath} instead. * * @task exec */ final public function willLintPath($path) { // TODO: Remove this method after some time. In the meantime, the "final" // will fatal subclasses which implement this hook and point at the API // change so maintainers get fewer surprises. throw new PhutilMethodNotImplementedException(); } /** * Obsolete hook which was invoked after linters ran. * * WARNING: This is an obsolete hook which is not called. If you maintain * a linter which relies on it, update to use @{method:didLintPaths} instead. * * @return void * @task exec */ final public function didRunLinters() { // TODO: Remove this method after some time. In the meantime, the "final" // will fatal subclasses which implement this hook and point at the API // change so maintainers get fewer surprises. throw new PhutilMethodNotImplementedException(); } public function getLinterPriority() { return 1.0; } /** * TODO: This should be `final`. */ public function setCustomSeverityMap(array $map) { $this->customSeverityMap = $map; return $this; } final public function setCustomSeverityRules(array $rules) { $this->customSeverityRules = $rules; return $this; } final public function getOtherLocation($offset, $path = null) { if ($path === null) { $path = $this->getActivePath(); } list($line, $char) = $this->getEngine()->getLineAndCharFromOffset( $path, $offset); return array( 'path' => $path, 'line' => $line + 1, 'char' => $char, ); } final public function stopAllLinters() { $this->stopAllLinters = true; return $this; } final public function didStopAllLinters() { return $this->stopAllLinters; } final public function addPath($path) { $this->paths[$path] = $path; $this->filteredPaths = null; return $this; } final public function setPaths(array $paths) { $this->paths = $paths; $this->filteredPaths = null; return $this; } /** * Filter out paths which this linter doesn't act on (for example, because * they are binaries and the linter doesn't apply to binaries). * * @param list * @return list */ final private function filterPaths(array $paths) { $engine = $this->getEngine(); $keep = array(); foreach ($paths as $path) { if (!$this->shouldLintDeletedFiles() && !$engine->pathExists($path)) { continue; } if (!$this->shouldLintDirectories() && $engine->isDirectory($path)) { continue; } if (!$this->shouldLintBinaryFiles() && $engine->isBinaryFile($path)) { continue; } if (!$this->shouldLintSymbolicLinks() && $engine->isSymbolicLink($path)) { continue; } $keep[] = $path; } return $keep; } final public function getPaths() { if ($this->filteredPaths === null) { $this->filteredPaths = $this->filterPaths(array_values($this->paths)); } return $this->filteredPaths; } final public function addData($path, $data) { $this->data[$path] = $data; return $this; } final protected function getData($path) { if (!array_key_exists($path, $this->data)) { $this->data[$path] = $this->getEngine()->loadData($path); } return $this->data[$path]; } public function getCacheVersion() { return 0; } final public function getLintMessageFullCode($short_code) { return $this->getLinterName().$short_code; } final public function getLintMessageSeverity($code) { $map = $this->customSeverityMap; if (isset($map[$code])) { return $map[$code]; } foreach ($this->customSeverityRules as $rule => $severity) { if (preg_match($rule, $code)) { return $severity; } } $map = $this->getLintSeverityMap(); if (isset($map[$code])) { return $map[$code]; } return $this->getDefaultMessageSeverity($code); } protected function getDefaultMessageSeverity($code) { return ArcanistLintSeverity::SEVERITY_ERROR; } final public function isMessageEnabled($code) { return ($this->getLintMessageSeverity($code) !== ArcanistLintSeverity::SEVERITY_DISABLED); } final public function getLintMessageName($code) { $map = $this->getLintNameMap(); if (isset($map[$code])) { return $map[$code]; } return 'Unknown lint message!'; } final protected function addLintMessage(ArcanistLintMessage $message) { - if (!$this->getEngine()->getCommitHookMode()) { - $root = $this->getEngine()->getWorkingCopy()->getProjectRoot(); - $path = Filesystem::resolvePath($message->getPath(), $root); - $message->setPath(Filesystem::readablePath($path, $root)); - } + $root = $this->getEngine()->getWorkingCopy()->getProjectRoot(); + $path = Filesystem::resolvePath($message->getPath(), $root); + $message->setPath(Filesystem::readablePath($path, $root)); + $this->messages[] = $message; return $message; } final public function getLintMessages() { return $this->messages; } final protected function raiseLintAtLine( $line, $char, $code, $desc, $original = null, $replacement = null) { $message = id(new ArcanistLintMessage()) ->setPath($this->getActivePath()) ->setLine($line) ->setChar($char) ->setCode($this->getLintMessageFullCode($code)) ->setSeverity($this->getLintMessageSeverity($code)) ->setName($this->getLintMessageName($code)) ->setDescription($desc) ->setOriginalText($original) ->setReplacementText($replacement); return $this->addLintMessage($message); } final protected function raiseLintAtPath($code, $desc) { return $this->raiseLintAtLine(null, null, $code, $desc, null, null); } final protected function raiseLintAtOffset( $offset, $code, $desc, $original = null, $replacement = null) { $path = $this->getActivePath(); $engine = $this->getEngine(); if ($offset === null) { $line = null; $char = null; } else { list($line, $char) = $engine->getLineAndCharFromOffset($path, $offset); } return $this->raiseLintAtLine( $line + 1, $char + 1, $code, $desc, $original, $replacement); } public function canRun() { return true; } abstract public function getLinterName(); public function getVersion() { return null; } final protected function isCodeEnabled($code) { $severity = $this->getLintMessageSeverity($code); return $this->getEngine()->isSeverityEnabled($severity); } public function getLintSeverityMap() { return array(); } public function getLintNameMap() { return array(); } public function getCacheGranularity() { return self::GRANULARITY_FILE; } /** * If this linter is selectable via `.arclint` configuration files, return * a short, human-readable name to identify it. For example, `"jshint"` or * `"pep8"`. * * If you do not implement this method, the linter will not be selectable * through `.arclint` files. */ public function getLinterConfigurationName() { return null; } public function getLinterConfigurationOptions() { if (!$this->canCustomizeLintSeverities()) { return array(); } return array( 'severity' => array( 'type' => 'optional map', 'help' => pht( 'Provide a map from lint codes to adjusted severity levels: error, '. 'warning, advice, autofix or disabled.'), ), 'severity.rules' => array( 'type' => 'optional map', 'help' => pht( 'Provide a map of regular expressions to severity levels. All '. 'matching codes have their severity adjusted.'), ), ); } public function setLinterConfigurationValue($key, $value) { $sev_map = array( 'error' => ArcanistLintSeverity::SEVERITY_ERROR, 'warning' => ArcanistLintSeverity::SEVERITY_WARNING, 'autofix' => ArcanistLintSeverity::SEVERITY_AUTOFIX, 'advice' => ArcanistLintSeverity::SEVERITY_ADVICE, 'disabled' => ArcanistLintSeverity::SEVERITY_DISABLED, ); switch ($key) { case 'severity': if (!$this->canCustomizeLintSeverities()) { break; } $custom = array(); foreach ($value as $code => $severity) { if (empty($sev_map[$severity])) { $valid = implode(', ', array_keys($sev_map)); throw new Exception( pht( 'Unknown lint severity "%s". Valid severities are: %s.', $severity, $valid)); } $code = $this->getLintCodeFromLinterConfigurationKey($code); $custom[$code] = $severity; } $this->setCustomSeverityMap($custom); return; case 'severity.rules': if (!$this->canCustomizeLintSeverities()) { break; } foreach ($value as $rule => $severity) { if (@preg_match($rule, '') === false) { throw new Exception( pht( 'Severity rule "%s" is not a valid regular expression.', $rule)); } if (empty($sev_map[$severity])) { $valid = implode(', ', array_keys($sev_map)); throw new Exception( pht( 'Unknown lint severity "%s". Valid severities are: %s.', $severity, $valid)); } } $this->setCustomSeverityRules($value); return; } throw new Exception("Incomplete implementation: {$key}!"); } protected function canCustomizeLintSeverities() { return true; } protected function shouldLintBinaryFiles() { return false; } protected function shouldLintDeletedFiles() { return false; } protected function shouldLintDirectories() { return false; } protected function shouldLintSymbolicLinks() { return false; } /** * Map a configuration lint code to an `arc` lint code. Primarily, this is * intended for validation, but can also be used to normalize case or * otherwise be more permissive in accepted inputs. * * If the code is not recognized, you should throw an exception. * * @param string Code specified in configuration. * @return string Normalized code to use in severity map. */ protected function getLintCodeFromLinterConfigurationKey($code) { return $code; } /** * Retrieve an old lint configuration value from `.arcconfig` or a similar * source. * * Modern linters should use @{method:getConfig} to read configuration from * `.arclint`. * * @param string Configuration key to retrieve. * @param wild Default value to return if key is not present in config. * @return wild Configured value, or default if no configuration exists. */ final protected function getDeprecatedConfiguration($key, $default = null) { // If we're being called in a context without an engine (probably from // `arc linters`), just return the default value. if (!$this->engine) { return $default; } $config = $this->getEngine()->getConfigurationManager(); // Construct a sentinel object so we can tell if we're reading config // or not. $sentinel = (object)array(); $result = $config->getConfigFromAnySource($key, $sentinel); // If we read config, warn the user that this mechanism is deprecated and // discouraged. if ($result !== $sentinel) { $console = PhutilConsole::getConsole(); $console->writeErr( "**%s**: %s\n", pht('Deprecation Warning'), pht( 'Configuration option "%s" is deprecated. Generally, linters should '. 'now be configured using an `.arclint` file. See "Arcanist User '. 'Guide: Lint" in the documentation for more information.', $key)); return $result; } return $default; } } diff --git a/src/lint/linter/__tests__/ArcanistLinterTestCase.php b/src/lint/linter/__tests__/ArcanistLinterTestCase.php index 61a50b14..322dca7f 100644 --- a/src/lint/linter/__tests__/ArcanistLinterTestCase.php +++ b/src/lint/linter/__tests__/ArcanistLinterTestCase.php @@ -1,266 +1,263 @@ getLinter(); } $files = id(new FileFinder($root)) ->withType('f') ->withSuffix('lint-test') ->find(); $test_count = 0; foreach ($files as $file) { $this->lintFile($root.$file, $linter); $test_count++; } $this->assertTrue( ($test_count > 0), pht('Expected to find some .lint-test tests in directory %s!', $root)); } private function lintFile($file, ArcanistLinter $linter) { $linter = clone $linter; $contents = Filesystem::readFile($file); $contents = preg_split('/^~{4,}\n/m', $contents); if (count($contents) < 2) { throw new Exception( pht( "Expected '%s' separating test case and results.", '~~~~~~~~~~')); } list ($data, $expect, $xform, $config) = array_merge( $contents, array(null, null)); $basename = basename($file); if ($config) { $config = phutil_json_decode($config); } else { $config = array(); } PhutilTypeSpec::checkMap( $config, array( - 'hook' => 'optional bool', 'config' => 'optional map', 'path' => 'optional string', 'mode' => 'optional string', 'stopped' => 'optional bool', )); $exception = null; $after_lint = null; $messages = null; $exception_message = false; $caught_exception = false; try { $tmp = new TempFile($basename); Filesystem::writeFile($tmp, $data); $full_path = (string)$tmp; $mode = idx($config, 'mode'); if ($mode) { Filesystem::changePermissions($tmp, octdec($mode)); } $dir = dirname($full_path); $path = basename($full_path); $working_copy = ArcanistWorkingCopyIdentity::newFromRootAndConfigFile( $dir, null, 'Unit Test'); $configuration_manager = new ArcanistConfigurationManager(); $configuration_manager->setWorkingCopyIdentity($working_copy); $engine = new ArcanistUnitTestableLintEngine(); $engine->setWorkingCopy($working_copy); $engine->setConfigurationManager($configuration_manager); - $engine->setCommitHookMode(idx($config, 'hook', false)); - $path_name = idx($config, 'path', $path); $engine->setPaths(array($path_name)); $linter->addPath($path_name); $linter->addData($path_name, $data); foreach (idx($config, 'config', array()) as $key => $value) { $linter->setLinterConfigurationValue($key, $value); } $engine->addLinter($linter); $engine->addFileData($path_name, $data); $results = $engine->run(); $this->assertEqual( 1, count($results), pht('Expect one result returned by linter.')); $assert_stopped = idx($config, 'stopped'); if ($assert_stopped !== null) { $this->assertEqual( $assert_stopped, $linter->didStopAllLinters(), $assert_stopped ? pht('Expect linter to be stopped.') : pht('Expect linter to not be stopped.')); } $result = reset($results); $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result); $after_lint = $patcher->getModifiedFileContent(); } catch (ArcanistPhutilTestTerminatedException $ex) { throw $ex; } catch (Exception $exception) { $caught_exception = true; if ($exception instanceof PhutilAggregateException) { $caught_exception = false; foreach ($exception->getExceptions() as $ex) { if ($ex instanceof ArcanistUsageException || $ex instanceof ArcanistMissingLinterException) { $this->assertSkipped($ex->getMessage()); } else { $caught_exception = true; } } } else if ($exception instanceof ArcanistUsageException || $exception instanceof ArcanistMissingLinterException) { $this->assertSkipped($exception->getMessage()); } $exception_message = $exception->getMessage()."\n\n". $exception->getTraceAsString(); } $this->assertEqual(false, $caught_exception, $exception_message); $this->compareLint($basename, $expect, $result); $this->compareTransform($xform, $after_lint); } private function compareLint($file, $expect, ArcanistLintResult $result) { $seen = array(); $raised = array(); $message_map = array(); foreach ($result->getMessages() as $message) { $sev = $message->getSeverity(); $line = $message->getLine(); $char = $message->getChar(); $code = $message->getCode(); $name = $message->getName(); $message_key = $sev.':'.$line.':'.$char; $message_map[$message_key] = $message; $seen[] = $message_key; $raised[] = sprintf( ' %s: %s %s', pht('%s at line %d, char %d', $sev, $line, $char), $code, $name); } $expect = trim($expect); if ($expect) { $expect = explode("\n", $expect); } else { $expect = array(); } foreach ($expect as $key => $expected) { $expect[$key] = head(explode(' ', $expected)); } $expect = array_fill_keys($expect, true); $seen = array_fill_keys($seen, true); if (!$raised) { $raised = array(pht('No messages.')); } $raised = sprintf( "%s:\n%s", pht('Actually raised'), implode("\n", $raised)); foreach (array_diff_key($expect, $seen) as $missing => $ignored) { $missing = explode(':', $missing); $sev = array_shift($missing); $pos = $missing; $this->assertFailure( pht( "In '%s', expected lint to raise %s on line %d at char %d, ". "but no %s was raised. %s", $file, $sev, idx($pos, 0), idx($pos, 1), $sev, $raised)); } foreach (array_diff_key($seen, $expect) as $surprising => $ignored) { $message = $message_map[$surprising]; $message_info = $message->getDescription(); list($sev, $line, $char) = explode(':', $surprising); $this->assertFailure( sprintf( "%s:\n\n%s\n\n%s", pht( "In '%s', lint raised %s on line %d at char %d, ". "but nothing was expected", $file, $sev, $line, $char), $message_info, $raised)); } } private function compareTransform($expected, $actual) { if (!strlen($expected)) { return; } $this->assertEqual( $expected, $actual, pht('File as patched by lint did not match the expected patched file.')); } } diff --git a/src/lint/linter/__tests__/spelling/spell.lint-test b/src/lint/linter/__tests__/spelling/spell.lint-test index a992d1f3..367ba8a5 100644 --- a/src/lint/linter/__tests__/spelling/spell.lint-test +++ b/src/lint/linter/__tests__/spelling/spell.lint-test @@ -1,25 +1,20 @@ Hello teh word $y = $x->recieveData(); for (i=0; isetCWD($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 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( "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( '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( "Unable to find any git commit named '{$symbolic_commit}' in ". "this repository."); } if ($this->symbolicHeadCommit === null) { $this->setBaseCommitExplanation( "it is the merge-base of the explicitly specified base commit ". "'{$symbolic_commit}' and HEAD."); } else { $this->setBaseCommitExplanation( "it is the merge-base of the explicitly specified base commit ". "'{$symbolic_commit}' and the explicitly specified head ". "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( 'the repository has no commits.'); } else { $this->setBaseCommitExplanation( '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( "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( "it is the merge-base of '{$default_relative}' and HEAD, as ". "specified in 'git.default-relative-commit' in '.arcconfig'. This ". "setting overrides other settings."); } 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( "it is the merge-base of '{$default_relative}' (the Git upstream ". "of the current branch) HEAD."); } } if (!$default_relative) { $default_relative = $this->readScratchFile('default-relative-commit'); $default_relative = trim($default_relative); if ($default_relative) { $this->setBaseCommitExplanation( "it is the merge-base of '{$default_relative}' and HEAD, as ". "specified in '.git/arc/default-relative-commit'."); } } if (!$default_relative) { // TODO: Remove the history lesson soon. echo phutil_console_format( "** Select a Default Commit Range **\n\n"); echo phutil_console_wrap( "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 'HEAD^' when you did not specify ". "a start revision, but this behavior does not make much sense in ". "most workflows outside of Facebook's historic git-svn workflow.\n\n". "arc no longer assumes 'HEAD^'. You must specify a relative commit ". "explicitly when you invoke a command (e.g., `arc diff HEAD^`, not ". "just `arc diff`) or select a default for this working copy.\n\n". "In most cases, the best default is 'origin/master'. You can also ". "select 'HEAD^' 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.)"); $prompt = '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( "Relative commit '{$default_relative}' is not the name of a commit!"); } 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( "it is the merge-base of '{$default_relative}' and HEAD, as you ". "just specified."); } 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( "Unable to find any git commit named '{$symbolic_commit}' in ". "this repository."); } 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', ); 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(); if ($head !== null) { list($stdout) = $this->execxLocal( "diff {$options} %s %s --", $base, $head); } else { list($stdout) = $this->execxLocal( "diff {$options} %s --", $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; } public function getBranchName() { // TODO: consider: // // $ git rev-parse --abbrev-ref `git symbolic-ref HEAD` // // But that may fail if you're not on a branch. list($stdout) = $this->execxLocal('branch --no-color'); // Assume that any branch beginning with '(' means 'no branch', or whatever // 'no branch' is in the current locale. $matches = null; if (preg_match('/^\* ([^\(].*)$/m', $stdout, $matches)) { return $matches[1]; } return null; } public function getRemoteURI() { list($stdout) = $this->execxLocal('remote show -n origin'); $matches = null; if (preg_match('/^\s*Fetch URL: (.*)$/m', $stdout, $matches)) { return trim($matches[1]); } return null; } 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( "Cannot find the {$vcs} equivalent of {$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'); } protected function buildUncommittedStatus() { $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->parseGitStatus($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 --', $this->getDiffBaseOptions(), $this->getBaseCommit()); return $this->parseGitStatus($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; } - public function getPreReceiveHookStatus($old_ref, $new_ref) { - $options = $this->getDiffBaseOptions(); - list($stdout) = $this->execxLocal( - "diff {$options} --raw %s %s --", - $old_ref, - $new_ref); - return $this->parseGitStatus($stdout, $full = true); - } - private function parseGitStatus($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; $flag = $line[4]; $file = $line[5]; foreach ($flags as $key => $bits) { if ($flag == $key) { $mask |= $bits; } } if ($full) { $files[$file] = array( 'mask' => $mask, 'ref' => rtrim($line[3], '.'), ); } 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->parseGitStatus($stdout); } public function getBlame($path) { // TODO: 'git blame' supports --porcelain and we should probably use it. list($stdout) = $this->execxLocal( 'blame --date=iso -w -M %s -- %s', $this->getBaseCommit(), $path); $blame = array(); foreach (explode("\n", trim($stdout)) as $line) { if (!strlen($line)) { continue; } // lines predating a git repo's history are blamed to the oldest revision, // with the commit hash prepended by a ^. we shouldn't count these lines // as blaming to the oldest diff's unfortunate author if ($line[0] == '^') { continue; } $matches = null; $ok = preg_match( '/^([0-9a-f]+)[^(]+?[(](.*?) +\d\d\d\d-\d\d-\d\d/', $line, $matches); if (!$ok) { throw new Exception("Bad blame? `{$line}'"); } $revision = $matches[1]; $author = $matches[2]; $blame[] = array($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('Failed to parse git ls-tree output!'); } $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. 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() { list($branch_info) = $this->execxLocal( 'branch --no-color'); $lines = explode("\n", rtrim($branch_info)); $result = array(); foreach ($lines as $line) { if (preg_match('@^[* ]+\(no branch|detached from \w+/\w+\)@', $line)) { // This is indicating that the working copy is in a detached state; // just ignore it. continue; } list($current, $name) = preg_split('/\s+/', $line, 2); $result[] = array( 'current' => !empty($current), 'name' => $name, ); } return $result; } public function getWorkingCopyRevision() { list($stdout) = $this->execxLocal('rev-parse HEAD'); return rtrim($stdout, "\n"); } public function getUnderlyingWorkingCopyRevision() { list($err, $stdout) = $this->execManualLocal('svn find-rev HEAD'); if (!$err && $stdout) { return rtrim($stdout, "\n"); } return $this->getWorkingCopyRevision(); } 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 supportsLocalBranchMerge() { return true; } public function performLocalBranchMerge($branch, $message) { if (!$branch) { throw new ArcanistUsageException( 'Under git, you must specify the branch you want to merge.'); } $err = phutil_passthru( '(cd %s && git merge --no-ff -m %s %s)', $this->getPath(), $message, $branch); if ($err) { throw new ArcanistUsageException('Merge failed!'); } } public function getFinalizedRevisionMessage() { return "You may now push this commit upstream, as appropriate (e.g. with ". "'git push', or 'git svn dcommit', or by printing and faxing it)."; } 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'] = "Commit message for '{$hash}' has explicit 'Differential Revision'."; } 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'] = '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 '(The Empty Tree)'; } list($summary) = $this->execxLocal( 'log -n 1 --format=%C %s', '%s', $commit); return trim($summary); } public function backoutCommit($commit_hash) { $this->execxLocal( 'revert %s -n --no-edit', $commit_hash); $this->reloadWorkingCopy(); if (!$this->getUncommittedStatus()) { throw new ArcanistUsageException( "{$commit_hash} has already been reverted."); } } public function getBackoutMessage($commit_hash) { return 'This reverts commit '.$commit_hash.'.'; } 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( "it is the merge-base of '{$matches[1]}' and HEAD, as ". "specified by '{$rule}' in your {$source} 'base' ". "configuration."); 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; foreach ($commits as $commit) { list($branches) = $this->execxLocal( 'branch --contains %s', $commit); $branches = array_filter(explode("\n", $branches)); 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) { foreach ($branches as $key => $branch) { $branches[$key] = trim($branch, ' *'); } $branches = implode(', ', $branches); $this->setBaseCommitExplanation( "it is the first commit between '{$merge_base}' (the ". "merge-base of '{$matches[1]}' and HEAD) which is also ". "contained by another branch ({$branches})."); return $commit; } } } else { list($err) = $this->execManualLocal( 'cat-file -t %s', $name); if (!$err) { $this->setBaseCommitExplanation( "it is specified by '{$rule}' in your {$source} 'base' ". "configuration."); return $name; } } break; case 'arc': switch ($name) { case 'empty': $this->setBaseCommitExplanation( "you specified '{$rule}' in your {$source} 'base' ". "configuration."); return self::GIT_MAGIC_ROOT_COMMIT; case 'amended': $text = $this->getCommitMessage('HEAD'); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( $text); if ($message->getRevisionID()) { $this->setBaseCommitExplanation( "HEAD has been amended with 'Differential Revision:', ". "as specified by '{$rule}' in your {$source} 'base' ". "configuration."); 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( "it is the merge-base of the upstream of the current branch ". "and HEAD, and matched the rule '{$rule}' in your {$source} ". "'base' configuration."); return $upstream_merge_base; } break; case 'this': $this->setBaseCommitExplanation( "you specified '{$rule}' in your {$source} 'base' ". "configuration."); 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; } } diff --git a/src/repository/hookapi/ArcanistHookAPI.php b/src/repository/hookapi/ArcanistHookAPI.php deleted file mode 100644 index 2c9a217f..00000000 --- a/src/repository/hookapi/ArcanistHookAPI.php +++ /dev/null @@ -1,9 +0,0 @@ -root = $root; - $this->transaction = $transaction; - $this->repository = $repository; - } - - public function getCurrentFileData($path) { - list($err, $file) = exec_manual( - 'svnlook cat --transaction %s %s %s', - $this->transaction, - $this->repository, - $path); - - return ($err? null : $file); - } - - public function getUpstreamFileData($path) { - list($err, $file) = exec_manual( - 'svnlook cat %s %s', - $this->repository, - $this->root."/$path"); - return ($err ? null : $file); - } - -}