Page MenuHomePhabricator

D20990.diff
No OneTemporary

D20990.diff

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

File Metadata

Mime Type
text/plain
Expires
Wed, Mar 12, 9:52 AM (4 h, 40 m)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/5f/iy/z6mtms3ai7k7r5nt
Default Alt Text
D20990.diff (121 KB)

Event Timeline