1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-06 04:41:01 +01:00

(stable) Promote 2016 Week 18

This commit is contained in:
epriestley 2016-04-30 05:01:43 -07:00
commit 04db64ca42
71 changed files with 4304 additions and 790 deletions

View file

@ -7,8 +7,8 @@
*/
return array(
'names' => array(
'core.pkg.css' => '04a95108',
'core.pkg.js' => '37344f3c',
'core.pkg.css' => 'b729f9f5',
'core.pkg.js' => '6972d365',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => '7ba78475',
'differential.pkg.js' => 'd0cd0df6',
@ -136,7 +136,7 @@ return array(
'rsrc/css/phui/phui-form-view.css' => '6a51768e',
'rsrc/css/phui/phui-form.css' => 'aac1d51d',
'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f',
'rsrc/css/phui/phui-header-view.css' => '230254d3',
'rsrc/css/phui/phui-header-view.css' => '4c7dd8f5',
'rsrc/css/phui/phui-hovercard.css' => 'de1a2119',
'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad',
'rsrc/css/phui/phui-icon.css' => '3f33ab57',
@ -232,7 +232,7 @@ return array(
'rsrc/externals/javelin/lib/DOM.js' => '805b806a',
'rsrc/externals/javelin/lib/History.js' => 'd4505101',
'rsrc/externals/javelin/lib/JSON.js' => '69adf288',
'rsrc/externals/javelin/lib/Leader.js' => 'b4ba945c',
'rsrc/externals/javelin/lib/Leader.js' => 'fea0eb47',
'rsrc/externals/javelin/lib/Mask.js' => '8a41885b',
'rsrc/externals/javelin/lib/Quicksand.js' => '6b8ef10b',
'rsrc/externals/javelin/lib/Request.js' => '94b750d2',
@ -704,7 +704,7 @@ return array(
'javelin-history' => 'd4505101',
'javelin-install' => '05270951',
'javelin-json' => '69adf288',
'javelin-leader' => 'b4ba945c',
'javelin-leader' => 'fea0eb47',
'javelin-magical-init' => '3010e992',
'javelin-mask' => '8a41885b',
'javelin-quicksand' => '6b8ef10b',
@ -829,7 +829,7 @@ return array(
'phui-form-css' => 'aac1d51d',
'phui-form-view-css' => '6a51768e',
'phui-head-thing-view-css' => 'fd311e5f',
'phui-header-view-css' => '230254d3',
'phui-header-view-css' => '4c7dd8f5',
'phui-hovercard' => '1bd28176',
'phui-hovercard-view-css' => 'de1a2119',
'phui-icon-set-selector-css' => '1ab67aad',
@ -1772,9 +1772,6 @@ return array(
'javelin-typeahead-preloaded-source',
'javelin-util',
),
'b4ba945c' => array(
'javelin-install',
),
'b59e1e96' => array(
'javelin-behavior',
'javelin-stratcom',
@ -2176,6 +2173,9 @@ return array(
'javelin-view-visitor',
'javelin-util',
),
'fea0eb47' => array(
'javelin-install',
),
),
'packages' => array(
'core.pkg.css' => array(

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_repository.repository_workingcopyversion
ADD lockOwner VARCHAR(255) COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,16 @@
CREATE TABLE {$NAMESPACE}_search.edge (
src VARBINARY(64) NOT NULL,
type INT UNSIGNED NOT NULL,
dst VARBINARY(64) NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
seq INT UNSIGNED NOT NULL,
dataID INT UNSIGNED,
PRIMARY KEY (src, type, dst),
KEY `src` (src, type, dateCreated, seq),
UNIQUE KEY `key_dst` (dst, type, src)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
CREATE TABLE {$NAMESPACE}_search.edgedata (
id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,19 @@
CREATE TABLE {$NAMESPACE}_repository.repository_uritransaction (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
viewPolicy VARBINARY(64) NOT NULL,
editPolicy VARBINARY(64) NOT NULL,
commentPHID VARBINARY(64) DEFAULT NULL,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (`phid`),
KEY `key_object` (`objectPHID`)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -10,7 +10,7 @@ AllowUsers vcs-user
# You may need to tweak these options, but mostly they just turn off everything
# dangerous.
Port 22
Port 2222
Protocol 2
PermitRootLogin no
AllowAgentForwarding no

View file

@ -743,9 +743,13 @@ phutil_register_library_map(array(
'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php',
'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php',
'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
'DiffusionRepositoryActionsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php',
'DiffusionRepositoryAutomationManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php',
'DiffusionRepositoryBasicsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php',
'DiffusionRepositoryBranchesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php',
'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php',
'DiffusionRepositoryClusterManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php',
'DiffusionRepositoryClusterEngine' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php',
'DiffusionRepositoryClusterEngineLogInterface' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php',
'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php',
'DiffusionRepositoryCreateController' => 'applications/diffusion/controller/DiffusionRepositoryCreateController.php',
'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php',
@ -778,12 +782,19 @@ phutil_register_library_map(array(
'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php',
'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php',
'DiffusionRepositorySearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositorySearchConduitAPIMethod.php',
'DiffusionRepositoryStagingManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php',
'DiffusionRepositoryStatusManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStatusManagementPanel.php',
'DiffusionRepositoryStorageManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php',
'DiffusionRepositorySymbolsController' => 'applications/diffusion/controller/DiffusionRepositorySymbolsController.php',
'DiffusionRepositorySymbolsManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php',
'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php',
'DiffusionRepositoryURIDisableController' => 'applications/diffusion/controller/DiffusionRepositoryURIDisableController.php',
'DiffusionRepositoryURIEditController' => 'applications/diffusion/controller/DiffusionRepositoryURIEditController.php',
'DiffusionRepositoryURIViewController' => 'applications/diffusion/controller/DiffusionRepositoryURIViewController.php',
'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php',
'DiffusionRepositoryURIsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php',
'DiffusionRepositoryURIsSearchEngineAttachment' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsSearchEngineAttachment.php',
'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
@ -807,6 +818,9 @@ phutil_register_library_map(array(
'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php',
'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php',
'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php',
'DiffusionURIEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionURIEditConduitAPIMethod.php',
'DiffusionURIEditEngine' => 'applications/diffusion/editor/DiffusionURIEditEngine.php',
'DiffusionURIEditor' => 'applications/diffusion/editor/DiffusionURIEditor.php',
'DiffusionURITestCase' => 'applications/diffusion/request/__tests__/DiffusionURITestCase.php',
'DiffusionUpdateCoverageConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php',
'DiffusionView' => 'applications/diffusion/view/DiffusionView.php',
@ -1930,6 +1944,7 @@ phutil_register_library_map(array(
'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php',
'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php',
'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php',
'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php',
'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php',
'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php',
@ -3171,6 +3186,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php',
'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php',
'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php',
'PhabricatorRepositoryManagementClusterizeWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php',
'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php',
'PhabricatorRepositoryManagementEditWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php',
'PhabricatorRepositoryManagementImportingWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php',
@ -3228,7 +3244,11 @@ phutil_register_library_map(array(
'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php',
'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
'PhabricatorRepositoryURIPHIDType' => 'applications/repository/phid/PhabricatorRepositoryURIPHIDType.php',
'PhabricatorRepositoryURIQuery' => 'applications/repository/query/PhabricatorRepositoryURIQuery.php',
'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
'PhabricatorRepositoryURITransaction' => 'applications/repository/storage/PhabricatorRepositoryURITransaction.php',
'PhabricatorRepositoryURITransactionQuery' => 'applications/repository/query/PhabricatorRepositoryURITransactionQuery.php',
'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php',
'PhabricatorRepositoryVersion' => 'applications/repository/constants/PhabricatorRepositoryVersion.php',
'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php',
@ -3302,6 +3322,7 @@ phutil_register_library_map(array(
'PhabricatorSearchPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php',
'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php',
'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php',
'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php',
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
@ -4420,7 +4441,7 @@ phutil_register_library_map(array(
'ConduitAPIRequest' => 'Phobject',
'ConduitAPIResponse' => 'Phobject',
'ConduitApplicationNotInstalledException' => 'ConduitMethodNotFoundException',
'ConduitBoolParameterType' => 'ConduitListParameterType',
'ConduitBoolParameterType' => 'ConduitParameterType',
'ConduitCall' => 'Phobject',
'ConduitCallTestCase' => 'PhabricatorTestCase',
'ConduitColumnsParameterType' => 'ConduitParameterType',
@ -4853,7 +4874,10 @@ phutil_register_library_map(array(
'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGitRequest' => 'DiffusionRequest',
'DiffusionGitResponse' => 'AphrontResponse',
'DiffusionGitSSHWorkflow' => 'DiffusionSSHWorkflow',
'DiffusionGitSSHWorkflow' => array(
'DiffusionSSHWorkflow',
'DiffusionRepositoryClusterEngineLogInterface',
),
'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionHistoryController' => 'DiffusionController',
'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
@ -4951,9 +4975,12 @@ phutil_register_library_map(array(
'DiffusionRefTableController' => 'DiffusionController',
'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionRenameHistoryQuery' => 'Phobject',
'DiffusionRepositoryActionsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryAutomationManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryBasicsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryBranchesManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositoryClusterManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryClusterEngine' => 'Phobject',
'DiffusionRepositoryController' => 'DiffusionController',
'DiffusionRepositoryCreateController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource',
@ -4986,12 +5013,19 @@ phutil_register_library_map(array(
'DiffusionRepositoryRef' => 'Phobject',
'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositorySearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionRepositoryStagingManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryStatusManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryStorageManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositorySymbolsController' => 'DiffusionRepositoryEditController',
'DiffusionRepositorySymbolsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryTag' => 'Phobject',
'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryURIDisableController' => 'DiffusionController',
'DiffusionRepositoryURIEditController' => 'DiffusionController',
'DiffusionRepositoryURIViewController' => 'DiffusionController',
'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'DiffusionRepositoryURIsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryURIsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DiffusionRequest' => 'Phobject',
'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionResolveUserQuery' => 'Phobject',
@ -5015,6 +5049,9 @@ phutil_register_library_map(array(
'DiffusionTagListController' => 'DiffusionController',
'DiffusionTagListView' => 'DiffusionView',
'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionURIEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionURIEditEngine' => 'PhabricatorEditEngine',
'DiffusionURIEditor' => 'PhabricatorApplicationTransactionEditor',
'DiffusionURITestCase' => 'PhutilTestCase',
'DiffusionUpdateCoverageConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionView' => 'AphrontView',
@ -6351,6 +6388,7 @@ phutil_register_library_map(array(
'PhabricatorBoardLayoutEngine' => 'Phobject',
'PhabricatorBoardRenderingEngine' => 'Phobject',
'PhabricatorBoardResponseEngine' => 'Phobject',
'PhabricatorBoolEditField' => 'PhabricatorEditField',
'PhabricatorBot' => 'PhabricatorDaemon',
'PhabricatorBotChannel' => 'PhabricatorBotTarget',
'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler',
@ -7824,6 +7862,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryGraphCache' => 'Phobject',
'PhabricatorRepositoryGraphStream' => 'Phobject',
'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementClusterizeWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementEditWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
@ -7892,11 +7931,21 @@ phutil_register_library_map(array(
'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryType' => 'Phobject',
'PhabricatorRepositoryURI' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryURI' => array(
'PhabricatorRepositoryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryURINormalizer' => 'Phobject',
'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryURIPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryURIQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryURITransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorRepositoryURITransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryVersion' => 'Phobject',
'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO',
@ -7972,6 +8021,7 @@ phutil_register_library_map(array(
'PhabricatorSearchPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSearchRelationship' => 'Phobject',
'PhabricatorSearchResultView' => 'AphrontView',
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',

View file

@ -19,13 +19,6 @@ final class AlmanacManagementRegisterWorkflow
'param' => 'key',
'help' => pht('Path to a private key for the host.'),
),
array(
'name' => 'allow-key-reuse',
'help' => pht(
'Register even if another host is already registered with this '.
'keypair. This is an advanced featuer which allows a pool of '.
'devices to share credentials.'),
),
array(
'name' => 'identify-as',
'param' => 'name',
@ -36,13 +29,13 @@ final class AlmanacManagementRegisterWorkflow
array(
'name' => 'force',
'help' => pht(
'Register this host even if keys already exist.'),
'Register this host even if keys already exist on disk.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$device_name = $args->getArg('device');
if (!strlen($device_name)) {
@ -51,7 +44,7 @@ final class AlmanacManagementRegisterWorkflow
}
$device = id(new AlmanacDeviceQuery())
->setViewer($this->getViewer())
->setViewer($viewer)
->withNames(array($device_name))
->executeOne();
if (!$device) {
@ -59,6 +52,23 @@ final class AlmanacManagementRegisterWorkflow
pht('No such device "%s" exists!', $device_name));
}
$identify_as = $args->getArg('identify-as');
$raw_device = $device_name;
if (strlen($identify_as)) {
$raw_device = $identify_as;
}
$identity_device = id(new AlmanacDeviceQuery())
->setViewer($viewer)
->withNames(array($raw_device))
->executeOne();
if (!$identity_device) {
throw new PhutilArgumentUsageException(
pht(
'No such device "%s" exists!', $raw_device));
}
$private_key_path = $args->getArg('private-key');
if (!strlen($private_key_path)) {
throw new PhutilArgumentUsageException(
@ -67,7 +77,7 @@ final class AlmanacManagementRegisterWorkflow
if (!Filesystem::pathExists($private_key_path)) {
throw new PhutilArgumentUsageException(
pht('Private key "%s" does not exist!', $private_key_path));
pht('No private key exists at path "%s"!', $private_key_path));
}
$raw_private_key = Filesystem::readFile($private_key_path);
@ -85,8 +95,8 @@ final class AlmanacManagementRegisterWorkflow
if ($err) {
throw new PhutilArgumentUsageException(
pht(
'Unable to change ownership of a file to daemon user "%s". Run '.
'this command as %s or root.',
'Unable to change ownership of an identity file to daemon user '.
'"%s". Run this command as %s or root.',
$phd_user,
$phd_user));
}
@ -133,43 +143,39 @@ final class AlmanacManagementRegisterWorkflow
->withKeys(array($key_object))
->executeOne();
if ($public_key) {
if ($public_key->getObjectPHID() !== $device->getPHID()) {
throw new PhutilArgumentUsageException(
pht(
'The public key corresponding to the given private key is '.
'already associated with an object other than the specified '.
'device. You can not use a single private key to identify '.
'multiple devices or users.'));
} else if (!$public_key->getIsTrusted()) {
throw new PhutilArgumentUsageException(
pht(
'The public key corresponding to the given private key is '.
'already associated with the device, but is not trusted. '.
'Registering this key would trust the other entities which '.
'hold it. Use a unique key, or explicitly enable trust for the '.
'current key.'));
} else if (!$args->getArg('allow-key-reuse')) {
throw new PhutilArgumentUsageException(
pht(
'The public key corresponding to the given private key is '.
'already associated with the device. If you do not want to '.
'use a unique key, use --allow-key-reuse to permit '.
'reassociation.'));
}
} else {
$public_key = id(new PhabricatorAuthSSHKey())
->setObjectPHID($device->getPHID())
->attachObject($device)
->setName($device->getSSHKeyDefaultName())
->setKeyType($key_object->getType())
->setKeyBody($key_object->getBody())
->setKeyComment(pht('Registered'))
->setIsTrusted(1);
if (!$public_key) {
throw new PhutilArgumentUsageException(
pht(
'The public key corresponding to the given private key is not '.
'yet known to Phabricator. Associate the public key with an '.
'Almanac device in the web interface before registering hosts '.
'with it.'));
}
if ($public_key->getObjectPHID() !== $device->getPHID()) {
$public_phid = $public_key->getObjectPHID();
$public_handles = $viewer->loadHandles(array($public_phid));
$public_handle = $public_handles[$public_phid];
$console->writeOut(
throw new PhutilArgumentUsageException(
pht(
'The public key corresponding to the given private key is already '.
'associated with an object ("%s") other than the specified '.
'device ("%s"). You can not use a single private key to identify '.
'multiple devices or users.',
$public_handle->getFullName(),
$device->getName()));
}
if (!$public_key->getIsTrusted()) {
throw new PhutilArgumentUsageException(
pht(
'The public key corresponding to the given private key is '.
'properly associated with the device, but is not yet trusted. '.
'Trust this key before registering devices with it.'));
}
echo tsprintf(
"%s\n",
pht('Installing public key...'));
@ -179,18 +185,12 @@ final class AlmanacManagementRegisterWorkflow
Filesystem::writeFile($tmp_public, $raw_public_key);
execx('mv -f %s %s', $tmp_public, $stored_public_path);
$console->writeOut(
echo tsprintf(
"%s\n",
pht('Installing private key...'));
execx('mv -f %s %s', $tmp_private, $stored_private_path);
$raw_device = $device_name;
$identify_as = $args->getArg('identify-as');
if (strlen($identify_as)) {
$raw_device = $identify_as;
}
$console->writeOut(
echo tsprintf(
"%s\n",
pht('Installing device %s...', $raw_device));
@ -202,14 +202,7 @@ final class AlmanacManagementRegisterWorkflow
Filesystem::writeFile($tmp_device, $raw_device);
execx('mv -f %s %s', $tmp_device, $stored_device_path);
if (!$public_key->getID()) {
$console->writeOut(
"%s\n",
pht('Registering device key...'));
$public_key->save();
}
$console->writeOut(
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('HOST REGISTERED'),
pht(

View file

@ -274,7 +274,7 @@ abstract class PhabricatorAphlictManagementWorkflow
$pid_path = $this->getPIDPath();
try {
$dir = dirname($path);
$dir = dirname($pid_path);
if (!Filesystem::pathExists($dir)) {
Filesystem::createDirectory($dir, 0755, true);
}

View file

@ -50,8 +50,8 @@ abstract class CelerityResourceController extends PhabricatorController {
// is not, refuse to cache this resource. This avoids poisoning caches
// and CDNs if we're getting a request for a new resource to an old node
// shortly after a push.
$is_cacheable = ($hash === $expect_hash) &&
$this->isCacheableResourceType($type);
$is_cacheable = ($hash === $expect_hash);
$is_locally_cacheable = $this->isLocallyCacheableResourceType($type);
if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) {
// Return a "304 Not Modified". We don't care about the value of this
// field since we never change what resource is served by a given URI.
@ -60,7 +60,7 @@ abstract class CelerityResourceController extends PhabricatorController {
$cache = null;
$data = null;
if ($is_cacheable && !$dev_mode) {
if ($is_cacheable && $is_locally_cacheable && !$dev_mode) {
$cache = PhabricatorCaches::getImmutableCache();
$request_path = $this->getRequest()->getPath();
@ -168,7 +168,7 @@ abstract class CelerityResourceController extends PhabricatorController {
* @param string Resource type.
* @return bool True to enable caching.
*/
private function isCacheableResourceType($type) {
private function isLocallyCacheableResourceType($type) {
$types = array(
'js' => true,
'css' => true,

View file

@ -1,7 +1,7 @@
<?php
final class ConduitBoolParameterType
extends ConduitListParameterType {
extends ConduitParameterType {
protected function getParameterValue(array $request, $key) {
$value = parent::getParameterValue($request, $key);

View file

@ -50,9 +50,35 @@ abstract class PhabricatorSetupCheck extends Phobject {
return $cache->getKey('phabricator.setup.issue-keys');
}
final public static function setOpenSetupIssueKeys(array $keys) {
final public static function setOpenSetupIssueKeys(
array $keys,
$update_database) {
$cache = PhabricatorCaches::getSetupCache();
$cache->setKey('phabricator.setup.issue-keys', $keys);
if ($update_database) {
$db_cache = new PhabricatorKeyValueDatabaseCache();
try {
$json = phutil_json_encode($keys);
$db_cache->setKey('phabricator.setup.issue-keys', $json);
} catch (Exception $ex) {
// Ignore any write failures, since they likely just indicate that we
// have a database-related setup issue that needs to be resolved.
}
}
}
final public static function getOpenSetupIssueKeysFromDatabase() {
$db_cache = new PhabricatorKeyValueDatabaseCache();
try {
$value = $db_cache->getKey('phabricator.setup.issue-keys');
if (!strlen($value)) {
return null;
}
return phutil_json_decode($value);
} catch (Exception $ex) {
return null;
}
}
final public static function getUnignoredIssueKeys(array $all_issues) {
@ -97,7 +123,21 @@ abstract class PhabricatorSetupCheck extends Phobject {
->setView($view);
}
}
self::setOpenSetupIssueKeys(self::getUnignoredIssueKeys($issues));
$issue_keys = self::getUnignoredIssueKeys($issues);
self::setOpenSetupIssueKeys($issue_keys, $update_database = true);
} else if ($issue_keys) {
// If Phabricator is configured in a cluster with multiple web devices,
// we can end up with setup issues cached on every device. This can cause
// a warning banner to show on every device so that each one needs to
// be dismissed individually, which is pretty annoying. See T10876.
// To avoid this, check if the issues we found have already been cleared
// in the database. If they have, we'll just wipe out our own cache and
// move on.
$issue_keys = self::getOpenSetupIssueKeysFromDatabase();
if ($issue_keys !== null) {
self::setOpenSetupIssueKeys($issue_keys, $update_database = false);
}
}
// Try to repair configuration unless we have a clean bill of health on it.

View file

@ -11,7 +11,8 @@ final class PhabricatorConfigIssueListController
$issues = PhabricatorSetupCheck::runAllChecks();
PhabricatorSetupCheck::setOpenSetupIssueKeys(
PhabricatorSetupCheck::getUnignoredIssueKeys($issues));
PhabricatorSetupCheck::getUnignoredIssueKeys($issues),
$update_database = true);
$important = $this->buildIssueList(
$issues, PhabricatorSetupCheck::GROUP_IMPORTANT);

View file

@ -9,7 +9,8 @@ final class PhabricatorConfigIssueViewController
$issues = PhabricatorSetupCheck::runAllChecks();
PhabricatorSetupCheck::setOpenSetupIssueKeys(
PhabricatorSetupCheck::getUnignoredIssueKeys($issues));
PhabricatorSetupCheck::getUnignoredIssueKeys($issues),
$update_database = true);
if (empty($issues[$issue_key])) {
$content = id(new PHUIInfoView())

View file

@ -91,6 +91,13 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
=> 'DiffusionCommitEditController',
'manage/(?:(?P<panel>[^/]+)/)?'
=> 'DiffusionRepositoryManageController',
'uri/' => array(
'view/(?P<id>[0-9]\d*)/' => 'DiffusionRepositoryURIViewController',
'disable/(?P<id>[0-9]\d*)/'
=> 'DiffusionRepositoryURIDisableController',
$this->getEditRoutePattern('edit/')
=> 'DiffusionRepositoryURIEditController',
),
'edit/' => array(
'' => 'DiffusionRepositoryEditMainController',
'basic/' => 'DiffusionRepositoryEditBasicController',

View file

@ -29,21 +29,25 @@ final class DiffusionQueryCommitsConduitAPIMethod
protected function execute(ConduitAPIRequest $request) {
$need_messages = $request->getValue('needMessages');
$bypass_cache = $request->getValue('bypassCache');
$viewer = $request->getUser();
$query = id(new DiffusionCommitQuery())
->setViewer($request->getUser())
->setViewer($viewer)
->needCommitData(true);
$repository_phid = $request->getValue('repositoryPHID');
if ($repository_phid) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($request->getUser())
->setViewer($viewer)
->withPHIDs(array($repository_phid))
->executeOne();
if ($repository) {
$query->withRepository($repository);
if ($bypass_cache) {
$repository->synchronizeWorkingCopyBeforeRead();
id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository)
->synchronizeWorkingCopyBeforeRead();
}
}
}

View file

@ -124,10 +124,11 @@ abstract class DiffusionQueryConduitAPIMethod
// to prevent infinite recursion.
$is_cluster_request = $request->getIsClusterRequest();
$viewer = $request->getUser();
$repository = $drequest->getRepository();
$client = $repository->newConduitClient(
$request->getUser(),
$viewer,
$is_cluster_request);
if ($client) {
// We're proxying, so just make an intracluster call.
@ -149,7 +150,10 @@ abstract class DiffusionQueryConduitAPIMethod
// fetching the most up-to-date data? Synchronization can be slow, and a
// lot of web reads are probably fine if they're a few seconds out of
// date.
$repository->synchronizeWorkingCopyBeforeRead();
id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository)
->synchronizeWorkingCopyBeforeRead();
return $this->getResult($request);
}

View file

@ -0,0 +1,20 @@
<?php
final class DiffusionURIEditConduitAPIMethod
extends PhabricatorEditEngineAPIMethod {
public function getAPIMethodName() {
return 'diffusion.uri.edit';
}
public function newEditEngine() {
return new DiffusionURIEditEngine();
}
public function getMethodSummary() {
return pht(
'Apply transactions to create a new repository URI or edit an existing '.
'one.');
}
}

View file

@ -4,9 +4,66 @@ final class DiffusionRepositoryEditproController
extends DiffusionRepositoryEditController {
public function handleRequest(AphrontRequest $request) {
return id(new DiffusionRepositoryEditEngine())
->setController($this)
->buildResponse();
$engine = id(new DiffusionRepositoryEditEngine())
->setController($this);
$id = $request->getURIData('id');
if (!$id) {
$this->requireApplicationCapability(
DiffusionCreateRepositoriesCapability::CAPABILITY);
$vcs = $request->getStr('vcs');
$vcs_types = PhabricatorRepositoryType::getRepositoryTypeMap();
if (empty($vcs_types[$vcs])) {
return $this->buildVCSTypeResponse();
}
$engine
->addContextParameter('vcs', $vcs)
->setVersionControlSystem($vcs);
}
return $engine->buildResponse();
}
private function buildVCSTypeResponse() {
$vcs_types = PhabricatorRepositoryType::getRepositoryTypeMap();
$request = $this->getRequest();
$viewer = $this->getViewer();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Create Repository'));
$crumbs->setBorder(true);
$title = pht('Choose Repository Type');
$header = id(new PHUIHeaderView())
->setHeader(pht('Create Repository'))
->setHeaderIcon('fa-plus-square');
$layout = id(new AphrontMultiColumnView())
->setFluidLayout(true);
$create_uri = $request->getRequestURI();
foreach ($vcs_types as $vcs_key => $vcs_type) {
$action = id(new PHUIActionPanelView())
->setIcon(idx($vcs_type, 'icon'))
->setHeader(idx($vcs_type, 'create.header'))
->setHref($create_uri->alter('vcs', $vcs_key))
->setSubheader(idx($vcs_type, 'create.subheader'));
$layout->addColumn($action);
}
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($layout);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}

View file

@ -0,0 +1,72 @@
<?php
final class DiffusionRepositoryURIDisableController
extends DiffusionController {
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContextForEdit();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$id = $request->getURIData('id');
$uri = id(new PhabricatorRepositoryURIQuery())
->setViewer($viewer)
->withIDs(array($id))
->withRepositories(array($repository))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$uri) {
return new Aphront404Response();
}
$is_disabled = $uri->getIsDisabled();
$view_uri = $uri->getViewURI();
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorRepositoryURITransaction())
->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_DISABLE)
->setNewValue(!$is_disabled);
$editor = id(new DiffusionURIEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
->applyTransactions($uri, $xactions);
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
if ($is_disabled) {
$title = pht('Enable URI');
$body = pht(
'Enable this URI? Any configured behaviors will begin working '.
'again.');
$button = pht('Enable URI');
} else {
$title = pht('Disable URI');
$body = pht(
'Disable this URI? It will no longer be observed, fetched, mirrored, '.
'served or shown to users.');
$button = pht('Disable URI');
}
return $this->newDialog()
->setTitle($title)
->appendParagraph($body)
->addCancelButton($view_uri)
->addSubmitButton($button);
}
}

View file

@ -0,0 +1,21 @@
<?php
final class DiffusionRepositoryURIEditController
extends DiffusionController {
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContextForEdit();
if ($response) {
return $response;
}
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
return id(new DiffusionURIEditEngine())
->setController($this)
->setRepository($repository)
->buildResponse();
}
}

View file

@ -0,0 +1,182 @@
<?php
final class DiffusionRepositoryURIViewController
extends DiffusionController {
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$id = $request->getURIData('id');
$uri = id(new PhabricatorRepositoryURIQuery())
->setViewer($viewer)
->withIDs(array($id))
->withRepositories(array($repository))
->executeOne();
if (!$uri) {
return new Aphront404Response();
}
$title = array(
pht('URI'),
$repository->getDisplayName(),
);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
$repository->getDisplayName(),
$repository->getURI());
$crumbs->addTextCrumb(
pht('Manage'),
$repository->getPathURI('manage/'));
$panel_label = id(new DiffusionRepositoryURIsManagementPanel())
->getManagementPanelLabel();
$panel_uri = $repository->getPathURI('manage/uris/');
$crumbs->addTextCrumb($panel_label, $panel_uri);
$crumbs->addTextCrumb(pht('URI %d', $uri->getID()));
$header_text = pht(
'%s: URI %d',
$repository->getDisplayName(),
$uri->getID());
$header = id(new PHUIHeaderView())
->setHeader($header_text)
->setHeaderIcon('fa-pencil');
if ($uri->getIsDisabled()) {
$header->setStatus('fa-ban', 'dark', pht('Disabled'));
} else {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
}
$curtain = $this->buildCurtain($uri);
$details = $this->buildPropertySection($uri);
$timeline = $this->buildTransactionTimeline(
$uri,
new PhabricatorRepositoryURITransactionQuery());
$timeline->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(
array(
$details,
$timeline,
))
->setCurtain($curtain);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildCurtain(PhabricatorRepositoryURI $uri) {
$viewer = $this->getViewer();
$id = $uri->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$uri,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain = $this->newCurtainView($uri);
$edit_uri = $uri->getEditURI();
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit URI'))
->setHref($edit_uri)
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit));
if ($uri->getIsDisabled()) {
$disable_name = pht('Enable URI');
$disable_icon = 'fa-check';
} else {
$disable_name = pht('Disable URI');
$disable_icon = 'fa-ban';
}
$disable_uri = $uri->getRepository()->getPathURI("uri/disable/{$id}/");
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon($disable_icon)
->setName($disable_name)
->setHref($disable_uri)
->setWorkflow(true)
->setDisabled(!$can_edit));
return $curtain;
}
private function buildPropertySection(PhabricatorRepositoryURI $uri) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$properties->addProperty(pht('URI'), $uri->getDisplayURI());
$properties->addProperty(pht('Credential'), 'TODO');
$io_type = $uri->getEffectiveIOType();
$io_map = PhabricatorRepositoryURI::getIOTypeMap();
$io_spec = idx($io_map, $io_type, array());
$io_icon = idx($io_spec, 'icon');
$io_color = idx($io_spec, 'color');
$io_label = idx($io_spec, 'label', $io_type);
$io_note = idx($io_spec, 'note');
$io_item = id(new PHUIStatusItemView())
->setIcon($io_icon, $io_color)
->setTarget(phutil_tag('strong', array(), $io_label))
->setNote($io_note);
$io_view = id(new PHUIStatusListView())
->addItem($io_item);
$properties->addProperty(pht('I/O'), $io_view);
$display_type = $uri->getEffectiveDisplayType();
$display_map = PhabricatorRepositoryURI::getDisplayTypeMap();
$display_spec = idx($display_map, $display_type, array());
$display_icon = idx($display_spec, 'icon');
$display_color = idx($display_spec, 'color');
$display_label = idx($display_spec, 'label', $display_type);
$display_note = idx($display_spec, 'note');
$display_item = id(new PHUIStatusItemView())
->setIcon($display_icon, $display_color)
->setTarget(phutil_tag('strong', array(), $display_label))
->setNote($display_note);
$display_view = id(new PHUIStatusListView())
->addItem($display_item);
$properties->addProperty(pht('Display'), $display_view);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
}

View file

@ -540,12 +540,16 @@ final class DiffusionServeController extends DiffusionController {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository);
$did_write_lock = false;
if ($this->isReadOnlyRequest($repository)) {
$repository->synchronizeWorkingCopyBeforeRead();
$cluster_engine->synchronizeWorkingCopyBeforeRead();
} else {
$did_write_lock = true;
$repository->synchronizeWorkingCopyBeforeWrite($viewer);
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
}
$caught = null;
@ -559,7 +563,7 @@ final class DiffusionServeController extends DiffusionController {
}
if ($did_write_lock) {
$repository->synchronizeWorkingCopyAfterWrite();
$cluster_engine->synchronizeWorkingCopyAfterWrite();
}
unset($unguarded);

View file

@ -5,6 +5,17 @@ final class DiffusionRepositoryEditEngine
const ENGINECONST = 'diffusion.repository';
private $versionControlSystem;
public function setVersionControlSystem($version_control_system) {
$this->versionControlSystem = $version_control_system;
return $this;
}
public function getVersionControlSystem() {
return $this->versionControlSystem;
}
public function isEngineConfigurable() {
return false;
}
@ -27,7 +38,14 @@ final class DiffusionRepositoryEditEngine
protected function newEditableObject() {
$viewer = $this->getViewer();
return PhabricatorRepository::initializeNewRepository($viewer);
$repository = PhabricatorRepository::initializeNewRepository($viewer);
$vcs = $this->getVersionControlSystem();
if ($vcs) {
$repository->setVersionControlSystem($vcs);
}
return $repository;
}
protected function newObjectQuery() {
@ -75,6 +93,12 @@ final class DiffusionRepositoryEditEngine
->setObject($object)
->execute();
$track_value = $object->getDetail('branch-filter', array());
$track_value = array_keys($track_value);
$autoclose_value = $object->getDetail('close-commits-filter', array());
$autoclose_value = array_keys($autoclose_value);
return array(
id(new PhabricatorSelectEditField())
->setKey('vcs')
@ -132,6 +156,19 @@ final class DiffusionRepositoryEditEngine
->setConduitDescription(pht('Change the default text encoding.'))
->setConduitTypeDescription(pht('New text encoding.'))
->setValue($object->getDetail('encoding')),
id(new PhabricatorBoolEditField())
->setKey('allowDangerousChanges')
->setLabel(pht('Allow Dangerous Changes'))
->setIsCopyable(true)
->setIsConduitOnly(true)
->setOptions(
pht('Prevent Dangerous Changes'),
pht('Allow Dangerous Changes'))
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DANGEROUS)
->setDescription(pht('Permit dangerous changes to be made.'))
->setConduitDescription(pht('Allow or prevent dangerous changes.'))
->setConduitTypeDescription(pht('New protection setting.'))
->setValue($object->shouldAllowDangerousChanges()),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
@ -152,6 +189,99 @@ final class DiffusionRepositoryEditEngine
->setConduitDescription(pht('Set the default branch name.'))
->setConduitTypeDescription(pht('New default branch name.'))
->setValue($object->getDetail('default-branch')),
id(new PhabricatorTextAreaEditField())
->setIsStringList(true)
->setKey('trackOnly')
->setLabel(pht('Track Only'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY)
->setIsCopyable(true)
->setDescription(pht('Track only these branches.'))
->setConduitDescription(pht('Set the tracked branches.'))
->setConduitTypeDescription(pht('New tracked branchs.'))
->setValue($track_value),
id(new PhabricatorTextAreaEditField())
->setIsStringList(true)
->setKey('autocloseOnly')
->setLabel(pht('Autoclose Only'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY)
->setIsCopyable(true)
->setDescription(pht('Autoclose commits on only these branches.'))
->setConduitDescription(pht('Set the autoclose branches.'))
->setConduitTypeDescription(pht('New default tracked branchs.'))
->setValue($autoclose_value),
id(new PhabricatorTextEditField())
->setKey('stagingAreaURI')
->setLabel(pht('Staging Area URI'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_STAGING_URI)
->setIsCopyable(true)
->setDescription(pht('Staging area URI.'))
->setConduitDescription(pht('Set the staging area URI.'))
->setConduitTypeDescription(pht('New staging area URI.'))
->setValue($object->getStagingURI()),
id(new PhabricatorDatasourceEditField())
->setKey('automationBlueprintPHIDs')
->setLabel(pht('Use Blueprints'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS)
->setIsCopyable(true)
->setDatasource(new DrydockBlueprintDatasource())
->setDescription(pht('Automation blueprints.'))
->setConduitDescription(pht('Change automation blueprints.'))
->setConduitTypeDescription(pht('New blueprint PHIDs.'))
->setValue($object->getAutomationBlueprintPHIDs()),
id(new PhabricatorStringListEditField())
->setKey('symbolLanguages')
->setLabel(pht('Languages'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE)
->setIsCopyable(true)
->setDescription(
pht('Languages which define symbols in this repository.'))
->setConduitDescription(
pht('Change symbol languages for this repository.'))
->setConduitTypeDescription(
pht('New symbol langauges.'))
->setValue($object->getSymbolLanguages()),
id(new PhabricatorDatasourceEditField())
->setKey('symbolRepositoryPHIDs')
->setLabel(pht('Uses Symbols From'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES)
->setIsCopyable(true)
->setDatasource(new DiffusionRepositoryDatasource())
->setDescription(pht('Repositories to link symbols from.'))
->setConduitDescription(pht('Change symbol source repositories.'))
->setConduitTypeDescription(pht('New symbol repositories.'))
->setValue($object->getSymbolSources()),
id(new PhabricatorBoolEditField())
->setKey('publish')
->setLabel(pht('Publish/Notify'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_NOTIFY)
->setIsCopyable(true)
->setOptions(
pht('Disable Notifications, Feed, and Herald'),
pht('Enable Notifications, Feed, and Herald'))
->setDescription(pht('Configure how changes are published.'))
->setConduitDescription(pht('Change publishing options.'))
->setConduitTypeDescription(pht('New notification setting.'))
->setValue(!$object->getDetail('herald-disabled')),
id(new PhabricatorBoolEditField())
->setKey('autoclose')
->setLabel(pht('Autoclose'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE)
->setIsCopyable(true)
->setOptions(
pht('Disable Autoclose'),
pht('Enable Autoclose'))
->setDescription(pht('Stop or resume autoclosing in this repository.'))
->setConduitDescription(pht('Change autoclose setting.'))
->setConduitTypeDescription(pht('New autoclose setting.'))
->setValue(!$object->getDetail('disable-autoclose')),
id(new PhabricatorPolicyEditField())
->setKey('policy.push')
->setLabel(pht('Push Policy'))

View file

@ -0,0 +1,156 @@
<?php
final class DiffusionURIEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'diffusion.uri';
private $repository;
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->repository;
}
public function isEngineConfigurable() {
return false;
}
public function getEngineName() {
return pht('Repository URIs');
}
public function getSummaryHeader() {
return pht('Edit Repository URI');
}
public function getSummaryText() {
return pht('Creates and edits repository URIs.');
}
public function getEngineApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
protected function newEditableObject() {
$uri = PhabricatorRepositoryURI::initializeNewURI();
$repository = $this->getRepository();
if ($repository) {
$uri->setRepositoryPHID($repository->getPHID());
$uri->attachRepository($repository);
}
return $uri;
}
protected function newObjectQuery() {
return new PhabricatorRepositoryURIQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create Repository URI');
}
protected function getObjectCreateButtonText($object) {
return pht('Create Repository URI');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Repository URI %d', $object->getID());
}
protected function getObjectEditShortText($object) {
return pht('URI %d', $object->getID());
}
protected function getObjectCreateShortText() {
return pht('Create Repository URI');
}
protected function getObjectName() {
return pht('Repository URI');
}
protected function getObjectViewURI($object) {
return $object->getViewURI();
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
return array(
id(new PhabricatorHandlesEditField())
->setKey('repository')
->setAliases(array('repositoryPHID'))
->setLabel(pht('Repository'))
->setIsRequired(true)
->setIsConduitOnly(true)
->setTransactionType(
PhabricatorRepositoryURITransaction::TYPE_REPOSITORY)
->setDescription(pht('The repository this URI is associated with.'))
->setConduitDescription(
pht(
'Create a URI in a given repository. This transaction type '.
'must be present when creating a new URI and must not be '.
'present when editing an existing URI.'))
->setConduitTypeDescription(
pht('Repository PHID to create a new URI for.'))
->setSingleValue($object->getRepositoryPHID()),
id(new PhabricatorTextEditField())
->setKey('uri')
->setLabel(pht('URI'))
->setIsRequired(true)
->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_URI)
->setDescription(pht('The repository URI.'))
->setConduitDescription(pht('Change the repository URI.'))
->setConduitTypeDescription(pht('New repository URI.'))
->setValue($object->getURI()),
id(new PhabricatorSelectEditField())
->setKey('io')
->setLabel(pht('I/O Type'))
->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_IO)
->setDescription(pht('URI I/O behavior.'))
->setConduitDescription(pht('Adjust I/O behavior.'))
->setConduitTypeDescription(pht('New I/O behavior.'))
->setValue($object->getIOType())
->setOptions($object->getAvailableIOTypeOptions()),
id(new PhabricatorSelectEditField())
->setKey('display')
->setLabel(pht('Display Type'))
->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_DISPLAY)
->setDescription(pht('URI display behavior.'))
->setConduitDescription(pht('Change display behavior.'))
->setConduitTypeDescription(pht('New display behavior.'))
->setValue($object->getDisplayType())
->setOptions($object->getAvailableDisplayTypeOptions()),
id(new PhabricatorHandlesEditField())
->setKey('credential')
->setAliases(array('credentialPHID'))
->setLabel(pht('Credential'))
->setIsConduitOnly(true)
->setTransactionType(
PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL)
->setDescription(
pht('The credential to use when interacting with this URI.'))
->setConduitDescription(pht('Change the credential for this URI.'))
->setConduitTypeDescription(pht('New credential PHID, or null.'))
->setSingleValue($object->getCredentialPHID()),
id(new PhabricatorBoolEditField())
->setKey('disable')
->setLabel(pht('Disabled'))
->setIsConduitOnly(true)
->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_DISABLE)
->setDescription(pht('Active status of the URI.'))
->setConduitDescription(pht('Disable or activate the URI.'))
->setConduitTypeDescription(pht('True to disable the URI.'))
->setOptions(pht('Enable'), pht('Disable'))
->setValue($object->getIsDisabled()),
);
}
}

View file

@ -0,0 +1,373 @@
<?php
final class DiffusionURIEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
public function getEditorObjectsDescription() {
return pht('Diffusion URIs');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorRepositoryURITransaction::TYPE_REPOSITORY;
$types[] = PhabricatorRepositoryURITransaction::TYPE_URI;
$types[] = PhabricatorRepositoryURITransaction::TYPE_IO;
$types[] = PhabricatorRepositoryURITransaction::TYPE_DISPLAY;
$types[] = PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL;
$types[] = PhabricatorRepositoryURITransaction::TYPE_DISABLE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorRepositoryURITransaction::TYPE_URI:
return $object->getURI();
case PhabricatorRepositoryURITransaction::TYPE_IO:
return $object->getIOType();
case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
return $object->getDisplayType();
case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
return $object->getRepositoryPHID();
case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
return $object->getCredentialPHID();
case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
return (int)$object->getIsDisabled();
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorRepositoryURITransaction::TYPE_URI:
case PhabricatorRepositoryURITransaction::TYPE_IO:
case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
return $xaction->getNewValue();
case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
return (int)$xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorRepositoryURITransaction::TYPE_URI:
$object->setURI($xaction->getNewValue());
break;
case PhabricatorRepositoryURITransaction::TYPE_IO:
$object->setIOType($xaction->getNewValue());
break;
case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
$object->setDisplayType($xaction->getNewValue());
break;
case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
$object->setRepositoryPHID($xaction->getNewValue());
break;
case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
$object->setCredentialPHID($xaction->getNewValue());
break;
case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
$object->setIsDisabled($xaction->getNewValue());
break;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorRepositoryURITransaction::TYPE_URI:
case PhabricatorRepositoryURITransaction::TYPE_IO:
case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
$missing = $this->validateIsEmptyTextField(
$object->getRepositoryPHID(),
$xactions);
if ($missing) {
// NOTE: This isn't being marked as a missing field error because
// it's a fundamental, required property of the URI.
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht(
'When creating a repository URI, you must specify which '.
'repository the URI will belong to.'),
nonempty(last($xactions), null));
break;
}
$viewer = $this->getActor();
foreach ($xactions as $xaction) {
$repository_phid = $xaction->getNewValue();
// If this isn't changing anything, let it through as-is.
if ($repository_phid == $object->getRepositoryPHID()) {
continue;
}
if (!$this->getIsNewObject()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'The repository a URI is associated with is immutable, and '.
'can not be changed after the URI is created.'),
$xaction);
continue;
}
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs(array($repository_phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$repository) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'To create a URI for a repository ("%s"), it must exist and '.
'you must have permission to edit it.',
$repository_phid),
$xaction);
continue;
}
}
break;
case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
$viewer = $this->getActor();
foreach ($xactions as $xaction) {
$credential_phid = $xaction->getNewValue();
if ($credential_phid == $object->getCredentialPHID()) {
continue;
}
$credential = id(new PassphraseCredentialQuery())
->setViewer($viewer)
->withPHIDs(array($credential_phid))
->executeOne();
if (!$credential) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can only associate a credential ("%s") with a repository '.
'URI if it exists and you have permission to see it.',
$credential_phid),
$xaction);
continue;
}
}
break;
case PhabricatorRepositoryURITransaction::TYPE_URI:
$missing = $this->validateIsEmptyTextField(
$object->getURI(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('A repository URI must have a nonempty URI.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
break;
}
foreach ($xactions as $xaction) {
$new_uri = $xaction->getNewValue();
if ($new_uri == $object->getURI()) {
continue;
}
try {
PhabricatorRepository::assertValidRemoteURI($new_uri);
} catch (Exception $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
continue;
}
}
break;
case PhabricatorRepositoryURITransaction::TYPE_IO:
$available = $object->getAvailableIOTypeOptions();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if (empty($available[$new])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Value "%s" is not a valid display setting for this URI. '.
'Available types for this URI are: %s.',
implode(', ', array_keys($available))),
$xaction);
continue;
}
// If we are setting this URI to use "Observe", we must have no
// other "Observe" URIs and must also have no "Read/Write" URIs.
// If we are setting this URI to "Read/Write", we must have no
// other "Observe" URIs. It's OK to have other "Read/Write" URIs.
$no_observers = false;
$no_readwrite = false;
switch ($new) {
case PhabricatorRepositoryURI::IO_OBSERVE:
$no_readwrite = true;
$no_observers = true;
break;
case PhabricatorRepositoryURI::IO_READWRITE:
$no_observers = true;
break;
}
if ($no_observers || $no_readwrite) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getRepositoryPHID()))
->needURIs(true)
->executeOne();
$uris = $repository->getURIs();
$observe_conflict = null;
$readwrite_conflict = null;
foreach ($uris as $uri) {
// If this is the URI being edited, it can not conflict with
// itself.
if ($uri->getID() == $object->getID()) {
continue;
}
$io_type = $uri->getIoType();
if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
if ($no_readwrite) {
$readwite_conflict = $uri;
break;
}
}
if ($io_type == PhabricatorRepositoryURI::IO_OBSERVE) {
if ($no_observers) {
$observe_conflict = $uri;
break;
}
}
}
if ($observe_conflict) {
if ($new == PhabricatorRepositoryURI::IO_OBSERVE) {
$message = pht(
'You can not set this URI to use Observe IO because '.
'another URI for this repository is already configured '.
'in Observe IO mode. A repository can not observe two '.
'different remotes simultaneously. Turn off IO for the '.
'other URI first.');
} else {
$message = pht(
'You can not set this URI to use Read/Write IO because '.
'another URI for this repository is already configured '.
'in Observe IO mode. An observed repository can not be '.
'made writable. Turn off IO for the other URI first.');
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$message,
$xaction);
continue;
}
if ($readwrite_conflict) {
$message = pht(
'You can not set this URI to use Observe IO because '.
'another URI for this repository is already configured '.
'in Read/Write IO mode. A repository can not simultaneously '.
'be writable and observe a remote. Turn off IO for the '.
'other URI first.');
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$message,
$xaction);
continue;
}
}
}
break;
case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
$available = $object->getAvailableDisplayTypeOptions();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if (empty($available[$new])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Value "%s" is not a valid display setting for this URI. '.
'Available types for this URI are: %s.',
implode(', ', array_keys($available))));
}
}
break;
}
return $errors;
}
}

View file

@ -0,0 +1,34 @@
<?php
final class DiffusionRepositoryURIsSearchEngineAttachment
extends PhabricatorSearchEngineAttachment {
public function getAttachmentName() {
return pht('Repository URIs');
}
public function getAttachmentDescription() {
return pht('Get a list of associated URIs for each repository.');
}
public function willLoadAttachmentData($query, $spec) {
$query->needURIs(true);
}
public function getAttachmentForObject($object, $data, $spec) {
$uris = array();
foreach ($object->getURIs() as $uri) {
$uris[] = array(
'id' => $uri->getID(),
'type' => phid_get_type($uri->getPHID()),
'phid' => $uri->getPHID(),
'fields' => $uri->getFieldValuesForConduit(),
);
}
return array(
'uris' => $uris,
);
}
}

View file

@ -0,0 +1,60 @@
<?php
final class DiffusionRepositoryActionsManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'actions';
public function getManagementPanelLabel() {
return pht('Actions');
}
public function getManagementPanelOrder() {
return 1100;
}
protected function buildManagementPanelActions() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$actions_uri = $repository->getPathURI('edit/actions/');
return array(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Actions'))
->setHref($actions_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit),
);
}
public function buildManagementPanelContent() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer)
->setActionList($this->newActions());
$notify = $repository->getDetail('herald-disabled')
? pht('Off')
: pht('On');
$notify = phutil_tag('em', array(), $notify);
$view->addProperty(pht('Publish/Notify'), $notify);
$autoclose = $repository->getDetail('disable-autoclose')
? pht('Off')
: pht('On');
$autoclose = phutil_tag('em', array(), $autoclose);
$view->addProperty(pht('Autoclose'), $autoclose);
return $this->newBox(pht('Branches'), $view);
}
}

View file

@ -0,0 +1,69 @@
<?php
final class DiffusionRepositoryAutomationManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'automation';
public function getManagementPanelLabel() {
return pht('Automation');
}
public function getManagementPanelOrder() {
return 800;
}
protected function buildManagementPanelActions() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$can_test = $can_edit && $repository->canPerformAutomation();
$automation_uri = $repository->getPathURI('edit/automation/');
$test_uri = $repository->getPathURI('edit/testautomation/');
return array(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Automation'))
->setHref($automation_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit),
id(new PhabricatorActionView())
->setIcon('fa-gamepad')
->setName(pht('Test Configuration'))
->setWorkflow(true)
->setDisabled(!$can_test)
->setHref($test_uri),
);
}
public function buildManagementPanelContent() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer)
->setActionList($this->newActions());
$blueprint_phids = $repository->getAutomationBlueprintPHIDs();
if (!$blueprint_phids) {
$blueprint_view = phutil_tag('em', array(), pht('Not Configured'));
} else {
$blueprint_view = id(new DrydockObjectAuthorizationView())
->setUser($viewer)
->setObjectPHID($repository->getPHID())
->setBlueprintPHIDs($blueprint_phids);
}
$view->addProperty(pht('Automation'), $blueprint_view);
return $this->newBox(pht('Automation'), $view);
}
}

View file

@ -26,6 +26,7 @@ final class DiffusionRepositoryBasicsManagementPanel
$activate_uri = $repository->getPathURI('edit/activate/');
$delete_uri = $repository->getPathURI('edit/delete/');
$encoding_uri = $repository->getPathURI('edit/encoding/');
$dangerous_uri = $repository->getPathURI('edit/dangerous/');
if ($repository->isTracked()) {
$activate_icon = 'fa-pause';
@ -35,6 +36,17 @@ final class DiffusionRepositoryBasicsManagementPanel
$activate_label = pht('Activate Repository');
}
$should_dangerous = $repository->shouldAllowDangerousChanges();
if ($should_dangerous) {
$dangerous_icon = 'fa-shield';
$dangerous_name = pht('Prevent Dangerous Changes');
$can_dangerous = $can_edit;
} else {
$dangerous_icon = 'fa-bullseye';
$dangerous_name = pht('Allow Dangerous Changes');
$can_dangerous = ($can_edit && $repository->canAllowDangerousChanges());
}
return array(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
@ -48,6 +60,12 @@ final class DiffusionRepositoryBasicsManagementPanel
->setHref($encoding_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit),
id(new PhabricatorActionView())
->setIcon($dangerous_icon)
->setName($dangerous_name)
->setHref($dangerous_uri)
->setDisabled(!$can_dangerous)
->setWorkflow(true),
id(new PhabricatorActionView())
->setHref($activate_uri)
->setIcon($activate_icon)
@ -110,6 +128,20 @@ final class DiffusionRepositoryBasicsManagementPanel
}
$view->addProperty(pht('Encoding'), $encoding);
$can_dangerous = $repository->canAllowDangerousChanges();
if (!$can_dangerous) {
$dangerous = phutil_tag('em', array(), pht('Not Preventable'));
} else {
$should_dangerous = $repository->shouldAllowDangerousChanges();
if ($should_dangerous) {
$dangerous = pht('Allowed');
} else {
$dangerous = pht('Not Allowed');
}
}
$view->addProperty(pht('Dangerous Changes'), $dangerous);
return $view;
}

View file

@ -0,0 +1,68 @@
<?php
final class DiffusionRepositoryBranchesManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'branches';
public function getManagementPanelLabel() {
return pht('Branches');
}
public function getManagementPanelOrder() {
return 1000;
}
protected function buildManagementPanelActions() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$branches_uri = $repository->getPathURI('edit/branches/');
return array(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Branches'))
->setHref($branches_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit),
);
}
public function buildManagementPanelContent() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer)
->setActionList($this->newActions());
$default_branch = nonempty(
$repository->getHumanReadableDetail('default-branch'),
phutil_tag('em', array(), $repository->getDefaultBranch()));
$view->addProperty(pht('Default Branch'), $default_branch);
$track_only = nonempty(
$repository->getHumanReadableDetail('branch-filter', array()),
phutil_tag('em', array(), pht('Track All Branches')));
$view->addProperty(pht('Track Only'), $track_only);
$autoclose_only = nonempty(
$repository->getHumanReadableDetail('close-commits-filter', array()),
phutil_tag('em', array(), pht('Autoclose On All Branches')));
if ($repository->getDetail('disable-autoclose')) {
$autoclose_only = phutil_tag('em', array(), pht('Disabled'));
}
$view->addProperty(pht('Autoclose Only'), $autoclose_only);
return $this->newBox(pht('Branches'), $view);
}
}

View file

@ -10,7 +10,7 @@ final class DiffusionRepositoryHistoryManagementPanel
}
public function getManagementPanelOrder() {
return 900;
return 2000;
}
public function buildManagementPanelContent() {

View file

@ -0,0 +1,55 @@
<?php
final class DiffusionRepositoryStagingManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'staging';
public function getManagementPanelLabel() {
return pht('Staging Area');
}
public function getManagementPanelOrder() {
return 700;
}
protected function buildManagementPanelActions() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$staging_uri = $repository->getPathURI('edit/staging/');
return array(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Staging'))
->setHref($staging_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit),
);
}
public function buildManagementPanelContent() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer)
->setActionList($this->newActions());
$staging_uri = $repository->getStagingURI();
if (!$staging_uri) {
$staging_uri = phutil_tag('em', array(), pht('No Staging Area'));
}
$view->addProperty(pht('Staging Area URI'), $staging_uri);
return $this->newBox(pht('Staging Area'), $view);
}
}

View file

@ -46,8 +46,12 @@ final class DiffusionRepositoryStatusManagementPanel
pht('Update Frequency'),
$this->buildRepositoryUpdateInterval($repository));
$messages = id(new PhabricatorRepositoryStatusMessage())
->loadAllWhere('repositoryID = %d', $repository->getID());
$messages = mpull($messages, null, 'getStatusType');
list($status, $raw_error) = $this->buildRepositoryStatus($repository);
$status = $this->buildRepositoryStatus($repository, $messages);
$raw_error = $this->buildRepositoryRawError($repository, $messages);
$view->addProperty(pht('Status'), $status);
if ($raw_error) {
@ -80,17 +84,14 @@ final class DiffusionRepositoryStatusManagementPanel
}
private function buildRepositoryStatus(
PhabricatorRepository $repository) {
PhabricatorRepository $repository,
array $messages) {
$viewer = $this->getViewer();
$is_cluster = $repository->getAlmanacServicePHID();
$view = new PHUIStatusListView();
$messages = id(new PhabricatorRepositoryStatusMessage())
->loadAllWhere('repositoryID = %d', $repository->getID());
$messages = mpull($messages, null, 'getStatusType');
if ($repository->isTracked()) {
$view->addItem(
id(new PHUIStatusItemView())
@ -361,8 +362,6 @@ final class DiffusionRepositoryStatusManagementPanel
}
}
$raw_error = null;
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH);
if ($message) {
switch ($message->getStatusCode()) {
@ -377,8 +376,6 @@ final class DiffusionRepositoryStatusManagementPanel
'access the repository.');
}
$raw_error = $message;
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
@ -432,11 +429,30 @@ final class DiffusionRepositoryStatusManagementPanel
->setNote(pht('This repository will be updated soon!')));
}
return $view;
}
private function buildRepositoryRawError(
PhabricatorRepository $repository,
array $messages) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$raw_error = null;
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$raw_error = $message->getParameter('message');
break;
}
}
if ($raw_error !== null) {
if (!$can_edit) {
$raw_message = pht(
@ -450,7 +466,7 @@ final class DiffusionRepositoryStatusManagementPanel
$raw_message = null;
}
return array($view, $raw_message);
return $raw_message;
}

View file

@ -1,12 +1,12 @@
<?php
final class DiffusionRepositoryClusterManagementPanel
final class DiffusionRepositoryStorageManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'cluster';
const PANELKEY = 'storage';
public function getManagementPanelLabel() {
return pht('Cluster Configuration');
return pht('Storage');
}
public function getManagementPanelOrder() {
@ -14,6 +14,45 @@ final class DiffusionRepositoryClusterManagementPanel
}
public function buildManagementPanelContent() {
return array(
$this->buildStorageStatusPanel(),
$this->buildClusterStatusPanel(),
);
}
private function buildStorageStatusPanel() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
if ($repository->usesLocalWorkingCopy()) {
$storage_path = $repository->getHumanReadableDetail('local-path');
} else {
$storage_path = phutil_tag('em', array(), pht('No Local Working Copy'));
}
$service_phid = $repository->getAlmanacServicePHID();
if ($service_phid) {
$storage_service = $viewer->renderHandle($service_phid);
} else {
$storage_service = phutil_tag('em', array(), pht('Local'));
}
$view->addProperty(pht('Storage Path'), $storage_path);
$view->addProperty(pht('Storage Cluster'), $storage_service);
$header = id(new PHUIHeaderView())
->setHeader(pht('Storage'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addPropertyList($view);
}
private function buildClusterStatusPanel() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
@ -175,18 +214,6 @@ final class DiffusionRepositoryClusterManagementPanel
->setTag('a')
->setText(pht('Documentation')));
if ($service) {
$header->setSubheader(
pht(
'This repository is hosted on %s.',
phutil_tag(
'a',
array(
'href' => $service->getURI(),
),
$service->getName())));
}
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)

View file

@ -0,0 +1,64 @@
<?php
final class DiffusionRepositorySymbolsManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'symbols';
public function getManagementPanelLabel() {
return pht('Symbols');
}
public function getManagementPanelOrder() {
return 900;
}
protected function buildManagementPanelActions() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$symbols_uri = $repository->getPathURI('edit/symbols/');
return array(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Symbols'))
->setHref($symbols_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit),
);
}
public function buildManagementPanelContent() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer)
->setActionList($this->newActions());
$languages = $repository->getSymbolLanguages();
if ($languages) {
$languages = implode(', ', $languages);
} else {
$languages = phutil_tag('em', array(), pht('Any'));
}
$view->addProperty(pht('Languages'), $languages);
$sources = $repository->getSymbolSources();
if ($sources) {
$sources = $viewer->renderHandleList($sources);
} else {
$sources = phutil_tag('em', array(), pht('This Repository Only'));
}
$view->addProperty(pht('Uses Symbols From'), $sources);
return $this->newBox(pht('Symbols'), $view);
}
}

View file

@ -16,8 +16,6 @@ final class DiffusionRepositoryURIsManagementPanel
public function buildManagementPanelContent() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$repository->attachURIs(array());
$uris = $repository->getURIs();
Javelin::initBehavior('phabricator-tooltips');
@ -25,6 +23,12 @@ final class DiffusionRepositoryURIsManagementPanel
foreach ($uris as $uri) {
$uri_name = $uri->getDisplayURI();
$uri_name = phutil_tag(
'a',
array(
'href' => $uri->getViewURI(),
),
$uri_name);
if ($uri->getIsDisabled()) {
$status_icon = 'fa-times grey';
@ -34,48 +38,30 @@ final class DiffusionRepositoryURIsManagementPanel
$uri_status = id(new PHUIIconView())->setIcon($status_icon);
switch ($uri->getEffectiveIOType()) {
case PhabricatorRepositoryURI::IO_OBSERVE:
$io_icon = 'fa-download green';
$io_label = pht('Observe');
break;
case PhabricatorRepositoryURI::IO_MIRROR:
$io_icon = 'fa-upload green';
$io_label = pht('Mirror');
break;
case PhabricatorRepositoryURI::IO_NONE:
$io_icon = 'fa-times grey';
$io_label = pht('No I/O');
break;
case PhabricatorRepositoryURI::IO_READ:
$io_icon = 'fa-folder blue';
$io_label = pht('Read Only');
break;
case PhabricatorRepositoryURI::IO_READWRITE:
$io_icon = 'fa-folder-open blue';
$io_label = pht('Read/Write');
break;
}
$io_type = $uri->getEffectiveIOType();
$io_map = PhabricatorRepositoryURI::getIOTypeMap();
$io_spec = idx($io_map, $io_type, array());
$io_icon = idx($io_spec, 'icon');
$io_color = idx($io_spec, 'color');
$io_label = idx($io_spec, 'label', $io_type);
$uri_io = array(
id(new PHUIIconView())->setIcon($io_icon),
id(new PHUIIconView())->setIcon("{$io_icon} {$io_color}"),
' ',
$io_label,
);
switch ($uri->getEffectiveDisplayType()) {
case PhabricatorRepositoryURI::DISPLAY_NEVER:
$display_icon = 'fa-eye-slash grey';
$display_label = pht('Hidden');
break;
case PhabricatorRepositoryURI::DISPLAY_ALWAYS:
$display_icon = 'fa-eye green';
$display_label = pht('Visible');
break;
}
$display_type = $uri->getEffectiveDisplayType();
$display_map = PhabricatorRepositoryURI::getDisplayTypeMap();
$display_spec = idx($display_map, $display_type, array());
$display_icon = idx($display_spec, 'icon');
$display_color = idx($display_spec, 'color');
$display_label = idx($display_spec, 'label', $display_type);
$uri_display = array(
id(new PHUIIconView())->setIcon($display_icon),
id(new PHUIIconView())->setIcon("{$display_icon} {$display_color}"),
' ',
$display_label,
);
@ -105,11 +91,17 @@ final class DiffusionRepositoryURIsManagementPanel
null,
));
$doc_href = PhabricatorEnv::getDoclink(
'Diffusion User Guide: Repository URIs');
$doc_href = PhabricatorEnv::getDoclink('Diffusion User Guide: URIs');
$add_href = $repository->getPathURI('uri/edit/');
$header = id(new PHUIHeaderView())
->setHeader(pht('Repository URIs'))
->addActionLink(
id(new PHUIButtonView())
->setIcon('fa-plus')
->setHref($add_href)
->setTag('a')
->setText(pht('Add New URI')))
->addActionLink(
id(new PHUIButtonView())
->setIcon('fa-book')

View file

@ -0,0 +1,625 @@
<?php
/**
* Manages repository synchronization for cluster repositories.
*
* @task config Configuring Synchronization
* @task sync Cluster Synchronization
* @task internal Internals
*/
final class DiffusionRepositoryClusterEngine extends Phobject {
private $repository;
private $viewer;
private $logger;
private $clusterWriteLock;
private $clusterWriteVersion;
private $clusterWriteOwner;
/* -( Configuring Synchronization )---------------------------------------- */
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->repository;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setLog(DiffusionRepositoryClusterEngineLogInterface $log) {
$this->logger = $log;
return $this;
}
/* -( Cluster Synchronization )-------------------------------------------- */
/**
* Synchronize repository version information after creating a repository.
*
* This initializes working copy versions for all currently bound devices to
* 0, so that we don't get stuck making an ambiguous choice about which
* devices are leaders when we later synchronize before a read.
*
* @task sync
*/
public function synchronizeWorkingCopyAfterCreation() {
if (!$this->shouldEnableSynchronization()) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$bindings = $service->getActiveBindings();
foreach ($bindings as $binding) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$binding->getDevicePHID(),
0);
}
return $this;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyBeforeRead() {
if (!$this->shouldEnableSynchronization()) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
$read_lock = PhabricatorRepositoryWorkingCopyVersion::getReadLock(
$repository_phid,
$device_phid);
$lock_wait = phutil_units('2 minutes in seconds');
$this->logLine(
pht(
'Waiting up to %s second(s) for a cluster read lock on "%s"...',
new PhutilNumber($lock_wait),
$device->getName()));
try {
$start = PhabricatorTime::getNow();
$read_lock->lock($lock_wait);
$waited = (PhabricatorTime::getNow() - $start);
if ($waited) {
$this->logLine(
pht(
'Acquired read lock after %s second(s).',
new PhutilNumber($waited)));
} else {
$this->logLine(
pht(
'Acquired read lock immediately.'));
}
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Failed to acquire read lock after waiting %s second(s). You '.
'may be able to retry later.',
new PhutilNumber($lock_wait)));
}
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
$versions = mpull($versions, null, 'getDevicePHID');
$this_version = idx($versions, $device_phid);
if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion();
} else {
$this_version = -1;
}
if ($versions) {
// This is the normal case, where we have some version information and
// can identify which nodes are leaders. If the current node is not a
// leader, we want to fetch from a leader and then update our version.
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
if ($max_version > $this_version) {
$fetchable = array();
foreach ($versions as $version) {
if ($version->getRepositoryVersion() == $max_version) {
$fetchable[] = $version->getDevicePHID();
}
}
$this->synchronizeWorkingCopyFromDevices($fetchable);
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
$max_version);
} else {
$this->logLine(
pht(
'Device "%s" is already a cluster leader and does not need '.
'to be synchronized.',
$device->getName()));
}
$result_version = $max_version;
} else {
// If no version records exist yet, we need to be careful, because we
// can not tell which nodes are leaders.
// There might be several nodes with arbitrary existing data, and we have
// no way to tell which one has the "right" data. If we pick wrong, we
// might erase some or all of the data in the repository.
// Since this is dangeorus, we refuse to guess unless there is only one
// device. If we're the only device in the group, we obviously must be
// a leader.
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$bindings = $service->getActiveBindings();
$device_map = array();
foreach ($bindings as $binding) {
$device_map[$binding->getDevicePHID()] = true;
}
if (count($device_map) > 1) {
throw new Exception(
pht(
'Repository "%s" exists on more than one device, but no device '.
'has any repository version information. Phabricator can not '.
'guess which copy of the existing data is authoritative. Remove '.
'all but one device from service to mark the remaining device '.
'as the authority.',
$repository->getDisplayName()));
}
if (empty($device_map[$device->getPHID()])) {
throw new Exception(
pht(
'Repository "%s" is being synchronized on device "%s", but '.
'this device is not bound to the corresponding cluster '.
'service ("%s").',
$repository->getDisplayName(),
$device->getName(),
$service->getName()));
}
// The current device is the only device in service, so it must be a
// leader. We can safely have any future nodes which come online read
// from it.
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
0);
$result_version = 0;
}
$read_lock->unlock();
return $result_version;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyBeforeWrite() {
if (!$this->shouldEnableSynchronization()) {
return;
}
$repository = $this->getRepository();
$viewer = $this->getViewer();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
$table = new PhabricatorRepositoryWorkingCopyVersion();
$locked_connection = $table->establishConnection('w');
$write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock(
$repository_phid);
$write_lock->useSpecificConnection($locked_connection);
$lock_wait = phutil_units('2 minutes in seconds');
$this->logLine(
pht(
'Waiting up to %s second(s) for a cluster write lock...',
new PhutilNumber($lock_wait)));
try {
$start = PhabricatorTime::getNow();
$write_lock->lock($lock_wait);
$waited = (PhabricatorTime::getNow() - $start);
if ($waited) {
$this->logLine(
pht(
'Acquired write lock after %s second(s).',
new PhutilNumber($waited)));
} else {
$this->logLine(
pht(
'Acquired write lock immediately.'));
}
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Failed to acquire write lock after waiting %s second(s). You '.
'may be able to retry later.',
new PhutilNumber($lock_wait)));
}
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
foreach ($versions as $version) {
if (!$version->getIsWriting()) {
continue;
}
throw new Exception(
pht(
'An previous write to this repository was interrupted; refusing '.
'new writes. This issue requires operator intervention to resolve, '.
'see "Write Interruptions" in the "Cluster: Repositories" in the '.
'documentation for instructions.'));
}
try {
$max_version = $this->synchronizeWorkingCopyBeforeRead();
} catch (Exception $ex) {
$write_lock->unlock();
throw $ex;
}
$pid = getmypid();
$hash = Filesystem::readRandomCharacters(12);
$this->clusterWriteOwner = "{$pid}.{$hash}";
PhabricatorRepositoryWorkingCopyVersion::willWrite(
$locked_connection,
$repository_phid,
$device_phid,
array(
'userPHID' => $viewer->getPHID(),
'epoch' => PhabricatorTime::getNow(),
'devicePHID' => $device_phid,
),
$this->clusterWriteOwner);
$this->clusterWriteVersion = $max_version;
$this->clusterWriteLock = $write_lock;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyAfterWrite() {
if (!$this->shouldEnableSynchronization()) {
return;
}
if (!$this->clusterWriteLock) {
throw new Exception(
pht(
'Trying to synchronize after write, but not holding a write '.
'lock!'));
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
// It is possible that we've lost the global lock while receiving the push.
// For example, the master database may have been restarted between the
// time we acquired the global lock and now, when the push has finished.
// We wrote a durable lock while we were holding the the global lock,
// essentially upgrading our lock. We can still safely release this upgraded
// lock even if we're no longer holding the global lock.
// If we fail to release the lock, the repository will be frozen until
// an operator can figure out what happened, so we try pretty hard to
// reconnect to the database and release the lock.
$now = PhabricatorTime::getNow();
$duration = phutil_units('5 minutes in seconds');
$try_until = $now + $duration;
$did_release = false;
$already_failed = false;
while (PhabricatorTime::getNow() <= $try_until) {
try {
// NOTE: This means we're still bumping the version when pushes fail. We
// could select only un-rejected events instead to bump a little less
// often.
$new_log = id(new PhabricatorRepositoryPushEventQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRepositoryPHIDs(array($repository_phid))
->setLimit(1)
->executeOne();
$old_version = $this->clusterWriteVersion;
if ($new_log) {
$new_version = $new_log->getID();
} else {
$new_version = $old_version;
}
PhabricatorRepositoryWorkingCopyVersion::didWrite(
$repository_phid,
$device_phid,
$this->clusterWriteVersion,
$new_log->getID(),
$this->clusterWriteOwner);
$did_release = true;
break;
} catch (AphrontConnectionQueryException $ex) {
$connection_exception = $ex;
} catch (AphrontConnectionLostQueryException $ex) {
$connection_exception = $ex;
}
if (!$already_failed) {
$already_failed = true;
$this->logLine(
pht('CRITICAL. Failed to release cluster write lock!'));
$this->logLine(
pht(
'The connection to the master database was lost while receiving '.
'the write.'));
$this->logLine(
pht(
'This process will spend %s more second(s) attempting to '.
'recover, then give up.',
new PhutilNumber($duration)));
}
sleep(1);
}
if ($did_release) {
if ($already_failed) {
$this->logLine(
pht('RECOVERED. Link to master database was restored.'));
}
$this->logLine(pht('Released cluster write lock.'));
} else {
throw new Exception(
pht(
'Failed to reconnect to master database and release held write '.
'lock ("%s") on device "%s" for repository "%s" after trying '.
'for %s seconds(s). This repository will be frozen.',
$this->clusterWriteOwner,
$device->getName(),
$this->getDisplayName(),
new PhutilNumber($duration)));
}
// We can continue even if we've lost this lock, everything is still
// consistent.
try {
$this->clusterWriteLock->unlock();
} catch (Exception $ex) {
// Ignore.
}
$this->clusterWriteLock = null;
$this->clusterWriteOwner = null;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function shouldEnableSynchronization() {
$repository = $this->getRepository();
$service_phid = $repository->getAlmanacServicePHID();
if (!$service_phid) {
return false;
}
// TODO: For now, this is only supported for Git.
if (!$repository->isGit()) {
return false;
}
// TODO: It may eventually make sense to try to version and synchronize
// observed repositories (so that daemons don't do reads against out-of
// date hosts), but don't bother for now.
if (!$repository->isHosted()) {
return false;
}
$device = AlmanacKeys::getLiveDevice();
if (!$device) {
return false;
}
return true;
}
/**
* @task internal
*/
private function synchronizeWorkingCopyFromDevices(array $device_phids) {
$repository = $this->getRepository();
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$device_map = array_fuse($device_phids);
$bindings = $service->getActiveBindings();
$fetchable = array();
foreach ($bindings as $binding) {
// We can't fetch from nodes which don't have the newest version.
$device_phid = $binding->getDevicePHID();
if (empty($device_map[$device_phid])) {
continue;
}
// TODO: For now, only fetch over SSH. We could support fetching over
// HTTP eventually.
if ($binding->getAlmanacPropertyValue('protocol') != 'ssh') {
continue;
}
$fetchable[] = $binding;
}
if (!$fetchable) {
throw new Exception(
pht(
'Leader lost: no up-to-date nodes in repository cluster are '.
'fetchable.'));
}
$caught = null;
foreach ($fetchable as $binding) {
try {
$this->synchronizeWorkingCopyFromBinding($binding);
$caught = null;
break;
} catch (Exception $ex) {
$caught = $ex;
}
}
if ($caught) {
throw $caught;
}
}
/**
* @task internal
*/
private function synchronizeWorkingCopyFromBinding($binding) {
$repository = $this->getRepository();
$device = AlmanacKeys::getLiveDevice();
$this->logLine(
pht(
'Synchronizing this device ("%s") from cluster leader ("%s") before '.
'read.',
$device->getName(),
$binding->getDevice()->getName()));
$fetch_uri = $repository->getClusterRepositoryURIFromBinding($binding);
$local_path = $repository->getLocalPath();
if ($repository->isGit()) {
if (!Filesystem::pathExists($local_path)) {
throw new Exception(
pht(
'Repository "%s" does not have a working copy on this device '.
'yet, so it can not be synchronized. Wait for the daemons to '.
'construct one or run `bin/repository update %s` on this host '.
'("%s") to build it explicitly.',
$repository->getDisplayName(),
$repository->getMonogram(),
$device->getName()));
}
$argv = array(
'fetch --prune -- %s %s',
$fetch_uri,
'+refs/*:refs/*',
);
} else {
throw new Exception(pht('Binding sync only supported for git!'));
}
$future = DiffusionCommandEngine::newCommandEngine($repository)
->setArgv($argv)
->setConnectAsDevice(true)
->setSudoAsDaemon(true)
->setProtocol($fetch_uri->getProtocol())
->newFuture();
$future->setCWD($local_path);
try {
$future->resolvex();
} catch (Exception $ex) {
$this->logLine(
pht(
'Synchronization of "%s" from leader "%s" failed: %s',
$device->getName(),
$binding->getDevice()->getName(),
$ex->getMessage()));
throw $ex;
}
}
/**
* @task internal
*/
private function logLine($message) {
return $this->logText("# {$message}\n");
}
/**
* @task internal
*/
private function logText($message) {
$log = $this->logger;
if ($log) {
$log->writeClusterEngineLogMessage($message);
}
return $this;
}
}

View file

@ -0,0 +1,7 @@
<?php
interface DiffusionRepositoryClusterEngineLogInterface {
public function writeClusterEngineLogMessage($message);
}

View file

@ -142,7 +142,8 @@ abstract class DiffusionRequest extends Phobject {
$query = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier));
->withIdentifiers(array($identifier))
->needURIs(true);
if ($need_edit) {
$query->requireCapabilities(

View file

@ -15,19 +15,38 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
protected function executeRepositoryOperations() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$device = AlmanacKeys::getLiveDevice();
// This is a write, and must have write access.
$this->requireWriteAccess();
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository)
->setLog($this);
if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
$did_synchronize = false;
if ($device) {
$this->writeClusterEngineLogMessage(
pht(
"# Push received by \"%s\", forwarding to cluster host.\n",
$device->getName()));
}
} else {
$command = csprintf('git-receive-pack %s', $repository->getLocalPath());
$did_synchronize = true;
$viewer = $this->getUser();
$repository->synchronizeWorkingCopyBeforeWrite($viewer);
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
if ($device) {
$this->writeClusterEngineLogMessage(
pht(
"# Ready to receive on cluster host \"%s\".\n",
$device->getName()));
}
}
$caught = null;
@ -40,7 +59,7 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
// We've committed the write (or rejected it), so we can release the lock
// without waiting for the client to receive the acknowledgement.
if ($did_synchronize) {
$repository->synchronizeWorkingCopyAfterWrite();
$cluster_engine->synchronizeWorkingCopyAfterWrite();
}
if ($caught) {

View file

@ -1,12 +1,19 @@
<?php
abstract class DiffusionGitSSHWorkflow extends DiffusionSSHWorkflow {
abstract class DiffusionGitSSHWorkflow
extends DiffusionSSHWorkflow
implements DiffusionRepositoryClusterEngineLogInterface {
protected function writeError($message) {
// Git assumes we'll add our own newlines.
return parent::writeError($message."\n");
}
public function writeClusterEngineLogMessage($message) {
parent::writeError($message);
$this->getErrorChannel()->update();
}
protected function identifyRepository() {
$args = $this->getArgs();
$path = head($args->getArg('dir'));

View file

@ -15,15 +15,35 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
protected function executeRepositoryOperations() {
$repository = $this->getRepository();
$viewer = $this->getUser();
$device = AlmanacKeys::getLiveDevice();
$skip_sync = $this->shouldSkipReadSynchronization();
if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
if ($device) {
$this->writeClusterEngineLogMessage(
pht(
"# Fetch received by \"%s\", forwarding to cluster host.\n",
$device->getName()));
}
} else {
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
if (!$skip_sync) {
$repository->synchronizeWorkingCopyBeforeRead();
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository)
->setLog($this)
->synchronizeWorkingCopyBeforeRead();
if ($device) {
$this->writeClusterEngineLogMessage(
pht(
"# Cleared to fetch on cluster host \"%s\".\n",
$device->getName()));
}
}
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);

View file

@ -55,6 +55,21 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
return $this;
}
protected function getCurrentDeviceName() {
$device = AlmanacKeys::getLiveDevice();
if ($device) {
return $device->getName();
}
return php_uname('n');
}
protected function getTargetDeviceName() {
// TODO: This should use the correct device identity.
$uri = new PhutilURI($this->proxyURI);
return $uri->getDomain();
}
protected function shouldProxy() {
return (bool)$this->proxyURI;
}

View file

@ -173,14 +173,15 @@ final class PholioMockViewController extends PholioController {
}
private function buildDescriptionView(PholioMock $mock) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$description = $mock->getDescription();
if (strlen($description)) {
$properties->addImageContent($description);
$properties->addTextContent(
new PHUIRemarkupView($viewer, $description));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Mock Description'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)

View file

@ -92,6 +92,16 @@ final class PholioMockImagesView extends AphrontView {
$current_set++;
}
$description = $engine->getOutput($image, 'default');
if (strlen($description)) {
$description = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$description);
}
$history_uri = '/pholio/image/history/'.$image->getID().'/';
$images[] = array(
'id' => $image->getID(),
@ -105,7 +115,7 @@ final class PholioMockImagesView extends AphrontView {
'width' => $x,
'height' => $y,
'title' => $image->getName(),
'descriptionMarkup' => $engine->getOutput($image, 'default'),
'descriptionMarkup' => $description,
'isObsolete' => (bool)$image->getIsObsolete(),
'isImage' => $file->isViewableImage(),
'isViewable' => $file->isViewableInBrowser(),

View file

@ -7,17 +7,41 @@ final class PhabricatorRepositoryType extends Phobject {
const REPOSITORY_TYPE_MERCURIAL = 'hg';
public static function getAllRepositoryTypes() {
$map = array(
self::REPOSITORY_TYPE_GIT => pht('Git'),
self::REPOSITORY_TYPE_MERCURIAL => pht('Mercurial'),
self::REPOSITORY_TYPE_SVN => pht('Subversion'),
);
return $map;
$map = self::getRepositoryTypeMap();
return ipull($map, 'name');
}
public static function getNameForRepositoryType($type) {
$map = self::getAllRepositoryTypes();
return idx($map, $type, pht('Unknown'));
$spec = self::getRepositoryTypeSpec($type);
return idx($spec, 'name', pht('Unknown ("%s")', $type));
}
public static function getRepositoryTypeSpec($type) {
$map = self::getRepositoryTypeMap();
return idx($map, $type, array());
}
public static function getRepositoryTypeMap() {
return array(
self::REPOSITORY_TYPE_GIT => array(
'name' => pht('Git'),
'icon' => 'fa-git',
'create.header' => pht('Create Git Repository'),
'create.subheader' => pht('Create a new Git repository.'),
),
self::REPOSITORY_TYPE_MERCURIAL => array(
'name' => pht('Mercurial'),
'icon' => 'fa-code-fork',
'create.header' => pht('Create Mercurial Repository'),
'create.subheader' => pht('Create a new Mercurial repository.'),
),
self::REPOSITORY_TYPE_SVN => array(
'name' => pht('Subversion'),
'icon' => 'fa-database',
'create.header' => pht('Create Subversion Repository'),
'create.subheader' => pht('Create a new Subversion repository.'),
),
);
}
}

View file

@ -342,7 +342,7 @@ final class PhabricatorRepositoryEditor
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
foreach ($xactions as $xaction) {
foreach ($xaction->getNewValue() as $pattern) {
@ -636,6 +636,43 @@ final class PhabricatorRepositoryEditor
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
foreach ($xactions as $xaction) {
$old = $object->getSymbolSources();
$new = $xaction->getNewValue();
// If the viewer is adding new repositories, make sure they are
// valid and visible.
$add = array_diff($new, $old);
if (!$add) {
continue;
}
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getActor())
->withPHIDs($add)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
foreach ($add as $phid) {
if (isset($repositories[$phid])) {
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Repository ("%s") does not exist, or you do not have '.
'permission to see it.',
$phid),
$xaction);
break;
}
}
break;
}
return $errors;
@ -684,7 +721,10 @@ final class PhabricatorRepositoryEditor
}
if ($this->getIsNewObject()) {
$object->synchronizeWorkingCopyAfterCreation();
id(new DiffusionRepositoryClusterEngine())
->setViewer($this->getActor())
->setRepository($object)
->synchronizeWorkingCopyAfterCreation();
}
return $xactions;

View file

@ -23,6 +23,7 @@ final class PhabricatorRepositoryPullEngine
public function pullRepository() {
$repository = $this->getRepository();
$viewer = PhabricatorUser::getOmnipotentUser();
$is_hg = false;
$is_git = false;
@ -96,7 +97,10 @@ final class PhabricatorRepositoryPullEngine
}
if ($repository->isHosted()) {
$repository->synchronizeWorkingCopyBeforeRead();
id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository)
->synchronizeWorkingCopyBeforeRead();
if ($is_git) {
$this->installGitHook();

View file

@ -0,0 +1,117 @@
<?php
final class PhabricatorRepositoryManagementClusterizeWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('clusterize')
->setExamples('**clusterize** [options] __repository__ ...')
->setSynopsis(
pht('Convert existing repositories into cluster repositories.'))
->setArguments(
array(
array(
'name' => 'service',
'param' => 'service',
'help' => pht(
'Cluster repository service in Almanac to move repositories '.
'into.'),
),
array(
'name' => 'remove-service',
'help' => pht('Take repositories out of a cluster.'),
),
array(
'name' => 'repositories',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$repositories = $this->loadRepositories($args, 'repositories');
if (!$repositories) {
throw new PhutilArgumentUsageException(
pht('Specify one or more repositories to clusterize.'));
}
$service_name = $args->getArg('service');
$remove_service = $args->getArg('remove-service');
if ($remove_service && $service_name) {
throw new PhutilArgumentUsageException(
pht('Specify --service or --remove-service, but not both.'));
}
if (!$service_name && !$remove_service) {
throw new PhutilArgumentUsageException(
pht('Specify --service or --remove-service.'));
}
if ($remove_service) {
$service = null;
} else {
$service = id(new AlmanacServiceQuery())
->setViewer($viewer)
->withNames(array($service_name))
->withServiceTypes(
array(
AlmanacClusterRepositoryServiceType::SERVICETYPE,
))
->executeOne();
if (!$service) {
throw new PhutilArgumentUsageException(
pht(
'No repository service "%s" exists.',
$service_name));
}
}
if ($service) {
$service_phid = $service->getPHID();
} else {
$service_phid = null;
}
$content_source = $this->newContentSource();
$diffusion_phid = id(new PhabricatorDiffusionApplication())->getPHID();
foreach ($repositories as $repository) {
$xactions = array();
$xactions[] = id(new PhabricatorRepositoryTransaction())
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_SERVICE)
->setNewValue($service_phid);
id(new PhabricatorRepositoryEditor())
->setActor($viewer)
->setActingAsPHID($diffusion_phid)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($repository, $xactions);
if ($service) {
echo tsprintf(
"%s\n",
pht(
'Moved repository "%s" to cluster service "%s".',
$repository->getDisplayName(),
$service->getName()));
} else {
echo tsprintf(
"%s\n",
pht(
'Removed repository "%s" from cluster service.',
$repository->getDisplayName()));
}
}
return 0;
}
}

View file

@ -66,7 +66,7 @@ final class PhabricatorRepositoryManagementMovePathsWorkflow
} else {
$dst = $to.substr($src, strlen($from));
$row['action'] = phutil_console_format('**%s**', pht('Move'));
$row['action'] = tsprintf('**%s**', pht('Move'));
$row['dst'] = $dst;
$row['move'] = true;
$any_changes = true;
@ -94,7 +94,7 @@ final class PhabricatorRepositoryManagementMovePathsWorkflow
->addColumn(
'dst',
array(
'title' => pht('dst'),
'title' => pht('Dst'),
))
->setBorders(true);

View file

@ -0,0 +1,40 @@
<?php
final class PhabricatorRepositoryURIPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'RURI';
public function getTypeName() {
return pht('Repository URI');
}
public function newObject() {
return new PhabricatorRepositoryURI();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorRepositoryURIQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$uri = $objects[$phid];
$handle->setName(
pht('URI %d %s', $uri->getID(), $uri->getDisplayURI()));
}
}
}

View file

@ -34,6 +34,7 @@ final class PhabricatorRepositoryQuery
private $needMostRecentCommits;
private $needCommitCounts;
private $needProjectPHIDs;
private $needURIs;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -148,6 +149,11 @@ final class PhabricatorRepositoryQuery
return $this;
}
public function needURIs($need_uris) {
$this->needURIs = $need_uris;
return $this;
}
public function getBuiltinOrders() {
return array(
'committed' => array(
@ -348,6 +354,20 @@ final class PhabricatorRepositoryQuery
}
}
$viewer = $this->getViewer();
if ($this->needURIs) {
$uris = id(new PhabricatorRepositoryURIQuery())
->setViewer($viewer)
->withRepositories($repositories)
->execute();
$uri_groups = mgroup($uris, 'getRepositoryPHID');
foreach ($repositories as $repository) {
$repository_uris = idx($uri_groups, $repository->getPHID(), array());
$repository->attachURIs($repository_uris);
}
}
return $repositories;
}

View file

@ -0,0 +1,106 @@
<?php
final class PhabricatorRepositoryURIQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $repositoryPHIDs;
private $repositories = array();
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withRepositoryPHIDs(array $phids) {
$this->repositoryPHIDs = $phids;
return $this;
}
public function withRepositories(array $repositories) {
$repositories = mpull($repositories, null, 'getPHID');
$this->withRepositoryPHIDs(array_keys($repositories));
$this->repositories = $repositories;
return $this;
}
public function withObjectHashes(array $hashes) {
$this->objectHashes = $hashes;
return $this;
}
public function newResultObject() {
return new PhabricatorRepositoryURI();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
return $where;
}
protected function willFilterPage(array $uris) {
$repositories = $this->repositories;
$repository_phids = mpull($uris, 'getRepositoryPHID');
$repository_phids = array_fuse($repository_phids);
$repository_phids = array_diff_key($repository_phids, $repositories);
if ($repository_phids) {
$more_repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($repository_phids)
->execute();
$repositories += mpull($more_repositories, null, 'getPHID');
}
foreach ($uris as $key => $uri) {
$repository_phid = $uri->getRepositoryPHID();
$repository = idx($repositories, $repository_phid);
if (!$repository) {
$this->didRejectResult($uri);
unset($uris[$key]);
continue;
}
$uri->attachRepository($repository);
}
return $uris;
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorRepositoryURITransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhabricatorRepositoryURITransaction();
}
}

View file

@ -68,9 +68,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
private $projectPHIDs = self::ATTACHABLE;
private $uris = self::ATTACHABLE;
private $clusterWriteLock;
private $clusterWriteVersion;
public static function initializeNewRepository(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
@ -1832,8 +1829,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
throw new Exception(
pht(
"The URI protocol is unrecognized. It should begin ".
"'%s', '%s', '%s', '%s', '%s', '%s', or be in the form '%s'.",
'The URI protocol is unrecognized. It should begin with '.
'"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".',
'ssh://',
'http://',
'https://',
@ -2182,7 +2179,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
$uris = array();
foreach ($protocol_map as $protocol => $proto_supported) {
foreach ($identifier_map as $identifier => $id_supported) {
$uris[] = PhabricatorRepositoryURI::initializeNewURI($this)
$uris[] = PhabricatorRepositoryURI::initializeNewURI()
->setRepositoryPHID($this->getPHID())
->attachRepository($this)
->setBuiltinProtocol($protocol)
->setBuiltinIdentifier($identifier)
->setIsDisabled(!$proto_supported || !$id_supported);
@ -2193,379 +2192,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
/* -( Cluster Synchronization )-------------------------------------------- */
private function shouldEnableSynchronization() {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
return false;
}
// TODO: For now, this is only supported for Git.
if (!$this->isGit()) {
return false;
}
// TODO: It may eventually make sense to try to version and synchronize
// observed repositories (so that daemons don't do reads against out-of
// date hosts), but don't bother for now.
if (!$this->isHosted()) {
return false;
}
$device = AlmanacKeys::getLiveDevice();
if (!$device) {
return false;
}
return true;
}
/**
* Synchronize repository version information after creating a repository.
*
* This initializes working copy versions for all currently bound devices to
* 0, so that we don't get stuck making an ambiguous choice about which
* devices are leaders when we later synchronize before a read.
*
* @task sync
*/
public function synchronizeWorkingCopyAfterCreation() {
if (!$this->shouldEnableSynchronization()) {
return;
}
$repository_phid = $this->getPHID();
$service = $this->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$bindings = $service->getActiveBindings();
foreach ($bindings as $binding) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$binding->getDevicePHID(),
0);
}
}
/**
* @task sync
*/
public function synchronizeWorkingCopyBeforeRead() {
if (!$this->shouldEnableSynchronization()) {
return;
}
$repository_phid = $this->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
$read_lock = PhabricatorRepositoryWorkingCopyVersion::getReadLock(
$repository_phid,
$device_phid);
// TODO: Raise a more useful exception if we fail to grab this lock.
$read_lock->lock(phutil_units('2 minutes in seconds'));
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
$versions = mpull($versions, null, 'getDevicePHID');
$this_version = idx($versions, $device_phid);
if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion();
} else {
$this_version = -1;
}
if ($versions) {
// This is the normal case, where we have some version information and
// can identify which nodes are leaders. If the current node is not a
// leader, we want to fetch from a leader and then update our version.
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
if ($max_version > $this_version) {
$fetchable = array();
foreach ($versions as $version) {
if ($version->getRepositoryVersion() == $max_version) {
$fetchable[] = $version->getDevicePHID();
}
}
$this->synchronizeWorkingCopyFromDevices($fetchable);
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
$max_version);
}
$result_version = $max_version;
} else {
// If no version records exist yet, we need to be careful, because we
// can not tell which nodes are leaders.
// There might be several nodes with arbitrary existing data, and we have
// no way to tell which one has the "right" data. If we pick wrong, we
// might erase some or all of the data in the repository.
// Since this is dangeorus, we refuse to guess unless there is only one
// device. If we're the only device in the group, we obviously must be
// a leader.
$service = $this->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$bindings = $service->getActiveBindings();
$device_map = array();
foreach ($bindings as $binding) {
$device_map[$binding->getDevicePHID()] = true;
}
if (count($device_map) > 1) {
throw new Exception(
pht(
'Repository "%s" exists on more than one device, but no device '.
'has any repository version information. Phabricator can not '.
'guess which copy of the existing data is authoritative. Remove '.
'all but one device from service to mark the remaining device '.
'as the authority.',
$this->getDisplayName()));
}
if (empty($device_map[$device->getPHID()])) {
throw new Exception(
pht(
'Repository "%s" is being synchronized on device "%s", but '.
'this device is not bound to the corresponding cluster '.
'service ("%s").',
$this->getDisplayName(),
$device->getName(),
$service->getName()));
}
// The current device is the only device in service, so it must be a
// leader. We can safely have any future nodes which come online read
// from it.
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
0);
$result_version = 0;
}
$read_lock->unlock();
return $result_version;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyBeforeWrite(
PhabricatorUser $actor) {
if (!$this->shouldEnableSynchronization()) {
return;
}
$repository_phid = $this->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
$write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock(
$repository_phid);
// TODO: Raise a more useful exception if we fail to grab this lock.
$write_lock->lock(phutil_units('2 minutes in seconds'));
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
foreach ($versions as $version) {
if (!$version->getIsWriting()) {
continue;
}
throw new Exception(
pht(
'An previous write to this repository was interrupted; refusing '.
'new writes. This issue resolves operator intervention to resolve, '.
'see "Write Interruptions" in the "Cluster: Repositories" in the '.
'documentation for instructions.'));
}
try {
$max_version = $this->synchronizeWorkingCopyBeforeRead();
} catch (Exception $ex) {
$write_lock->unlock();
throw $ex;
}
PhabricatorRepositoryWorkingCopyVersion::willWrite(
$repository_phid,
$device_phid,
array(
'userPHID' => $actor->getPHID(),
'epoch' => PhabricatorTime::getNow(),
'devicePHID' => $device_phid,
));
$this->clusterWriteVersion = $max_version;
$this->clusterWriteLock = $write_lock;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyAfterWrite() {
if (!$this->shouldEnableSynchronization()) {
return;
}
if (!$this->clusterWriteLock) {
throw new Exception(
pht(
'Trying to synchronize after write, but not holding a write '.
'lock!'));
}
$repository_phid = $this->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
// NOTE: This means we're still bumping the version when pushes fail. We
// could select only un-rejected events instead to bump a little less
// often.
$new_log = id(new PhabricatorRepositoryPushEventQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRepositoryPHIDs(array($repository_phid))
->setLimit(1)
->executeOne();
$old_version = $this->clusterWriteVersion;
if ($new_log) {
$new_version = $new_log->getID();
} else {
$new_version = $old_version;
}
PhabricatorRepositoryWorkingCopyVersion::didWrite(
$repository_phid,
$device_phid,
$this->clusterWriteVersion,
$new_log->getID());
$this->clusterWriteLock->unlock();
$this->clusterWriteLock = null;
}
/**
* @task sync
*/
private function synchronizeWorkingCopyFromDevices(array $device_phids) {
$service = $this->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$device_map = array_fuse($device_phids);
$bindings = $service->getActiveBindings();
$fetchable = array();
foreach ($bindings as $binding) {
// We can't fetch from nodes which don't have the newest version.
$device_phid = $binding->getDevicePHID();
if (empty($device_map[$device_phid])) {
continue;
}
// TODO: For now, only fetch over SSH. We could support fetching over
// HTTP eventually.
if ($binding->getAlmanacPropertyValue('protocol') != 'ssh') {
continue;
}
$fetchable[] = $binding;
}
if (!$fetchable) {
throw new Exception(
pht(
'Leader lost: no up-to-date nodes in repository cluster are '.
'fetchable.'));
}
$caught = null;
foreach ($fetchable as $binding) {
try {
$this->synchronizeWorkingCopyFromBinding($binding);
$caught = null;
break;
} catch (Exception $ex) {
$caught = $ex;
}
}
if ($caught) {
throw $caught;
}
}
private function synchronizeWorkingCopyFromBinding($binding) {
$fetch_uri = $this->getClusterRepositoryURIFromBinding($binding);
$local_path = $this->getLocalPath();
if ($this->isGit()) {
if (!Filesystem::pathExists($local_path)) {
$device = AlmanacKeys::getLiveDevice();
throw new Exception(
pht(
'Repository "%s" does not have a working copy on this device '.
'yet, so it can not be synchronized. Wait for the daemons to '.
'construct one or run `bin/repository update %s` on this host '.
'("%s") to build it explicitly.',
$this->getDisplayName(),
$this->getMonogram(),
$device->getName()));
}
$argv = array(
'fetch --prune -- %s %s',
$fetch_uri,
'+refs/*:refs/*',
);
} else {
throw new Exception(pht('Binding sync only supported for git!'));
}
$future = DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->setConnectAsDevice(true)
->setSudoAsDaemon(true)
->setProtocol($fetch_uri->getProtocol())
->newFuture();
$future->setCWD($local_path);
$future->resolvex();
}
private function getClusterRepositoryURIFromBinding(
public function getClusterRepositoryURIFromBinding(
AlmanacBinding $binding) {
$protocol = $binding->getAlmanacPropertyValue('protocol');
if ($protocol === null) {
@ -2613,8 +2240,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
/* -( Symbols )-------------------------------------------------------------*/
public function getSymbolSources() {
@ -2829,6 +2454,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
->setKey('shortName')
->setType('string')
->setDescription(pht('Unique short name, if the repository has one.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or inactive status.')),
);
}
@ -2838,11 +2467,15 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
'vcs' => $this->getVersionControlSystem(),
'callsign' => $this->getCallsign(),
'shortName' => $this->getRepositorySlug(),
'status' => $this->getStatus(),
);
}
public function getConduitSearchAttachments() {
return array();
return array(
id(new DiffusionRepositoryURIsSearchEngineAttachment())
->setAttachmentKey('uris'),
);
}
}

View file

@ -1,7 +1,12 @@
<?php
final class PhabricatorRepositoryURI
extends PhabricatorRepositoryDAO {
extends PhabricatorRepositoryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorConduitResultInterface {
protected $repositoryPHID;
protected $uri;
@ -58,15 +63,18 @@ final class PhabricatorRepositoryURI
) + parent::getConfiguration();
}
public static function initializeNewURI(PhabricatorRepository $repository) {
public static function initializeNewURI() {
return id(new self())
->attachRepository($repository)
->setRepositoryPHID($repository->getPHID())
->setIoType(self::IO_DEFAULT)
->setDisplayType(self::DISPLAY_DEFAULT)
->setIsDisabled(0);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryURIPHIDType::TYPECONST);
}
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
@ -85,6 +93,7 @@ final class PhabricatorRepositoryURI
$this->getBuiltinProtocol(),
$this->getBuiltinIdentifier(),
);
return implode('.', $parts);
}
@ -95,58 +104,57 @@ final class PhabricatorRepositoryURI
public function getEffectiveDisplayType() {
$display = $this->getDisplayType();
if ($display != self::IO_DEFAULT) {
if ($display != self::DISPLAY_DEFAULT) {
return $display;
}
return $this->getDefaultDisplayType();
}
public function getDefaultDisplayType() {
switch ($this->getEffectiveIOType()) {
case self::IO_MIRROR:
case self::IO_OBSERVE:
return self::DISPLAY_NEVER;
case self::IO_NONE:
if ($this->isBuiltin()) {
return self::DISPLAY_NEVER;
} else {
return self::DISPLAY_ALWAYS;
}
return self::DISPLAY_NEVER;
case self::IO_READ:
case self::IO_READWRITE:
// By default, only show the "best" version of the builtin URI, not the
// other redundant versions.
if ($this->isBuiltin()) {
$repository = $this->getRepository();
$other_uris = $repository->getURIs();
$repository = $this->getRepository();
$other_uris = $repository->getURIs();
$identifier_value = array(
self::BUILTIN_IDENTIFIER_CALLSIGN => 3,
self::BUILTIN_IDENTIFIER_SHORTNAME => 2,
self::BUILTIN_IDENTIFIER_ID => 1,
);
$identifier_value = array(
self::BUILTIN_IDENTIFIER_CALLSIGN => 3,
self::BUILTIN_IDENTIFIER_SHORTNAME => 2,
self::BUILTIN_IDENTIFIER_ID => 1,
);
$have_identifiers = array();
foreach ($other_uris as $other_uri) {
if ($other_uri->getIsDisabled()) {
continue;
}
$identifier = $other_uri->getBuiltinIdentifier();
if (!$identifier) {
continue;
}
$have_identifiers[$identifier] = $identifier_value[$identifier];
$have_identifiers = array();
foreach ($other_uris as $other_uri) {
if ($other_uri->getIsDisabled()) {
continue;
}
$best_identifier = max($have_identifiers);
$this_identifier = $identifier_value[$this->getBuiltinIdentifier()];
if ($this_identifier < $best_identifier) {
return self::DISPLAY_NEVER;
$identifier = $other_uri->getBuiltinIdentifier();
if (!$identifier) {
continue;
}
$have_identifiers[$identifier] = $identifier_value[$identifier];
}
$best_identifier = max($have_identifiers);
$this_identifier = $identifier_value[$this->getBuiltinIdentifier()];
if ($this_identifier < $best_identifier) {
return self::DISPLAY_NEVER;
}
return self::DISPLAY_ALWAYS;
}
return self::DISPLAY_NEVER;
}
@ -157,6 +165,10 @@ final class PhabricatorRepositoryURI
return $io;
}
return $this->getDefaultIOType();
}
public function getDefaultIOType() {
if ($this->isBuiltin()) {
$repository = $this->getRepository();
$other_uris = $repository->getURIs();
@ -176,7 +188,7 @@ final class PhabricatorRepositoryURI
}
}
return self::IO_IGNORE;
return self::IO_NONE;
}
@ -298,4 +310,290 @@ final class PhabricatorRepositoryURI
}
}
public function getViewURI() {
$id = $this->getID();
return $this->getRepository()->getPathURI("uri/view/{$id}/");
}
public function getEditURI() {
$id = $this->getID();
return $this->getRepository()->getPathURI("uri/edit/{$id}/");
}
public function getAvailableIOTypeOptions() {
$options = array(
self::IO_DEFAULT,
self::IO_NONE,
);
if ($this->isBuiltin()) {
$options[] = self::IO_READ;
$options[] = self::IO_WRITE;
} else {
$options[] = self::IO_OBSERVE;
$options[] = self::IO_MIRROR;
}
$map = array();
$io_map = self::getIOTypeMap();
foreach ($options as $option) {
$spec = idx($io_map, $option, array());
$label = idx($spec, 'label', $option);
$short = idx($spec, 'short');
$name = pht('%s: %s', $label, $short);
$map[$option] = $name;
}
return $map;
}
public function getAvailableDisplayTypeOptions() {
$options = array(
self::DISPLAY_DEFAULT,
self::DISPLAY_ALWAYS,
self::DISPLAY_NEVER,
);
$map = array();
$display_map = self::getDisplayTypeMap();
foreach ($options as $option) {
$spec = idx($display_map, $option, array());
$label = idx($spec, 'label', $option);
$short = idx($spec, 'short');
$name = pht('%s: %s', $label, $short);
$map[$option] = $name;
}
return $map;
}
public static function getIOTypeMap() {
return array(
self::IO_DEFAULT => array(
'label' => pht('Default'),
'short' => pht('Use default behavior.'),
),
self::IO_OBSERVE => array(
'icon' => 'fa-download',
'color' => 'green',
'label' => pht('Observe'),
'note' => pht(
'Phabricator will observe changes to this URI and copy them.'),
'short' => pht('Copy from a remote.'),
),
self::IO_MIRROR => array(
'icon' => 'fa-upload',
'color' => 'green',
'label' => pht('Mirror'),
'note' => pht(
'Phabricator will push a copy of any changes to this URI.'),
'short' => pht('Push a copy to a remote.'),
),
self::IO_NONE => array(
'icon' => 'fa-times',
'color' => 'grey',
'label' => pht('No I/O'),
'note' => pht(
'Phabricator will not push or pull any changes to this URI.'),
'short' => pht('Do not perform any I/O.'),
),
self::IO_READ => array(
'icon' => 'fa-folder',
'color' => 'blue',
'label' => pht('Read Only'),
'note' => pht(
'Phabricator will serve a read-only copy of the repository from '.
'this URI.'),
'short' => pht('Serve repository in read-only mode.'),
),
self::IO_READWRITE => array(
'icon' => 'fa-folder-open',
'color' => 'blue',
'label' => pht('Read/Write'),
'note' => pht(
'Phabricator will serve a read/write copy of the repository from '.
'this URI.'),
'short' => pht('Serve repository in read/write mode.'),
),
);
}
public static function getDisplayTypeMap() {
return array(
self::DISPLAY_DEFAULT => array(
'label' => pht('Default'),
'short' => pht('Use default behavior.'),
),
self::DISPLAY_ALWAYS => array(
'icon' => 'fa-eye',
'color' => 'green',
'label' => pht('Visible'),
'note' => pht('This URI will be shown to users as a clone URI.'),
'short' => pht('Show as a clone URI.'),
),
self::DISPLAY_NEVER => array(
'icon' => 'fa-eye-slash',
'color' => 'grey',
'label' => pht('Hidden'),
'note' => pht(
'This URI will be hidden from users.'),
'short' => pht('Do not show as a clone URI.'),
),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DiffusionURIEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorRepositoryURITransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
// To edit a repository URI, you must be able to edit the
// corresponding repository.
$extended[] = array($this->getRepository(), $capability);
break;
}
return $extended;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid')
->setDescription(pht('The associated repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('uri')
->setType('map<string, string>')
->setDescription(pht('The raw and effective URI.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('io')
->setType('map<string, const>')
->setDescription(
pht('The raw, default, and effective I/O Type settings.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('display')
->setType('map<string, const>')
->setDescription(
pht('The raw, default, and effective Display Type settings.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('credentialPHID')
->setType('phid?')
->setDescription(
pht('The associated credential PHID, if one exists.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('disabled')
->setType('bool')
->setDescription(pht('True if the URI is disabled.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('builtin')
->setType('map<string, string>')
->setDescription(
pht('Information about builtin URIs.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dateCreated')
->setType('int')
->setDescription(
pht('Epoch timestamp when the object was created.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dateModified')
->setType('int')
->setDescription(
pht('Epoch timestamp when the object was last updated.')),
);
}
public function getFieldValuesForConduit() {
return array(
'repositoryPHID' => $this->getRepositoryPHID(),
'uri' => array(
'raw' => $this->getURI(),
'effective' => (string)$this->getDisplayURI(),
),
'io' => array(
'raw' => $this->getIOType(),
'default' => $this->getDefaultIOType(),
'effective' => $this->getEffectiveIOType(),
),
'display' => array(
'raw' => $this->getDisplayType(),
'default' => $this->getDefaultDisplayType(),
'effective' => $this->getEffectiveDisplayType(),
),
'credentialPHID' => $this->getCredentialPHID(),
'disabled' => (bool)$this->getIsDisabled(),
'builtin' => array(
'protocol' => $this->getBuiltinProtocol(),
'identifier' => $this->getBuiltinIdentifier(),
),
'dateCreated' => $this->getDateCreated(),
'dateModified' => $this->getDateModified(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}

View file

@ -0,0 +1,70 @@
<?php
final class PhabricatorRepositoryURITransaction
extends PhabricatorApplicationTransaction {
const TYPE_REPOSITORY = 'diffusion.uri.repository';
const TYPE_URI = 'diffusion.uri.uri';
const TYPE_IO = 'diffusion.uri.io';
const TYPE_DISPLAY = 'diffusion.uri.display';
const TYPE_CREDENTIAL = 'diffusion.uri.credential';
const TYPE_DISABLE = 'diffusion.uri.disable';
public function getApplicationName() {
return 'repository';
}
public function getApplicationTransactionType() {
return PhabricatorRepositoryURIPHIDType::TYPECONST;
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_URI:
return pht(
'%s changed this URI from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
case self::TYPE_IO:
$map = PhabricatorRepositoryURI::getIOTypeMap();
$old_label = idx(idx($map, $old, array()), 'label', $old);
$new_label = idx(idx($map, $new, array()), 'label', $new);
return pht(
'%s changed the display type for this URI from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old_label,
$new_label);
case self::TYPE_DISPLAY:
$map = PhabricatorRepositoryURI::getDisplayTypeMap();
$old_label = idx(idx($map, $old, array()), 'label', $old);
$new_label = idx(idx($map, $new, array()), 'label', $new);
return pht(
'%s changed the display type for this URI from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old_label,
$new_label);
case self::TYPE_DISABLE:
if ($new) {
return pht(
'%s disabled this URI.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s enabled this URI.',
$this->renderHandleLink($author_phid));
}
}
return parent::getTitle();
}
}

View file

@ -7,6 +7,7 @@ final class PhabricatorRepositoryWorkingCopyVersion
protected $devicePHID;
protected $repositoryVersion;
protected $isWriting;
protected $lockOwner;
protected $writeProperties;
protected function getConfiguration() {
@ -16,6 +17,7 @@ final class PhabricatorRepositoryWorkingCopyVersion
'repositoryVersion' => 'uint32',
'isWriting' => 'bool',
'writeProperties' => 'text?',
'lockOwner' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_workingcopy' => array(
@ -69,29 +71,33 @@ final class PhabricatorRepositoryWorkingCopyVersion
* by default.
*/
public static function willWrite(
AphrontDatabaseConnection $locked_connection,
$repository_phid,
$device_phid,
array $write_properties) {
array $write_properties,
$lock_owner) {
$version = new self();
$conn_w = $version->establishConnection('w');
$table = $version->getTableName();
queryfx(
$conn_w,
$locked_connection,
'INSERT INTO %T
(repositoryPHID, devicePHID, repositoryVersion, isWriting,
writeProperties)
writeProperties, lockOwner)
VALUES
(%s, %s, %d, %d, %s)
(%s, %s, %d, %d, %s, %s)
ON DUPLICATE KEY UPDATE
isWriting = VALUES(isWriting),
writeProperties = VALUES(writeProperties)',
writeProperties = VALUES(writeProperties),
lockOwner = VALUES(lockOwner)',
$table,
$repository_phid,
$device_phid,
0,
1,
phutil_json_encode($write_properties));
phutil_json_encode($write_properties),
$lock_owner);
}
@ -102,7 +108,9 @@ final class PhabricatorRepositoryWorkingCopyVersion
$repository_phid,
$device_phid,
$old_version,
$new_version) {
$new_version,
$lock_owner) {
$version = new self();
$conn_w = $version->establishConnection('w');
$table = $version->getTableName();
@ -111,17 +119,20 @@ final class PhabricatorRepositoryWorkingCopyVersion
$conn_w,
'UPDATE %T SET
repositoryVersion = %d,
isWriting = 0
isWriting = 0,
lockOwner = NULL
WHERE
repositoryPHID = %s AND
devicePHID = %s AND
repositoryVersion = %d AND
isWriting = 1',
isWriting = 1 AND
lockOwner = %s',
$table,
$new_version,
$repository_phid,
$device_phid,
$old_version);
$old_version,
$lock_owner);
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorSearchSchemaSpec
extends PhabricatorConfigSchemaSpec {
public function buildSchemata() {
$this->buildEdgeSchemata(new PhabricatorProfilePanelConfiguration());
}
}

View file

@ -1808,8 +1808,9 @@ abstract class PhabricatorEditEngine
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Exception when processing transaction of type "%s".',
$xaction['type']),
'Exception when processing transaction of type "%s": %s',
$xaction['type'],
$ex->getMessage()),
$ex);
}

View file

@ -0,0 +1,42 @@
<?php
final class PhabricatorBoolEditField
extends PhabricatorEditField {
private $options;
public function setOptions($off_label, $on_label) {
$this->options = array(
'0' => $off_label,
'1' => $on_label,
);
return $this;
}
public function getOptions() {
return $this->options;
}
protected function newControl() {
$options = $this->getOptions();
if (!$options) {
$options = array(
'0' => pht('False'),
'1' => pht('True'),
);
}
return id(new AphrontFormSelectControl())
->setOptions($options);
}
protected function newHTTPParameterType() {
return new AphrontBoolHTTPParameterType();
}
protected function newConduitParameterType() {
return new ConduitBoolParameterType();
}
}

View file

@ -5,6 +5,7 @@ final class PhabricatorTextAreaEditField
private $monospaced;
private $height;
private $isStringList;
public function setMonospaced($monospaced) {
$this->monospaced = $monospaced;
@ -24,6 +25,15 @@ final class PhabricatorTextAreaEditField
return $this->height;
}
public function setIsStringList($is_string_list) {
$this->isStringList = $is_string_list;
return $this;
}
public function getIsStringList() {
return $this->isStringList;
}
protected function newControl() {
$control = new AphrontFormTextAreaControl();
@ -39,8 +49,25 @@ final class PhabricatorTextAreaEditField
return $control;
}
protected function getValueForControl() {
$value = $this->getValue();
return implode("\n", $value);
}
protected function newConduitParameterType() {
return new ConduitStringParameterType();
if ($this->getIsStringList()) {
return new ConduitStringListParameterType();
} else {
return new ConduitStringParameterType();
}
}
protected function newHTTPParameterType() {
if ($this->getIsStringList()) {
return new AphrontStringListHTTPParameterType();
} else {
return new AphrontStringHTTPParameterType();
}
}
}

View file

@ -0,0 +1,247 @@
@title Cluster: Devices
@group cluster
Guide to configuring hosts to act as cluster devices.
Cluster Context
===============
This document describes a step in configuring Phabricator to run on
multiple hosts in a cluster configuration. This is an advanced feature. For
more information on clustering, see @{article:Clustering Introduction}.
In this context, device configuration is mostly relevant to configuring
repository services in a cluster. You can find more details about this in
@{article:Cluster: Repositories}.
Overview
========
Some cluster services need to be able to authenticate themselves and interact
with other services. For example, two repository hosts holding copies of the
same repository must be able to fetch changes from one another, even if the
repository is private.
Within a cluster, devices authenticate using SSH keys. Some operations happen
over SSH (using keys in a normal way, as you would when running `ssh` from the
command line), while others happen over HTTP (using SSH keys to sign requests).
Before hosts can authenticate to one another, you need to configure the
credentials so other devices know the keys can be trusted. Beyond establishing
trust, this configuration will establish //device identity//, so each host
knows which device it is explicitly.
Today, this is primarily necessary when configuring repository clusters.
Using Almanac
=============
The tool Phabricator uses to manage cluster devices is the **Almanac**
application, and most configuration will occur through the application's web
UI. If you are not familiar with it, see @{article:Almanac User Guide} first.
This document assumes you are familiar with Almanac concepts.
What Lies Ahead
===============
Here's a brief overview of the steps required to register cluster devices. The
remainder of this document walks through these points in more detail.
- Create an Almanac device record for each device.
- Generate, add, and trust SSH keys if necessary.
- Install Phabricator on the host.
- Use `bin/almanac register` from the host to register it as a device.
See below for guidance on each of these steps.
Individual vs Shared Keys
=========================
Before getting started, you should choose how you plan to manage device SSH
keys. Trust and device identity are handled separately, and there are two ways
to set up SSH keys so that devices can authenticate with one another:
- you can generate a unique SSH key for each device; or
- you can generate one SSH key and share it across multiple devices.
Using **unique keys** allows the tools to do some more sanity/safety checks and
makes it a bit more difficult to misconfigure things, but you'll have to do
more work managing the actual keys. This may be a better choice if you are
setting up a small cluster (2-3 devices) for the first time.
Using **shared keys** makes key management easier but safety checks won't be
able to catch a few kinds of mistakes. This may be a better choice if you are
setting up a larger cluster, plan to expand the cluster later, or have
experience with Phabricator clustering.
Because all cluster keys are all-powerful, there is no material difference
between these methods from a security or trust viewpoint. Unique keys are just
potentially easier to administrate at small scales, while shared keys are
easier at larger scales.
Create Almanac Device Records
=============================
For each host you plan to make part of a Phabricator cluster, go to the
{nav Almanac} application and create a **device** record. For guidance on this
application, see @{article:Almanac User Guide}.
Add **interfaces** to each device record so Phabricator can tell how to
connect to these hosts. Normally, you'll add one HTTP interface (usually on
port 80) and one SSH interface (by default, on port 2222) to each device:
For example, if you are building a two-host repository cluster, you may end
up with records that look like these:
- Device: `repo001.mycompany.net`
- Interface: `123.0.0.1:2222`
- Interface: `123.0.0.1:80`
- Device: `repo002.mycopmany.net`
- Interface: `123.0.0.2:2222`
- Interface: `123.0.0.2:80`
Note that these hosts will normally run two `sshd` ports: the standard `sshd`
which you connect to to operate and administrate the host, and the special
Phabricator `sshd` that you connect to to clone and push repositories.
You should specify the Phabricator `sshd` port, **not** the standard `sshd`
port.
If you're using **unique** SSH keys for each device, continue to the next step.
If you're using **shared** SSH keys, create a third device with no interfaces,
like `keywarden.mycompany.net`. This device will just be used as a container to
hold the trusted SSH key and is not a real device.
NOTE: Do **not** create a **service** record yet. Today, service records become
active immediately once they are created, and you haven't set things up yet.
Generate and Trust SSH Keys
===========================
Next, you need to generate or upload SSH keys and mark them as trusted. Marking
a key as trusted gives it tremendous power.
If you're using **unique** SSH keys, upload or generate a key for each
individual device from the device detail screen in the Almanac web UI. Save the
private keys for the next step.
If you're using a **shared** SSH key, upload or generate a single key for
the keywarden device from the device detail screen in the Almanac web UI.
Save the private key for the next step.
Regardless of how many keys you generated, take the key IDs from the tables
in the web UI and run this command from the command line for each key, to mark
each key as trusted:
```
phabricator/ $ ./bin/almanac trust-key --id <key-id-1>
phabricator/ $ ./bin/almanac trust-key --id <key-id-2>
...
```
The warnings this command emits are serious. The private keys are now trusted,
and allow any user or device possessing them to sign requests that bypass
policy checks without requiring additional credentials. Guard them carefully!
If you need to revoke trust for a key later, use `untrust-key`:
```
phabricator/ $ ./bin/almanac untrust-key --id <key-id>
```
Once the keys are trusted, continue to the next step.
Install Phabricator
===================
If you haven't already, install Phabricator on each device you plan to enroll
in the cluster. Cluster repository devices must provide services over both HTTP
and SSH, so you need to install and configure both a webserver and a
Phabricator `sshd` on these hosts.
Generally, you will follow whatever process you otherwise use when installing
Phabricator.
NOTE: Do not start the daemons on the new devices yet. They won't work properly
until you've finished configuring things.
Once Phabricator is installed, you can enroll the devices in the cluster by
registering them.
Register Devices
================
To register a host as an Almanac device, use `bin/almanac register`.
If you are using **unique** keys, run it like this:
```
$ ./bin/almanac register \
--device <device> \
--private-key <key>
```
For example, you might run this command on `repo001` when using unique keys:
```
$ ./bin/almanac register \
--device repo001.mycompany.net \
--private-key /path/to/private.key
```
If you are using a **shared** key, this will be a little more complicated
because you need to override some checks that are intended to prevent mistakes.
Use the `--identify-as` flag to choose a device identity:
```
$ ./bin/almanac register \
--device <keywarden-device> \
--private-key <key> \
--identify-as <actual-device>
```
For example, you might run this command on `repo001` when using a shared key:
```
$ ./bin/almanac register
--device keywarden.mycompany.net \
--private-key /path/to/private-key \
--identify-as repo001.mycompany.net
```
In particular, note that `--device` is always the **trusted** device associated
with the trusted key. The `--identify-as` flag allows several different hosts
to share the same key but still identify as different devices.
The overall effect of the `bin/almanac` command is to copy identity and key
files into `phabricator/conf/keys/`. You can inspect the results by examining
that directory. The helper script just catches potential mistakes and makes
sure the process is completed correctly.
Note that a copy of the active private key is stored in the `conf/keys/`
directory permanently.
When converting a host into a cluster host, you may need to revisit
@{article:Diffusion User Guide: Repository Hosting} and double check the `sudo`
permission for the host. In particular, cluster hosts need to be able to run
`ssh` via `sudo` so they can read the device private key.
Next Steps
==========
Now that devices are registered, you can build cluster services from them.
Return to the relevant cluster service documentation to continue:
- build repository clusters with @{article:Cluster: Repositories};
- return to @{article:Clustering Introduction}; or
- review the Almanac application with @{article:Almanac User Guide}.

View file

@ -9,9 +9,9 @@ Overview
WARNING: This feature is a very early prototype; the features this document
describes are mostly speculative fantasy.
If you use Git or Mercurial, you can deploy Phabricator with multiple
repository hosts, configured so that each host is readable and writable. The
advantages of doing this are:
If you use Git, you can deploy Phabricator with multiple repository hosts,
configured so that each host is readable and writable. The advantages of doing
this are:
- you can completely survive the loss of repository hosts;
- reads and writes can scale across multiple machines; and
@ -22,24 +22,6 @@ This configuration is complex, and many installs do not need to pursue it.
This configuration is not currently supported with Subversion or Mercurial.
Repository Hosts
================
Repository hosts must run a complete, fully configured copy of Phabricator,
including a webserver. They must also run a properly configured `sshd`.
Generally, these hosts will run the same set of services and configuration that
web hosts run. If you prefer, you can overlay these services and put web and
repository services on the same hosts. See @{article:Clustering Introduction}
for some guidance on overlaying services.
When a user requests information about a repository that can only be satisfied
by examining a repository working copy, the webserver receiving the request
will make an HTTP service call to a repository server which hosts the
repository to retrieve the data it needs. It will use the result of this query
to respond to the user.
How Reads and Writes Work
=========================
@ -95,6 +77,162 @@ Other mitigations are possible, but securing a network against the NSA and
similar agents of other rogue nations is beyond the scope of this document.
Repository Hosts
================
Repository hosts must run a complete, fully configured copy of Phabricator,
including a webserver. They must also run a properly configured `sshd`.
If you are converting existing hosts into cluster hosts, you may need to
revisit @{article:Diffusion User Guide: Repository Hosting} and make sure
the system user accounts have all the necessary `sudo` permissions. In
particular, cluster devices need `sudo` access to `ssh` so they can read
device keys.
Generally, these hosts will run the same set of services and configuration that
web hosts run. If you prefer, you can overlay these services and put web and
repository services on the same hosts. See @{article:Clustering Introduction}
for some guidance on overlaying services.
When a user requests information about a repository that can only be satisfied
by examining a repository working copy, the webserver receiving the request
will make an HTTP service call to a repository server which hosts the
repository to retrieve the data it needs. It will use the result of this query
to respond to the user.
Setting up a Cluster Services
=============================
To set up clustering, first register the devices that you want to use as part
of the cluster with Almanac. For details, see @{article:Cluster: Devices}.
NOTE: Once you create a service, new repositories will immediately allocate
on it. You may want to disable repository creation during initial setup.
Once the hosts are registered as devices, you can create a new service in
Almanac:
- First, register at least one device according to the device clustering
instructions.
- Create a new service of type **Phabricator Cluster: Repository** in
Almanac.
- Bind this service to all the interfaces on the device or devices.
- For each binding, add a `protocol` key with one of these values:
`ssh`, `http`, `https`.
For example, a service might look like this:
- Service: `repos001.mycompany.net`
- Binding: `repo001.mycompany.net:80`, `protocol=http`
- Binding: `repo001.mycompany.net:2222`, `protocol=ssh`
The service itself has a `closed` property. You can set this to `true` to
disable new repository allocations on this service (for example, if it is
reaching capacity).
Migrating to Clustered Services
===============================
To convert existing repositories on an install into cluster repositories, you
will generally perform these steps:
- Register the existing host as a cluster device.
- Configure a single host repository service using //only// that host.
This puts you in a transitional state where repositories on the host can work
as either on-host repositories or cluster repositories. You can move forward
from here slowly and make sure services still work, with a quick path back to
safety if you run into trouble.
To move forward, migrate one repository to the service and make sure things
work correctly. If you run into issues, you can back out by migrating the
repository off the service.
To migrate a repository onto a cluster service, use this command:
```
$ ./bin/repository clusterize <repository> --service <service>
```
To migrate a repository back off a service, use this command:
```
$ ./bin/repoistory clusterize <repository> --remove-service
```
This command only changes how Phabricator connects to the repository; it does
not move any data or make any complex structural changes.
When Phabricator needs information about a non-clustered repository, it just
runs a command like `git log` directly on disk. When Phabricator needs
information about a clustered repository, it instead makes a service call to
another server, asking that server to run `git log` instead.
In a single-host cluster the server will make this service call to itself, so
nothing will really change. But this //is// an effective test for most
possible configuration mistakes.
If your canary repository works well, you can migrate the rest of your
repositories when ready (you can use `bin/repository list` to quickly get a
list of all repository monograms).
Once all repositories are migrated, you've reached a stable state and can
remain here as long as you want. This state is sufficient to convert daemons,
SSH, and web services into clustered versions and spread them across multiple
machines if those goals are more interesting.
Obviously, your single-device "cluster" will not be able to survive the loss of
the single repository host, but you can take as long as you want to expand the
cluster and add redundancy.
After creating a service, you do not need to `clusterize` new repositories:
they will automatically allocate onto an open service.
When you're ready to expand the cluster, continue below.
Expanding a Cluster
===================
To expand an existing cluster, follow these general steps:
- Register new devices in Almanac.
- Add bindings to the new devices to the repository service, also in Almanac.
- Start the daemons on the new devices.
For instructions on configuring and registering devices, see
@{article:Cluster: Devices}.
As soon as you add active bindings to a service, Phabricator will begin
synchronizing repositories and sending traffic to the new device. You do not
need to copy any repository data to the device: Phabricator will automatically
synchronize it.
If you have a large amount of repository data, you may want to help this
process along by copying the repository directory from an existing cluster
device before bringing the new host online. This is optional, but can reduce
the amount of time required to fully synchronize the cluster.
You do not need to synchronize the most up-to-date data or stop writes during
this process. For example, loading the most recent backup snapshot onto the new
device will substantially reduce the amount of data that needs to be
synchronized.
Contracting a Cluster
=====================
To reduce the size of an existing cluster, follow these general steps:
- Disable the bindings from the service to the dead device in Almanac.
If you are removing a device because it failed abruptly (or removing several
devices at once) it is possible that some repositories will have lost all their
leaders. See "Loss of Leaders" below to understand and resolve this.
Monitoring Services
===================

View file

@ -273,7 +273,7 @@ You do not need to adjust the `"admin"` server.
**Aphlict**: Your Aphlict configuration should make these adjustments to
the `"client"` server:
- The `protocol` should be `"http"`: `nginx` will send plain HTTP traffic
- Do not specify any `ssl.*` options: `nginx` will send plain HTTP traffic
to Aphlict.
- Optionally, you can `listen` on `127.0.0.1` instead of `0.0.0.0`, because
the server will no longer receive external traffic.

View file

@ -3,13 +3,15 @@
Guide to configuring Phabricator repository hosting.
= Overview =
Overview
========
Phabricator can host repositories and provide authenticated read and write
access to them over HTTP and SSH. This document describes how to configure
repository hosting.
= Understanding Supported Protocols =
Understanding Supported Protocols
=================================
Phabricator supports hosting over these protocols:
@ -35,99 +37,165 @@ performant, but HTTP is easier to set up and supports anonymous access.
| Performance | Better | Okay |
| Setup | Hard | Easy |
Each repository can be configured individually, and you can use either protocol,
or both, or a mixture across different repositories.
Each repository can be configured individually, and you can use either
protocol, or both, or a mixture across different repositories.
SSH is recommended unless you need anonymous access, or are not able to
configure it for technical reasons.
= Configuring System User Accounts =
Phabricator uses as many as three user accounts. This section will guide you
through creating and configuring them. These are system user accounts on the
machine Phabricator runs on, not Phabricator user accounts.
Creating System User Accounts
=============================
The system accounts are:
Phabricator uses two system user accounts, plus a third account if you
configure SSH access. This section will guide you through creating and
configuring them. These are system user accounts on the machine Phabricator
runs on, not Phabricator user accounts.
- The user the daemons run as. We'll call this `daemon-user`. For more
information on the daemons, see @{article:Managing Daemons with phd}. This
The system accounts Phabricator uses are:
- The user the webserver runs as. We'll call this `www-user`.
- The user the daemons run as. We'll call this `daemon-user`. This
user is the only user which will interact with the repositories directly.
Other accounts will `sudo` to this account in order to perform VCS
Other accounts will `sudo` to this account in order to perform repository
operations.
- The user the webserver runs as. We'll call this `www-user`. If you do not
plan to make repositories available over HTTP, you do not need to perform
any special configuration for this user.
- The user that users will connect over SSH as. We'll call this `vcs-user`.
- The user that humans will connect over SSH as. We'll call this `vcs-user`.
If you do not plan to make repositories available over SSH, you do not need
to perform any special configuration for this user.
to create or configure this user.
To configure these users:
To create these users:
- Create a `www-user` if one does not already exist. In most cases, this
user will already exist and you just need to identify which user it is. Run
your webserver as this user.
- Create a `daemon-user` if one does not already exist (you can call this user
whatever you want, or use an existing account). When you start the daemons,
start them using this user.
- Create a `www-user` if one does not already exist. Run your webserver as
this user. In most cases, this user will already exist.
- Create a `vcs-user` if one does not already exist. Common names for this
user are `git` or `hg`. When users clone repositories, they will use a URI
like `vcs-user@phabricator.yourcompany.com`.
whatever you want, or use an existing account). Below, you'll configure
the daemons to start as this user.
- Create a `vcs-user` if one does not already exist and you plan to set up
SSH. When users clone repositories, they will use a URI like
`vcs-user@phabricator.yourcompany.com`, so common names for this user are
`git` or `hg`.
Now, allow the `vcs-user` and `www-user` to `sudo` as the `daemon-user`. Add
this to `/etc/sudoers`, using `visudo` or `sudoedit`.
Continue below to configure these accounts.
If you plan to use SSH:
vcs-user ALL=(daemon-user) SETENV: NOPASSWD: /path/to/bin/git-upload-pack, /path/to/bin/git-receive-pack, /path/to/bin/hg, /path/to/bin/svnserve
Configuring Phabricator
=======================
If you plan to use HTTP:
Now that you have created or identified these accounts, update the Phabricator
configuration to specify them.
www-user ALL=(daemon-user) SETENV: NOPASSWD: /usr/bin/git-http-backend, /usr/bin/hg
First, set `phd.user` to the `daemon-user`:
Replace `vcs-user`, `www-user` and `daemon-user` with the right usernames for
your configuration. Make sure all the paths point to the real locations of the
binaries on your system. You can omit any binaries associated with VCSes you do
not use.
```
phabricator/ $ ./bin/config set phd.user daemon-user
```
Adding these commands to `sudoers` will allow the daemon and webserver users to
write to repositories as the daemon user.
Restart the daemons to make sure this configuration works properly. They should
start as the correct user automatically.
Before saving and closing `/etc/sudoers`, look for this line:
If you're using a `vcs-user` for SSH, you should also configure that:
```
phabricator/ $ ./bin/config set diffusion.ssh-user vcs-user
```
Next, you'll set up `sudo` permissions so these users can interact with one
another.
Configuring Sudo
================
The `www-user` and `vcs-user` need to be able to `sudo` as the `daemon-user`
so they can interact with repositories.
To grant them access, edit the `sudo` system configuration. On many systems,
you will do this by modifying the `/etc/sudoers` file using `visudo` or
`sudoedit`. In some cases, you may add a new file to `/etc/sudoers.d` instead.
To give a user account `sudo` access to run a list of binaries, add a line like
this to the configuration file (this example would grant `vcs-user` permission
to run `ls` as `daemon-user`):
```
vcs-user ALL=(daemon-user) SETENV: NOPASSWD: /path/to/bin/ls
```
The `www-user` needs to be able to run these binaries as the `daemon-user`:
- `git` (if using Git)
- `git-http-backend` (if using Git)
- `hg` (if using Mercurial)
- `ssh` (if configuring clusters)
If you plan to use SSH, the `vcs-user` needs to be able to run these binaries
as the `daemon-user`:
- `git` (if using Git)
- `git-upload-pack` (if using Git)
- `git-receive-pack` (if using Git)
- `hg` (if using Mercurial)
- `svnserve` (if using Subversion)
- `ssh` (if configuring clusters)
Identify the full paths to all of these binaries on your system and add the
appropriate permissions to the `sudo` configuration.
Normally, you'll add two lines that look something like this:
```
www-user ALL=(daemon-user) SETENV: NOPASSWD: /path/to/x, /path/to/y, ...
vcs-user ALL=(daemon-user) SETENV: NOPASSWD: /path/to/x, /path/to/y, ...
```
This is just a template. In the real configuration file, you need to:
- Replace `www-user`, `dameon-user` and `vcs-user` with the correct
usernames for your system.
- List every binary that these users need access to, as described above.
- Make sure each binary path is the full path to the correct binary location
on your system.
Before continuing, look for this line in your `sudo` configuration:
Defaults requiretty
If it's present, comment it out by putting a `#` at the beginning of the line.
With this option enabled, VCS SSH sessions won't be able to use `sudo`.
Additional SSH User Configuration
=================================
If you're planning to use SSH, you should also edit `/etc/passwd` and
`/etc/shadow` to make sure the `vcs-user` account is set up correctly.
- Open `/etc/shadow` and find the line for the `vcs-user` account.
- The second field (which is the password field) must not be set to
`!!`. This value will prevent login. If it is set to `!!`, edit it
and set it to `NP` ("no password") instead.
- Open `/etc/passwd` and find the line for the `vcs-user` account.
- The last field (which is the login shell) must be set to a real shell.
If it is set to something like `/bin/false`, then `sshd` will not be able
to execute commands. Instead, you should set it to a real shell, like
`/bin/sh`.
**`/etc/shadow`**: Open `/etc/shadow` and find the line for the `vcs-user`
account.
Finally, once you've configured `/etc/sudoers`, `/etc/shadow` and `/etc/passwd`,
set `phd.user` to the `daemon-user`:
The second field (which is the password field) must not be set to `!!`. This
value will prevent login. If it is set to `!!`, edit it and set it to `NP` ("no
password") instead.
phabricator/ $ ./bin/config set phd.user daemon-user
**`/etc/passwd`**: Open `/etc/passwd` and find the line for the `vcs-user`
account.
If you're using a `vcs-user`, you should also configure that here:
The last field (which is the login shell) must be set to a real shell. If it is
set to something like `/bin/false`, then `sshd` will not be able to execute
commands. Instead, you should set it to a real shell, like `/bin/sh`.
phabricator/ $ ./bin/config set diffusion.ssh-user vcs-user
= Configuring HTTP =
Configuring HTTP
================
If you plan to use authenticated HTTP, you need to set
`diffusion.allow-http-auth` in Config. If you don't plan to use HTTP, or plan to
use only anonymous HTTP, you can leave this setting disabled.
If you plan to serve repositories over authenticated HTTP, you need to set
`diffusion.allow-http-auth` in Config. If you don't plan to serve repositories
over HTTP (or plan to use only anonymous HTTP) you can leave this setting
disabled.
If you plan to use authenticated HTTP, you'll also need to configure a VCS
password in {nav Settings > VCS Password}.
If you plan to use authenticated HTTP, you (and all other users) also need to
configure a VCS password for your account in {nav Settings > VCS Password}.
Your VCS password must be a different password than your main Phabricator
password because VCS passwords are very easy to accidentally disclose. They are
@ -136,60 +204,58 @@ and present in command output and logs. We strongly encourage you to use SSH
instead of HTTP to authenticate access to repositories.
Otherwise, if you've configured system accounts above, you're all set. No
additional server configuration is required to make HTTP work.
additional server configuration is required to make HTTP work. You should now
be able to fetch and push repositories over HTTP. See "Cloning a Repository"
below for more details.
= Configuring SSH =
If you're having trouble, see "Troubleshooting HTTP" below.
SSH access requires some additional setup. Here's an overview of how setup
works:
- You'll move the normal `sshd` daemon to another port, like `222`. When
connecting to the machine to administrate it, you'll use this alternate
port to get a normal login shell.
- You'll run a highly restricted `sshd` on port 22, with a special locked-down
configuration that uses Phabricator to authorize users and execute commands.
- The `sshd` on port 22 **MUST** be 6.2 or newer, because Phabricator relies
on the `AuthorizedKeysCommand` option.
Configuring SSH
===============
Here's a walkthrough of how to perform this configuration in detail:
SSH access requires some additional setup. You will configure and run a second,
restricted copy of `sshd` on the machine, on a different port from the standard
`sshd`. This special copy of `sshd` will serve repository requests and provide
other Phabricator SSH services.
**Move Normal SSHD**: Be careful when editing the configuration for `sshd`. If
you get it wrong, you may lock yourself out of the machine. Restarting `sshd`
generally will not interrupt existing connections, but you should exercise
caution. Two strategies you can use to mitigate this risk are: smoke-test
configuration by starting a second `sshd`; and use a `screen` session which
automatically repairs configuration unless stopped.
NOTE: The Phabricator `sshd` service **MUST** be 6.2 or newer, because
Phabricator relies on the `AuthorizedKeysCommand` option.
To smoke-test a configuration, just start another `sshd` using the `-f` flag:
**Choose a Port**: These instructions will configure the alternate `sshd` on
port `2222`. This is easy to configure, but if you run the service on this port
users will clone and push to URIs like `ssh://git@host.com:2222/`, which is
a little ugly.
sudo /path/to/sshd -f /path/to/config_file.edited
The easiest way to fix this is to put a load balancer in front of the host and
have it forward TCP traffic on port `22` to port `2222`. Then users can clone
from `ssh://git@host.com/` without an explicit port number and you don't need
to do anything else.
You can then connect and make sure the edited config file is valid before
replacing your primary configuration file.
Alternatively, you can move the administrative `sshd` to a new port, then run
Phabricator `sshd` on port 22. This is complicated and risky. See "Moving the
sshd Port" below for help.
To automatically repair configuration, start a `screen` session with a command
like this in it:
Finally, you can just run on port `2222` and accept the explicit port in the
URIs. This is the simplest approach, and you can start here and clean things
up later.
sleep 60 ; mv sshd_config.good sshd_config ; /etc/init.d/sshd restart
If you plan to connect to a port other than `22`, you should set this port
as `diffusion.ssh-port` in your Phabricator config:
The specific command may vary for your system, but the general idea is to have
the machine automatically restore configuration after some period of time if
you don't stop it. If you lock yourself out, this will fix things automatically.
```
$ ./bin/config set diffusion.ssh-port 2222
```
Now that you're ready to edit your configuration, open up your `sshd` config
(often `/etc/ssh/sshd_config`) and change the `Port` setting to some other port,
like `222` (you can choose any port other than 22).
This port is not special, and you are free to choose a different port, provided
you make the appropriate configuration adjustment below.
Port 222
**Configure and Start Phabricator SSHD**: Now, you'll configure and start a
copy of `sshd` which will serve Phabricator services, including repositories,
over SSH.
Very carefully, restart `sshd`. Verify that you can connect on the new port:
ssh -p 222 ...
**Configure and Start Phabricator SSHD**: Now, configure and start a second
`sshd` instance which will run on port `22`. This instance will use a special
locked-down configuration that uses Phabricator to handle authentication and
command execution.
This instance will use a special locked-down configuration that uses
Phabricator to handle authentication and command execution.
There are three major steps:
@ -200,10 +266,16 @@ There are three major steps:
**Create `phabricator-ssh-hook.sh`**: Copy the template in
`phabricator/resources/sshd/phabricator-ssh-hook.sh` to somewhere like
`/usr/libexec/phabricator-ssh-hook.sh` and edit it to have the correct
settings. Then make it owned by `root` and restrict editing:
settings.
sudo chown root /path/to/phabricator-ssh-hook.sh
sudo chmod 755 /path/to/phabricator-ssh-hook.sh
Both the script itself **and** the parent directory the script resides in must
be owned by `root`, and the script must have `755` permissions:
```
$ sudo chown root /path/to/somewhere/
$ sudo chown root /path/to/somewhere/phabricator-ssh-hook.sh
$ sudo chmod 755 /path/to/somewhere/phabricator-ssh-hook.sh
```
If you don't do this, `sshd` will refuse to execute the hook.
@ -215,34 +287,38 @@ Open the file and edit the `AuthorizedKeysCommand`,
`AuthorizedKeysCommandUser`, and `AllowUsers` settings to be correct for your
system.
This configuration file also specifies the `Port` the service should run on.
If you intend to run on a non-default port, adjust it now.
**Start SSHD**: Now, start the Phabricator `sshd`:
sudo /path/to/sshd -f /path/to/sshd_config.phabricator
If you did everything correctly, you should be able to run this:
If you did everything correctly, you should be able to run this command:
echo {} | ssh vcs-user@phabricator.yourcompany.com conduit conduit.ping
```
$ echo {} | ssh vcs-user@phabricator.yourcompany.com conduit conduit.ping
```
...and get a response like this:
{"result":"orbital","error_code":null,"error_info":null}
```lang=json
{"result":"phabricator.yourcompany.com","error_code":null,"error_info":null}
```
(If you get an authentication error, make sure you added your public key in
**Settings > SSH Public Keys**.) If you're having trouble, check the
If you get an authentication error, make sure you added your public key in
{nav Settings > SSH Public Keys}. If you're having trouble, check the
troubleshooting section below.
= Authentication Over HTTP =
Authentication Over SSH
=======================
To authenticate over HTTP, users should configure a **VCS Password** in the
**Settings** screen. This panel is available only if `diffusion.allow-http-auth`
is enabled.
To authenticate over SSH, users should add their public keys under
{nav Settings > SSH Public Keys}.
= Authentication Over SSH =
To authenticate over SSH, users should add **SSH Public Keys** in the
**Settings** screen.
= Cloning a Repository =
Cloning a Repository
====================
If you've already set up a hosted repository, you can try cloning it now. To
do this, browse to the repository's main screen in Diffusion. You should see
@ -253,13 +329,15 @@ To clone the repository, just run the appropriate command.
If you don't see the commands or running them doesn't work, see below for tips
on troubleshooting.
= Troubleshooting HTTP =
Troubleshooting HTTP
====================
Some general tips for troubleshooting problems with HTTP:
- Make sure `diffusion.allow-http-auth` is enabled in your Phabricator config.
- Make sure HTTP serving is enabled for the repository you're trying to clone.
You can find this in {nav Edit Repository > Hosting}.
- Make sure HTTP serving is enabled for the repository you're trying to
clone. You can find this in {nav Edit Repository > Hosting}.
- Make sure you've configured a VCS password. This is separate from your main
account password. You can configure this in {nav Settings > VCS Password}.
- Make sure the main repository screen in Diffusion shows a clone/checkout
@ -281,7 +359,8 @@ with the HTTP response is likely to be useful:
In many cases, this can give you more information about what's wrong.
= Troubleshooting SSH =
Troubleshooting SSH
===================
Some general tips for troubleshooting problems with SSH:
@ -305,11 +384,11 @@ Some general tips for troubleshooting problems with SSH:
- Check your `phabricator-ssh-hook.sh` file for proper settings.
- Check your `sshd_config.phabricator` file for proper settings.
To troubleshoot SSH setup: connect to the server with `ssh`, without running
a command. You may need to use the `-T` flag. You should see a message like
this one:
To troubleshoot SSH setup: connect to the server with `ssh`, without running a
command. You may need to use the `-T` flag, and will need to use `-p` if you
are running on a nonstandard port. You should see a message like this one:
$ ssh -T dweller@secure.phabricator.com
$ ssh -T -p 2222 vcs-user@phabricator.yourcompany.com
phabricator-ssh-exec: Welcome to Phabricator.
You are logged in as alincoln.
@ -332,8 +411,8 @@ settings:
- You're connecting as the `vcs-user`.
- The `vcs-user` has `NP` in `/etc/shadow`.
- The `vcs-user` has `/bin/sh` or some other valid shell in `/etc/passwd`.
- Your SSH key is correct, and you've added it to Phabricator in the Settings
panel.
- Your SSH private key is correct, and you've added the corresponding
public key to Phabricator in the Settings panel.
If you can get this far, but can't execute VCS commands like `git clone`, there
is probably an issue with your `sudoers` configuration. Check:
@ -351,7 +430,7 @@ It may also be helpful to run `sshd` in debug mode:
$ /path/to/sshd -d -d -d -f /path/to/sshd_config.phabricator
This will run it in the foreground and emit a large amount of debugging
information.
information when you connect to it.
Finally, you can usually test that `sudoers` is configured correctly by
doing something like this:
@ -363,7 +442,9 @@ That will try to run the binary via `sudo` in a manner similar to the way that
Phabricator will run it. This can give you better error messages about issues
with `sudoers` configuration.
= Miscellaneous Troubleshooting =
Miscellaneous Troubleshooting
=============================
- If you're getting an error about `svnlook` not being found, add the path
where `svnlook` is located to the Phabricator configuration
@ -371,6 +452,54 @@ with `sudoers` configuration.
is caused by SVN wiping the environment (including PATH) when invoking
commit hooks.
Moving the sshd Port
====================
If you want to move the standard (administrative) `sshd` to a different port to
make Phabricator repository URIs cleaner, this section has some tips.
This is optional, and it is normally easier to do this by putting a load
balancer in front of Phabricator and having it accept TCP traffic on port 22
and forward it to some other port.
When moving `sshd`, be careful when editing the configuration. If you get it
wrong, you may lock yourself out of the machine. Restarting `sshd` generally
will not interrupt existing connections, but you should exercise caution. Two
strategies you can use to mitigate this risk are: smoke-test configuration by
starting a second `sshd`; and use a `screen` session which automatically
repairs configuration unless stopped.
To smoke-test a configuration, just start another `sshd` using the `-f` flag:
sudo /path/to/sshd -f /path/to/config_file.edited
You can then connect and make sure the edited config file is valid before
replacing your primary configuration file.
To automatically repair configuration, start a `screen` session with a command
like this in it:
sleep 60 ; mv sshd_config.good sshd_config ; /etc/init.d/sshd restart
The specific command may vary for your system, but the general idea is to have
the machine automatically restore configuration after some period of time if
you don't stop it. If you lock yourself out, this can fix things automatically.
Now that you're ready to edit your configuration, open up your `sshd` config
(often `/etc/ssh/sshd_config`) and change the `Port` setting to some other port,
like `222` (you can choose any port other than 22).
Port 222
Very carefully, restart `sshd`. Verify that you can connect on the new port:
ssh -p 222 ...
Now you can move the Phabricator `sshd` to port 22, then adjust the value
for `diffusion.ssh-port` in your Phabricator configuration.
No Direct Pushes
================
@ -406,7 +535,8 @@ document provides instructions for configuring. Its absence indicates that the
request did not pass through Phabricator.
= Next Steps =
Next Steps
==========
Once hosted repositories are set up:

View file

@ -9,8 +9,8 @@ Overview
WARNING: This document describes a feature which is still under development,
and is not necessarily accurate or complete.
Phabricator can host, observe, mirror, and proxy repositories. For example,
here are some supported use cases:
Phabricator can create, host, observe, mirror, proxy, and import repositories.
For example, you can:
**Host Repositories**: Phabricator can host repositories locally. Phabricator
maintains the writable master version of the repository, and you can push and
@ -45,3 +45,231 @@ configuring URIs and marking them to be fetched from, mirrored to, clonable,
and so on. By configuring all the URIs that a repository should interact with
and expose to users, you configure the read, write, and mirroring behavior
of the repository.
The remainder of this document walks through this configuration in greater
detail.
Host a Repository
=================
You can create new repositories that Phabricator will host, like you would
create repositories on services like GitHub or Bitbucket. Phabricator will
serve a read-write copy of the repository and you can clone it from Phabricator
and push changes to Phabricator.
If you haven't already, you may need to configure Phabricator for hosting
before you can create your first hosted repository. For a detailed guide,
see @{article:Diffusion User Guide: Repository Hosting}.
This is the default mode for new repositories. To host a repository:
- Create a new repository.
- Activate it.
Phabricator will create an empty repository and allow you to fetch from it and
push to it.
Observe a Repository
====================
If you have an existing repository hosted on another service (like GitHub,
Bitbucket, or a private server) that you want to work with in Phabricator,
you can configure Phabricator to observe it.
When observing a repository, Phabricator will keep track of changes in the
remote repository and allow you to browse and interact with the repository from
the web UI in Diffusion and other applications, but you can continue hosting it
elsewhere.
To observe a repository:
- Create a new repository, but don't activate it yet.
- Add the remote URI you want to observe as a repository URI.
- Set the **I/O Type** for the URI to **Observe**.
- If necessary, configure a credential.
- Activate the repository.
Phabricator will perform an initial import of the repository, creating a local
read-only copy. Once this process completes, it will continue keeping track of
changes in the remote, fetching them, and reflecting them in the UI.
Mirror a Repository
===================
NOTE: Mirroring is not supported in Subversion.
You can create a read-only mirror of an existing repository. Phabricator will
push all changes made to the repository to the mirror.
For example, if you have a repository hosted in Phabricator that you want to
mirror to GitHub, you can configure Phabricator to automatically maintain the
mirror. This is how the upstream repositories are set up.
You can mirror any repository, even if Phabricator is only observing it and not
hosting it directly.
To begin mirroring a repository:
- Create a hosted or observed repository by following the relevant
instructions above.
- Add the remote URI you want to mirror to as a repository URI.
- Set the **I/O Type** for the URI to **Mirror**.
- If necessary, configure a credential.
To stop mirroring:
- Disable the mirror URI; or
- Change the **I/O Type** for the URI to **None**.
Import a Repository
===================
If you have an existing repository that you want to move so it is hosted on
Phabricator, there are three ways to do it:
**Push Everything**: //(Git, Mercurial)// Create a new empty hosted repository
according to the instructions above. Once the empty repository initializes,
push your entire existing repository to it.
**Observe First**: //(Git, Mercurial)// Observe the existing repository first,
according to the instructions above. Once Phabricator's copy of the repository
is fully synchronized, change the **I/O Type** for the **Observe** URI to
**None** to stop fetching changes from the remote.
By default, this will automatically make Phabricator's copy of the repository
writable, and you can begin pushing to it. If you've adjusted URI
configuration away from the defaults, you may need to set at least one URI
to **Read/Write** mode so you can push to it.
**Copy on Disk**: //(Git, Mercurial, Subversion)// Create a new empty hosted
repository according to the instructions above, but do not activate it yet.
Using the **Storage** tab, find the location of the repository's working copy
on disk, and place a working copy of the repository you wish to import there.
For Git and Mercurial, use a bare working copy for best results.
This is the only way to import a Subversion repository because only the master
copy of the repository has history.
Once you've put a working copy in the right place on disk, activate the
repository.
Customizing Displayed Clone URIs
================================
If you have an unusual configuration and want the UI to offers users specific
clone URIs other than the URIs that Phabricator serves or interacts with, you
can add those URIs with the **I/O Type** set to **None** and then set their
**Display Type** to **Always**.
Likewise, you can set the **Display Type** of any URIs you do //not// want
to be visible to **Never**.
This allows you to precisely configure which clone URIs are shown to users for
a repository.
Reference: I/O Types
====================
This section details the available **I/O Type** options for URIs.
Each repository has some **builtin** URIs. These are URIs hosted by Phabricator
itself. The modes available for each URI depend primarily on whether it is a
builtin URI or not.
**Default**: This setting has Phabricator guess the correct option for the
URI.
For **builtin** URIs, the default behavior is //Read/Write// if the repository
is hosted, and //Read-Only// if the repository is observed.
For custom URIs, the default type is //None// because we can not automatically
guess if you want to ignore, observe, or mirror a URI and //None// is the
safest default.
**Observe**: Phabricator will observe this repository and regularly fetch any
changes made to it to a local read-only copy.
You can not observe builtin URIs because reading a repository from itself
does not make sense.
You can not add a URI in Observe mode if an existing builtin URI is in
//Read/Write// mode, because this would mean the repository had two different
authorities: the observed remote copy and the hosted local copy. Take the
other URI out of //Read/Write// mode first.
**Mirror**: Phabricator will push any changes made to this repository to the
remote URI, keeping a read-only mirror hosted at that URI up to date.
This works for both observed and hosted repositories.
This option is not available for builtin URIs because it does not make sense
to mirror a repository to itself.
It is possible to mirror a repository to another repository that is also
hosted by Phabricator by adding that other repository's URI, although this is
silly and probably very rarely of any use.
**None**: Phabricator will not fetch changes from or push changes to this URI.
For builtin URIs, it will not let users fetch changes from or push changes to
this URI.
You can use this mode to turn off an Observe URI after an import, stop a Mirror
URI from updating, or to add URIs that you're only using to customize which
clone URIs are displayed to the user but don't want Phabricator to interact
with directly.
**Read Only**: Phabricator will serve the repository from this URI in read-only
mode. Users will be able to fetch from it but will not be able to push to it.
Because Phabricator must be able to serve the repository from URIs configured
in this mode, this option is only available for builtin URIs.
**Read/Write**: Phabricator will serve the repository from this URI in
read/write mode. Users will be able to fetch from it and push to it.
URIs can not be set into this mode if another URI is set to //Observe// mode,
because that would mean the repository had two different authorities: the
observed remote copy and the hosted local copy. Take the other URI out of
//Observe// mode first.
Because Phabricator must be able to serve the repository from URIs configured
in this mode, this option is only available for builtin URIs.
Reference: Display Types
========================
This section details the available **Display Type** options for URIs.
**Default**: Phabricator will guess the correct option for the URI. It
guesses based on the configured **I/O Type** and whether the URI is
**builtin** or not.
For //Observe//, //Mirror// and //None// URIs, the default is //Never//.
For builtin URIs in //Read Only// or //Read/Write// mode, the most
human-readable URI defaults to //Always// and the others default to //Never//.
**Always**: This URI will be shown to users as a clone/checkout URI. You can
add URIs in this mode to cutomize exactly what users are shown.
**Never**: This URI will not be shown to users. You can hide less-preferred
URIs to guide users to the URIs they should be using to interact with the
repository.
Next Steps
==========
Continue by:
- configuring Phabricator to host repositories with
@{article:Diffusion User Guide: Repository Hosting}.

View file

@ -75,6 +75,12 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
$connection->setReadOnly(true);
}
// Unless this is a script running from the CLI, prevent any query from
// running for more than 30 seconds. See T10849 for discussion.
if (php_sapi_name() != 'cli') {
$connection->setQueryTimeout(30);
}
return $connection;
}

View file

@ -29,6 +29,7 @@
final class PhabricatorGlobalLock extends PhutilLock {
private $conn;
private $isExternalConnection = false;
private static $pool = array();
@ -74,6 +75,7 @@ final class PhabricatorGlobalLock extends PhutilLock {
*/
public function useSpecificConnection(AphrontDatabaseConnection $conn) {
$this->conn = $conn;
$this->isExternalConnection = true;
return $this;
}
@ -109,29 +111,54 @@ final class PhabricatorGlobalLock extends PhutilLock {
$max_allowed_timeout = 2147483;
queryfx($conn, 'SET wait_timeout = %d', $max_allowed_timeout);
$lock_name = $this->getName();
$result = queryfx_one(
$conn,
'SELECT GET_LOCK(%s, %f)',
$this->getName(),
$lock_name,
$wait);
$ok = head($result);
if (!$ok) {
throw new PhutilLockException($this->getName());
throw new PhutilLockException($lock_name);
}
$conn->rememberLock($lock_name);
$this->conn = $conn;
}
protected function doUnlock() {
queryfx(
$this->conn,
'SELECT RELEASE_LOCK(%s)',
$this->getName());
$lock_name = $this->getName();
$conn = $this->conn;
try {
$result = queryfx_one(
$conn,
'SELECT RELEASE_LOCK(%s)',
$lock_name);
$conn->forgetLock($lock_name);
} catch (Exception $ex) {
$result = array(null);
}
$ok = head($result);
if (!$ok) {
// TODO: We could throw here, but then this lock doesn't get marked
// unlocked and we throw again later when exiting. It also doesn't
// particularly matter for any current applications. For now, just
// swallow the error.
}
$this->conn->close();
self::$pool[] = $this->conn;
$this->conn = null;
$this->isExternalConnection = false;
if (!$this->isExternalConnection) {
$conn->close();
self::$pool[] = $conn;
}
}
}

View file

@ -78,6 +78,10 @@ body .phui-header-shell.phui-bleed-header
.phui-header-header .phui-header-icon {
margin-right: 8px;
color: {$lightbluetext};
/* This allows the header text to be triple-clicked to select it in Firefox,
see T10905 for discussion. */
display: inline;
}
.phui-object-box .phui-header-tall .phui-header-header,

View file

@ -118,6 +118,10 @@ JX.install('Leader', {
// Read the current leadership lease.
var lease = self._read();
// Stagger these delays so that they are unlikely to race one another.
var expire_delay = 50;
var usurp_delay = 75;
// If the lease is good, we're all set.
var now = +new Date();
if (lease.until > now) {
@ -135,15 +139,17 @@ JX.install('Leader', {
} else {
// Set a callback to try to become the leader shortly after the
// current lease expires. This lets us recover from cases where the
// leader goes missing quickly.
if (self._timeoout) {
window.clearTimeout(self._timeout);
self._timeout = null;
// current lease expires. This lets us quickly recover from cases
// where the leader goes missing.
// In particular, this can happen in Safari if you close windows or
// quit the browser instead of browsing away: the "pagehide" event
// does not fire when the leader is simply destroyed, so it does not
// evict itself from the throne of power.
if (!self._timeout) {
var usurp_at = (lease.until - now) + usurp_delay;
self._timeout = window.setTimeout(self._usurp, usurp_at);
}
self._timeout = window.setTimeout(
self._usurp,
(lease.until - now) + 50);
follower_callback();
}
@ -174,7 +180,7 @@ JX.install('Leader', {
window.setTimeout(
JX.bind(null, self._callIf, leader_callback, follower_callback),
50);
expire_delay);
},
@ -306,6 +312,7 @@ JX.install('Leader', {
_usurp: function() {
var self = JX.Leader;
self.call(JX.bag);
self._timeout = null;
},