1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-28 17:52:43 +01:00

(stable) Promote 2016 Week 17

This commit is contained in:
epriestley 2016-04-22 17:11:12 -07:00
commit ee92a3f25a
109 changed files with 4379 additions and 531 deletions

View file

@ -7,7 +7,7 @@
*/ */
return array( return array(
'names' => array( 'names' => array(
'core.pkg.css' => 'ce06b6f6', 'core.pkg.css' => '04a95108',
'core.pkg.js' => '37344f3c', 'core.pkg.js' => '37344f3c',
'darkconsole.pkg.js' => 'e7393ebb', 'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => '7ba78475', 'differential.pkg.css' => '7ba78475',
@ -88,7 +88,7 @@ return array(
'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02', 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02',
'rsrc/css/application/phortune/phortune.css' => '9149f103', 'rsrc/css/application/phortune/phortune.css' => '9149f103',
'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad', 'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad',
'rsrc/css/application/phriction/phriction-document-css.css' => 'd1861e06', 'rsrc/css/application/phriction/phriction-document-css.css' => '4282e4ad',
'rsrc/css/application/policy/policy-edit.css' => '815c66f7', 'rsrc/css/application/policy/policy-edit.css' => '815c66f7',
'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43',
'rsrc/css/application/policy/policy.css' => '957ea14c', 'rsrc/css/application/policy/policy.css' => '957ea14c',
@ -104,7 +104,7 @@ return array(
'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/application/uiexample/example.css' => '528b19de',
'rsrc/css/core/core.css' => 'd0801452', 'rsrc/css/core/core.css' => 'd0801452',
'rsrc/css/core/remarkup.css' => '2c9ed46f', 'rsrc/css/core/remarkup.css' => '6aae5360',
'rsrc/css/core/syntax.css' => '9fd11da8', 'rsrc/css/core/syntax.css' => '9fd11da8',
'rsrc/css/core/z-index.css' => '5b6fcf3f', 'rsrc/css/core/z-index.css' => '5b6fcf3f',
'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa',
@ -131,7 +131,7 @@ return array(
'rsrc/css/phui/phui-document-pro.css' => '73e45fd2', 'rsrc/css/phui/phui-document-pro.css' => '73e45fd2',
'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf',
'rsrc/css/phui/phui-document.css' => '9c71d2bf', 'rsrc/css/phui/phui-document.css' => '9c71d2bf',
'rsrc/css/phui/phui-feed-story.css' => '04aec08f', 'rsrc/css/phui/phui-feed-story.css' => 'd8440402',
'rsrc/css/phui/phui-fontkit.css' => '9cda225e', 'rsrc/css/phui/phui-fontkit.css' => '9cda225e',
'rsrc/css/phui/phui-form-view.css' => '6a51768e', 'rsrc/css/phui/phui-form-view.css' => '6a51768e',
'rsrc/css/phui/phui-form.css' => 'aac1d51d', 'rsrc/css/phui/phui-form.css' => 'aac1d51d',
@ -479,7 +479,7 @@ return array(
'rsrc/js/core/behavior-device.js' => 'b5b36110', 'rsrc/js/core/behavior-device.js' => 'b5b36110',
'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '4f6a4b4e', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '4f6a4b4e',
'rsrc/js/core/behavior-error-log.js' => '6882e80a', 'rsrc/js/core/behavior-error-log.js' => '6882e80a',
'rsrc/js/core/behavior-fancy-datepicker.js' => '8ae55229', 'rsrc/js/core/behavior-fancy-datepicker.js' => '568931f3',
'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-file-tree.js' => '88236f00',
'rsrc/js/core/behavior-form.js' => '5c54cbf3', 'rsrc/js/core/behavior-form.js' => '5c54cbf3',
'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c',
@ -622,7 +622,7 @@ return array(
'javelin-behavior-editengine-reorder-fields' => 'b59e1e96', 'javelin-behavior-editengine-reorder-fields' => 'b59e1e96',
'javelin-behavior-error-log' => '6882e80a', 'javelin-behavior-error-log' => '6882e80a',
'javelin-behavior-event-all-day' => '38dcf3c8', 'javelin-behavior-event-all-day' => '38dcf3c8',
'javelin-behavior-fancy-datepicker' => '8ae55229', 'javelin-behavior-fancy-datepicker' => '568931f3',
'javelin-behavior-global-drag-and-drop' => 'c8e57404', 'javelin-behavior-global-drag-and-drop' => 'c8e57404',
'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-herald-rule-editor' => '7ebaeed3',
'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-high-security-warning' => 'a464fe03',
@ -777,7 +777,7 @@ return array(
'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646', 'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => 'e67df814', 'phabricator-prefab' => 'e67df814',
'phabricator-remarkup-css' => '2c9ed46f', 'phabricator-remarkup-css' => '6aae5360',
'phabricator-search-results-css' => '7dea472c', 'phabricator-search-results-css' => '7dea472c',
'phabricator-shaped-request' => '7cbe244b', 'phabricator-shaped-request' => '7cbe244b',
'phabricator-side-menu-view-css' => '3a3d9f41', 'phabricator-side-menu-view-css' => '3a3d9f41',
@ -807,7 +807,7 @@ return array(
'phortune-credit-card-form-css' => '8391eb02', 'phortune-credit-card-form-css' => '8391eb02',
'phortune-css' => '9149f103', 'phortune-css' => '9149f103',
'phrequent-css' => 'ffc185ad', 'phrequent-css' => 'ffc185ad',
'phriction-document-css' => 'd1861e06', 'phriction-document-css' => '4282e4ad',
'phui-action-panel-css' => '91c7b835', 'phui-action-panel-css' => '91c7b835',
'phui-badge-view-css' => '3baef8db', 'phui-badge-view-css' => '3baef8db',
'phui-big-info-view-css' => 'bd903741', 'phui-big-info-view-css' => 'bd903741',
@ -823,7 +823,7 @@ return array(
'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-summary-view-css' => '9ca48bdf',
'phui-document-view-css' => '9c71d2bf', 'phui-document-view-css' => '9c71d2bf',
'phui-document-view-pro-css' => '73e45fd2', 'phui-document-view-pro-css' => '73e45fd2',
'phui-feed-story-css' => '04aec08f', 'phui-feed-story-css' => 'd8440402',
'phui-font-icon-base-css' => '6449bce8', 'phui-font-icon-base-css' => '6449bce8',
'phui-fontkit-css' => '9cda225e', 'phui-fontkit-css' => '9cda225e',
'phui-form-css' => 'aac1d51d', 'phui-form-css' => 'aac1d51d',
@ -1301,6 +1301,13 @@ return array(
'phabricator-drag-and-drop-file-upload', 'phabricator-drag-and-drop-file-upload',
'javelin-workboard-board', 'javelin-workboard-board',
), ),
'568931f3' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'56a1ca03' => array( '56a1ca03' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-behavior-device', 'javelin-behavior-device',
@ -1554,13 +1561,6 @@ return array(
'javelin-install', 'javelin-install',
'javelin-dom', 'javelin-dom',
), ),
'8ae55229' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'8bdb2835' => array( '8bdb2835' => array(
'phui-fontkit-css', 'phui-fontkit-css',
), ),

View file

@ -0,0 +1,14 @@
CREATE TABLE {$NAMESPACE}_repository.repository_uri (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
repositoryPHID VARBINARY(64) NOT NULL,
uri VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
builtinProtocol VARCHAR(32) COLLATE {$COLLATE_TEXT},
builtinIdentifier VARCHAR(32) COLLATE {$COLLATE_TEXT},
ioType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
displayType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
isDisabled BOOL NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_builtin` (repositoryPHID, builtinProtocol, builtinIdentifier)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_repository.repository_uri
ADD credentialPHID VARBINARY(64);

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_repository.repository_workingcopyversion
ADD writeProperties LONGTEXT COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_repository.repository_pushlog
ADD devicePHID VARBINARY(64);

View file

@ -34,7 +34,28 @@ $pattern[] = 'StrictHostKeyChecking=no';
$pattern[] = '-o'; $pattern[] = '-o';
$pattern[] = 'UserKnownHostsFile=/dev/null'; $pattern[] = 'UserKnownHostsFile=/dev/null';
$as_device = getenv('PHABRICATOR_AS_DEVICE');
$credential_phid = getenv('PHABRICATOR_CREDENTIAL'); $credential_phid = getenv('PHABRICATOR_CREDENTIAL');
if ($as_device) {
$device = AlmanacKeys::getLiveDevice();
if (!$device) {
throw new Exception(
pht(
'Attempting to create an SSH connection that authenticates with '.
'the current device, but this host is not configured as a cluster '.
'device.'));
}
if ($credential_phid) {
throw new Exception(
pht(
'Attempting to proxy an SSH connection that authenticates with '.
'both the current device and a specific credential. These options '.
'are mutually exclusive.'));
}
}
if ($credential_phid) { if ($credential_phid) {
$viewer = PhabricatorUser::getOmnipotentUser(); $viewer = PhabricatorUser::getOmnipotentUser();
$key = PassphraseSSHKey::loadFromPHID($credential_phid, $viewer); $key = PassphraseSSHKey::loadFromPHID($credential_phid, $viewer);
@ -45,6 +66,13 @@ if ($credential_phid) {
$arguments[] = $key->getKeyfileEnvelope(); $arguments[] = $key->getKeyfileEnvelope();
} }
if ($as_device) {
$pattern[] = '-l %R';
$arguments[] = AlmanacKeys::getClusterSSHUser();
$pattern[] = '-i %R';
$arguments[] = AlmanacKeys::getKeyPath('device.key');
}
$port = $args->getArg('port'); $port = $args->getArg('port');
if ($port) { if ($port) {
$pattern[] = '-p %d'; $pattern[] = '-p %d';

View file

@ -153,15 +153,11 @@ try {
->splitArguments($original_command); ->splitArguments($original_command);
if ($device) { if ($device) {
// If we're authenticating as a device, the first argument may be a
// "@username" argument to act as a particular user.
$first_argument = head($original_argv);
if (preg_match('/^@/', $first_argument)) {
$act_as_name = array_shift($original_argv); $act_as_name = array_shift($original_argv);
if (!preg_match('/^@/', $act_as_name)) {
throw new Exception(
pht(
'Commands executed by devices must identify an acting user in the '.
'first command argument. This request was not constructed '.
'properly.'));
}
$act_as_name = substr($act_as_name, 1); $act_as_name = substr($act_as_name, 1);
$user = id(new PhabricatorPeopleQuery()) $user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser()) ->setViewer(PhabricatorUser::getOmnipotentUser())
@ -174,11 +170,20 @@ try {
'username ("%s"). There is no user with this username.', 'username ("%s"). There is no user with this username.',
$act_as_name)); $act_as_name));
} }
} else {
$user = PhabricatorUser::getOmnipotentUser();
}
}
if ($user->isOmnipotent()) {
$user_name = 'device/'.$device->getName();
} else {
$user_name = $user->getUsername();
} }
$ssh_log->setData( $ssh_log->setData(
array( array(
'u' => $user->getUsername(), 'u' => $user_name,
'P' => $user->getPHID(), 'P' => $user->getPHID(),
)); ));
@ -187,7 +192,7 @@ try {
pht( pht(
'Your account ("%s") does not have permission to establish SSH '. 'Your account ("%s") does not have permission to establish SSH '.
'sessions. Visit the web interface for more information.', 'sessions. Visit the web interface for more information.',
$user->getUsername())); $user_name));
} }
$workflows = id(new PhutilClassMapQuery()) $workflows = id(new PhutilClassMapQuery())
@ -206,7 +211,7 @@ try {
"Usually, you should run a command like `%s` or `%s` ". "Usually, you should run a command like `%s` or `%s` ".
"rather than connecting directly with SSH.\n\n". "rather than connecting directly with SSH.\n\n".
"Supported commands are: %s.", "Supported commands are: %s.",
$user->getUsername(), $user_name,
'git clone', 'git clone',
'hg push', 'hg push',
implode(', ', array_keys($workflows)))); implode(', ', array_keys($workflows))));

View file

@ -570,6 +570,8 @@ phutil_register_library_map(array(
'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php', 'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php',
'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php', 'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php',
'DiffusionChangeHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionChangeHeraldFieldGroup.php', 'DiffusionChangeHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionChangeHeraldFieldGroup.php',
'DiffusionCommandEngine' => 'applications/diffusion/protocol/DiffusionCommandEngine.php',
'DiffusionCommandEngineTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionCommandEngineTestCase.php',
'DiffusionCommitAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionCommitAffectedFilesHeraldField.php', 'DiffusionCommitAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionCommitAffectedFilesHeraldField.php',
'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php', 'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php',
'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php', 'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php',
@ -634,6 +636,7 @@ phutil_register_library_map(array(
'DiffusionGitBlameQuery' => 'applications/diffusion/query/blame/DiffusionGitBlameQuery.php', 'DiffusionGitBlameQuery' => 'applications/diffusion/query/blame/DiffusionGitBlameQuery.php',
'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php', 'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php',
'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php', 'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php',
'DiffusionGitCommandEngine' => 'applications/diffusion/protocol/DiffusionGitCommandEngine.php',
'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php', 'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php',
'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php', 'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php',
'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php', 'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php',
@ -667,6 +670,7 @@ phutil_register_library_map(array(
'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php', 'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php',
'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php', 'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php',
'DiffusionMercurialBlameQuery' => 'applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php', 'DiffusionMercurialBlameQuery' => 'applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php',
'DiffusionMercurialCommandEngine' => 'applications/diffusion/protocol/DiffusionMercurialCommandEngine.php',
'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php', 'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php', 'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php', 'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
@ -739,6 +743,7 @@ phutil_register_library_map(array(
'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php', 'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php',
'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php', 'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php',
'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', 'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
'DiffusionRepositoryBasicsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php',
'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php', 'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php',
'DiffusionRepositoryClusterManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php', 'DiffusionRepositoryClusterManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php',
'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php', 'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php',
@ -750,27 +755,35 @@ phutil_register_library_map(array(
'DiffusionRepositoryEditAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryEditAutomationController.php', 'DiffusionRepositoryEditAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryEditAutomationController.php',
'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php', 'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php',
'DiffusionRepositoryEditBranchesController' => 'applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php', 'DiffusionRepositoryEditBranchesController' => 'applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php',
'DiffusionRepositoryEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositoryEditConduitAPIMethod.php',
'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php', 'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php',
'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php', 'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php',
'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php', 'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php',
'DiffusionRepositoryEditEncodingController' => 'applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php', 'DiffusionRepositoryEditEncodingController' => 'applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php',
'DiffusionRepositoryEditEngine' => 'applications/diffusion/editor/DiffusionRepositoryEditEngine.php',
'DiffusionRepositoryEditHostingController' => 'applications/diffusion/controller/DiffusionRepositoryEditHostingController.php', 'DiffusionRepositoryEditHostingController' => 'applications/diffusion/controller/DiffusionRepositoryEditHostingController.php',
'DiffusionRepositoryEditMainController' => 'applications/diffusion/controller/DiffusionRepositoryEditMainController.php', 'DiffusionRepositoryEditMainController' => 'applications/diffusion/controller/DiffusionRepositoryEditMainController.php',
'DiffusionRepositoryEditStagingController' => 'applications/diffusion/controller/DiffusionRepositoryEditStagingController.php', 'DiffusionRepositoryEditStagingController' => 'applications/diffusion/controller/DiffusionRepositoryEditStagingController.php',
'DiffusionRepositoryEditStorageController' => 'applications/diffusion/controller/DiffusionRepositoryEditStorageController.php', 'DiffusionRepositoryEditStorageController' => 'applications/diffusion/controller/DiffusionRepositoryEditStorageController.php',
'DiffusionRepositoryEditSubversionController' => 'applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php', 'DiffusionRepositoryEditSubversionController' => 'applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php',
'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php', 'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php',
'DiffusionRepositoryEditproController' => 'applications/diffusion/controller/DiffusionRepositoryEditproController.php',
'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php',
'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php', 'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php',
'DiffusionRepositoryManageController' => 'applications/diffusion/controller/DiffusionRepositoryManageController.php', 'DiffusionRepositoryManageController' => 'applications/diffusion/controller/DiffusionRepositoryManageController.php',
'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php', 'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php',
'DiffusionRepositoryNewController' => 'applications/diffusion/controller/DiffusionRepositoryNewController.php', 'DiffusionRepositoryNewController' => 'applications/diffusion/controller/DiffusionRepositoryNewController.php',
'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php',
'DiffusionRepositoryPoliciesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php',
'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php', 'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php',
'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php', 'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php',
'DiffusionRepositorySearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositorySearchConduitAPIMethod.php',
'DiffusionRepositoryStatusManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStatusManagementPanel.php',
'DiffusionRepositorySymbolsController' => 'applications/diffusion/controller/DiffusionRepositorySymbolsController.php', 'DiffusionRepositorySymbolsController' => 'applications/diffusion/controller/DiffusionRepositorySymbolsController.php',
'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php', 'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php', 'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php',
'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php', 'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php',
'DiffusionRepositoryURIsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php',
'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php', 'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php', 'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php', 'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
@ -779,6 +792,7 @@ phutil_register_library_map(array(
'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', 'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php', 'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
'DiffusionSubversionCommandEngine' => 'applications/diffusion/protocol/DiffusionSubversionCommandEngine.php',
'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php', 'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php',
'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php', 'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php',
'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php', 'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php',
@ -2041,6 +2055,7 @@ phutil_register_library_map(array(
'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php', 'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php',
'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php', 'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php',
'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php', 'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php',
'PhabricatorConfigClusterRepositoriesController' => 'applications/config/controller/PhabricatorConfigClusterRepositoriesController.php',
'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php', 'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php',
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php', 'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php', 'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php',
@ -3169,6 +3184,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php', 'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php',
'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php', 'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php',
'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php', 'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php',
'PhabricatorRepositoryManagementThawWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php',
'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php', 'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php',
'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php', 'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php',
'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php',
@ -3208,6 +3224,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php', 'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php', 'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php', 'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
'PhabricatorRepositoryURI' => 'applications/repository/storage/PhabricatorRepositoryURI.php',
'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php', 'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php',
'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php', 'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php', 'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
@ -3905,6 +3922,7 @@ phutil_register_library_map(array(
'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php', 'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php',
'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php', 'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php',
'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php', 'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php',
'PhrictionMarkupPreviewController' => 'applications/phriction/controller/PhrictionMarkupPreviewController.php',
'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php', 'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php',
'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php', 'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php',
'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php', 'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php',
@ -4760,6 +4778,8 @@ phutil_register_library_map(array(
'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery', 'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery',
'DiffusionChangeController' => 'DiffusionController', 'DiffusionChangeController' => 'DiffusionController',
'DiffusionChangeHeraldFieldGroup' => 'HeraldFieldGroup', 'DiffusionChangeHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionCommandEngine' => 'Phobject',
'DiffusionCommandEngineTestCase' => 'PhabricatorTestCase',
'DiffusionCommitAffectedFilesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAffectedFilesHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField',
@ -4824,6 +4844,7 @@ phutil_register_library_map(array(
'DiffusionGitBlameQuery' => 'DiffusionBlameQuery', 'DiffusionGitBlameQuery' => 'DiffusionBlameQuery',
'DiffusionGitBranch' => 'Phobject', 'DiffusionGitBranch' => 'Phobject',
'DiffusionGitBranchTestCase' => 'PhabricatorTestCase', 'DiffusionGitBranchTestCase' => 'PhabricatorTestCase',
'DiffusionGitCommandEngine' => 'DiffusionCommandEngine',
'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow', 'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGitLFSResponse' => 'AphrontResponse', 'DiffusionGitLFSResponse' => 'AphrontResponse',
@ -4857,6 +4878,7 @@ phutil_register_library_map(array(
'DiffusionLowLevelQuery' => 'Phobject', 'DiffusionLowLevelQuery' => 'Phobject',
'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery',
'DiffusionMercurialBlameQuery' => 'DiffusionBlameQuery', 'DiffusionMercurialBlameQuery' => 'DiffusionBlameQuery',
'DiffusionMercurialCommandEngine' => 'DiffusionCommandEngine',
'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionMercurialRequest' => 'DiffusionRequest', 'DiffusionMercurialRequest' => 'DiffusionRequest',
@ -4929,6 +4951,7 @@ phutil_register_library_map(array(
'DiffusionRefTableController' => 'DiffusionController', 'DiffusionRefTableController' => 'DiffusionController',
'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionRenameHistoryQuery' => 'Phobject', 'DiffusionRenameHistoryQuery' => 'Phobject',
'DiffusionRepositoryBasicsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositoryClusterManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryClusterManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryController' => 'DiffusionController', 'DiffusionRepositoryController' => 'DiffusionController',
@ -4940,27 +4963,35 @@ phutil_register_library_map(array(
'DiffusionRepositoryEditAutomationController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditAutomationController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditBranchesController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditBranchesController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionRepositoryEditController' => 'DiffusionController', 'DiffusionRepositoryEditController' => 'DiffusionController',
'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditEncodingController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditEncodingController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditEngine' => 'PhabricatorEditEngine',
'DiffusionRepositoryEditHostingController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditHostingController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditMainController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditMainController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditStagingController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditStagingController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditStorageController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditStorageController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditSubversionController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditSubversionController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditproController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryListController' => 'DiffusionController', 'DiffusionRepositoryListController' => 'DiffusionController',
'DiffusionRepositoryManageController' => 'DiffusionController', 'DiffusionRepositoryManageController' => 'DiffusionController',
'DiffusionRepositoryManagementPanel' => 'Phobject', 'DiffusionRepositoryManagementPanel' => 'Phobject',
'DiffusionRepositoryNewController' => 'DiffusionController', 'DiffusionRepositoryNewController' => 'DiffusionController',
'DiffusionRepositoryPath' => 'Phobject', 'DiffusionRepositoryPath' => 'Phobject',
'DiffusionRepositoryPoliciesManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryRef' => 'Phobject', 'DiffusionRepositoryRef' => 'Phobject',
'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositorySearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionRepositoryStatusManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositorySymbolsController' => 'DiffusionRepositoryEditController', 'DiffusionRepositorySymbolsController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryTag' => 'Phobject', 'DiffusionRepositoryTag' => 'Phobject',
'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'DiffusionRepositoryURIsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRequest' => 'Phobject', 'DiffusionRequest' => 'Phobject',
'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionResolveUserQuery' => 'Phobject', 'DiffusionResolveUserQuery' => 'Phobject',
@ -4969,6 +5000,7 @@ phutil_register_library_map(array(
'DiffusionServeController' => 'DiffusionController', 'DiffusionServeController' => 'DiffusionController',
'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
'DiffusionSetupException' => 'Exception', 'DiffusionSetupException' => 'Exception',
'DiffusionSubversionCommandEngine' => 'DiffusionCommandEngine',
'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow',
'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow', 'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow',
'DiffusionSubversionWireProtocol' => 'Phobject', 'DiffusionSubversionWireProtocol' => 'Phobject',
@ -6471,6 +6503,7 @@ phutil_register_library_map(array(
'PhabricatorConfigCacheController' => 'PhabricatorConfigController', 'PhabricatorConfigCacheController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterRepositoriesController' => 'PhabricatorConfigController',
'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule', 'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule',
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType', 'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType',
@ -7746,6 +7779,7 @@ phutil_register_library_map(array(
'PhabricatorDestructibleInterface', 'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface', 'PhabricatorProjectInterface',
'PhabricatorSpacesInterface', 'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
), ),
'PhabricatorRepositoryAuditRequest' => array( 'PhabricatorRepositoryAuditRequest' => array(
'PhabricatorRepositoryDAO', 'PhabricatorRepositoryDAO',
@ -7803,6 +7837,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementThawWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
@ -7857,6 +7892,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryType' => 'Phobject', 'PhabricatorRepositoryType' => 'Phobject',
'PhabricatorRepositoryURI' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryURINormalizer' => 'Phobject', 'PhabricatorRepositoryURINormalizer' => 'Phobject',
'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
@ -8702,6 +8738,7 @@ phutil_register_library_map(array(
'PhrictionHistoryController' => 'PhrictionController', 'PhrictionHistoryController' => 'PhrictionController',
'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionListController' => 'PhrictionController', 'PhrictionListController' => 'PhrictionController',
'PhrictionMarkupPreviewController' => 'PhabricatorController',
'PhrictionMoveController' => 'PhrictionController', 'PhrictionMoveController' => 'PhrictionController',
'PhrictionNewController' => 'PhrictionController', 'PhrictionNewController' => 'PhrictionController',
'PhrictionRemarkupRule' => 'PhutilRemarkupRule', 'PhrictionRemarkupRule' => 'PhutilRemarkupRule',

View file

@ -132,7 +132,7 @@ final class AlmanacServiceViewController
->setHideServiceColumn(true); ->setHideServiceColumn(true);
$header = id(new PHUIHeaderView()) $header = id(new PHUIHeaderView())
->setHeader(pht('SERVICE BINDINGS')) ->setHeader(pht('Service Bindings'))
->addActionLink( ->addActionLink(
id(new PHUIButtonView()) id(new PHUIButtonView())
->setTag('a') ->setTag('a')

View file

@ -48,4 +48,22 @@ final class AlmanacKeys extends Phobject {
return $device; return $device;
} }
public static function getClusterSSHUser() {
// NOTE: When instancing, we currently use the SSH username to figure out
// which instance you are connecting to. We can't use the host name because
// we have no way to tell which host you think you're reaching: the SSH
// protocol does not have a mechanism like a "Host" header.
$username = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($username)) {
return $username;
}
$username = PhabricatorEnv::getEnvConfig('diffusion.ssh-user');
if (strlen($username)) {
return $username;
}
return null;
}
} }

View file

@ -7,10 +7,11 @@ final class PhabricatorAphlictManagementStatusWorkflow
$this $this
->setName('status') ->setName('status')
->setSynopsis(pht('Show the status of the notification server.')) ->setSynopsis(pht('Show the status of the notification server.'))
->setArguments(array()); ->setArguments($this->getLaunchArguments());
} }
public function execute(PhutilArgumentParser $args) { public function execute(PhutilArgumentParser $args) {
$this->parseLaunchArguments($args);
$console = PhutilConsole::getConsole(); $console = PhutilConsole::getConsole();
$pid = $this->getPID(); $pid = $this->getPID();

View file

@ -301,7 +301,7 @@ abstract class PhabricatorAphlictManagementWorkflow
return $pid; return $pid;
} }
final public function cleanup($signo = '?') { final public function cleanup($signo = null) {
global $g_future; global $g_future;
if ($g_future) { if ($g_future) {
$g_future->resolveKill(); $g_future->resolveKill();
@ -310,6 +310,11 @@ abstract class PhabricatorAphlictManagementWorkflow
Filesystem::remove($this->getPIDPath()); Filesystem::remove($this->getPIDPath());
if ($signo !== null) {
$signame = phutil_get_signal_name($signo);
error_log("Caught signal {$signame}, exiting.");
}
exit(1); exit(1);
} }
@ -428,6 +433,15 @@ abstract class PhabricatorAphlictManagementWorkflow
$console = PhutilConsole::getConsole(); $console = PhutilConsole::getConsole();
$this->willLaunch(); $this->willLaunch();
$log = $this->getOverseerLogPath();
if ($log !== null) {
echo tsprintf(
"%s\n",
pht(
'Writing logs to: %s',
$log));
}
$pid = pcntl_fork(); $pid = pcntl_fork();
if ($pid < 0) { if ($pid < 0) {
throw new Exception( throw new Exception(
@ -439,6 +453,12 @@ abstract class PhabricatorAphlictManagementWorkflow
exit(0); exit(0);
} }
// Redirect process errors to the error log. If we do not do this, any
// error the `aphlict` process itself encounters vanishes into thin air.
if ($log !== null) {
ini_set('error_log', $log);
}
// When we fork, the child process will inherit its parent's set of open // When we fork, the child process will inherit its parent's set of open
// file descriptors. If the parent process of bin/aphlict is waiting for // file descriptors. If the parent process of bin/aphlict is waiting for
// bin/aphlict's file descriptors to close, it will be stuck waiting on // bin/aphlict's file descriptors to close, it will be stuck waiting on
@ -529,4 +549,15 @@ abstract class PhabricatorAphlictManagementWorkflow
$server_argv); $server_argv);
} }
private function getOverseerLogPath() {
// For now, just return the first log. We could refine this eventually.
$logs = idx($this->configData, 'logs', array());
foreach ($logs as $log) {
return $log['path'];
}
return null;
}
} }

View file

@ -12,7 +12,7 @@ final class PhabricatorAuthRegisterController
$account_key = $request->getURIData('akey'); $account_key = $request->getURIData('akey');
if ($request->getUser()->isLoggedIn()) { if ($request->getUser()->isLoggedIn()) {
return $this->renderError(pht('You are already logged in.')); return id(new AphrontRedirectResponse())->setURI('/');
} }
$is_setup = false; $is_setup = false;

View file

@ -82,7 +82,8 @@ final class PhabricatorCalendarEventCancelController
} else if ($is_parent) { } else if ($is_parent) {
$title = pht('Reinstate Recurrence'); $title = pht('Reinstate Recurrence');
$paragraph = pht( $paragraph = pht(
'Reinstate the entire series of recurring events?'); 'Reinstate all instances of this recurrence
that have not been individually cancelled?');
$cancel = pht("Don't Reinstate Recurrence"); $cancel = pht("Don't Reinstate Recurrence");
$submit = pht('Reinstate Recurrence'); $submit = pht('Reinstate Recurrence');
} else { } else {

View file

@ -459,7 +459,7 @@ final class PhabricatorCalendarEventEditController
} }
$projects = id(new AphrontFormTokenizerControl()) $projects = id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setValue($projects) ->setValue($projects)
->setUser($viewer) ->setUser($viewer)

View file

@ -219,8 +219,8 @@ final class PhabricatorCalendarEventViewController
$reinstate_label = pht('Reinstate This Instance'); $reinstate_label = pht('Reinstate This Instance');
$cancel_disabled = (!$can_edit || $can_reinstate); $cancel_disabled = (!$can_edit || $can_reinstate);
} else if ($event->getIsRecurrenceParent()) { } else if ($event->getIsRecurrenceParent()) {
$cancel_label = pht('Cancel Recurrence'); $cancel_label = pht('Cancel All');
$reinstate_label = pht('Reinstate Recurrence'); $reinstate_label = pht('Reinstate All');
$cancel_disabled = !$can_edit; $cancel_disabled = !$can_edit;
} else { } else {
$cancel_label = pht('Cancel Event'); $cancel_label = pht('Cancel Event');

View file

@ -271,8 +271,11 @@ final class PhabricatorCalendarEventSearchEngine
$attendees = array(); $attendees = array();
foreach ($event->getInvitees() as $invitee) { foreach ($event->getInvitees() as $invitee) {
$status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
if ($invitee->getStatus() === $status_attending) {
$attendees[] = $invitee->getInviteePHID(); $attendees[] = $invitee->getInviteePHID();
} }
}
if ($event->getIsGhostEvent()) { if ($event->getIsGhostEvent()) {
$title_text = $event->getMonogram() $title_text = $event->getMonogram()

View file

@ -208,6 +208,11 @@ final class CelerityResourceMap extends Phobject {
} }
public function getHashForName($name) {
return idx($this->nameMap, $name);
}
/** /**
* Return the absolute URI for a resource, identified by hash. * Return the absolute URI for a resource, identified by hash.
* This method is fairly low-level and ignores packaging. * This method is fairly low-level and ignores packaging.

View file

@ -31,7 +31,11 @@ final class CelerityPhabricatorResourceController
return new Aphront400Response(); return new Aphront400Response();
} }
return $this->serveResource($this->path); return $this->serveResource(
array(
'path' => $this->path,
'hash' => $this->hash,
));
} }
protected function buildResourceTransformer() { protected function buildResourceTransformer() {

View file

@ -24,7 +24,10 @@ abstract class CelerityResourceController extends PhabricatorController {
abstract public function getCelerityResourceMap(); abstract public function getCelerityResourceMap();
protected function serveResource($path, $package_hash = null) { protected function serveResource(array $spec) {
$path = $spec['path'];
$hash = idx($spec, 'hash');
// Sanity checking to keep this from exposing anything sensitive, since it // Sanity checking to keep this from exposing anything sensitive, since it
// ultimately boils down to disk reads. // ultimately boils down to disk reads.
if (preg_match('@(//|\.\.)@', $path)) { if (preg_match('@(//|\.\.)@', $path)) {
@ -40,18 +43,24 @@ abstract class CelerityResourceController extends PhabricatorController {
$dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); $dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
if (AphrontRequest::getHTTPHeader('If-Modified-Since') && !$dev_mode) { $map = $this->getCelerityResourceMap();
$expect_hash = $map->getHashForName($path);
// Test if the URI hash is correct for our current resource map. If it
// 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);
if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) {
// Return a "304 Not Modified". We don't care about the value of this // 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. // field since we never change what resource is served by a given URI.
return $this->makeResponseCacheable(new Aphront304Response()); return $this->makeResponseCacheable(new Aphront304Response());
} }
$is_cacheable = (!$dev_mode) &&
$this->isCacheableResourceType($type);
$cache = null; $cache = null;
$data = null; $data = null;
if ($is_cacheable) { if ($is_cacheable && !$dev_mode) {
$cache = PhabricatorCaches::getImmutableCache(); $cache = PhabricatorCaches::getImmutableCache();
$request_path = $this->getRequest()->getPath(); $request_path = $this->getRequest()->getPath();
@ -61,8 +70,6 @@ abstract class CelerityResourceController extends PhabricatorController {
} }
if ($data === null) { if ($data === null) {
$map = $this->getCelerityResourceMap();
if ($map->isPackageResource($path)) { if ($map->isPackageResource($path)) {
$resource_names = $map->getResourceNamesForPackageName($path); $resource_names = $map->getResourceNamesForPackageName($path);
if (!$resource_names) { if (!$resource_names) {
@ -117,7 +124,11 @@ abstract class CelerityResourceController extends PhabricatorController {
$response->addAllowOrigin('*'); $response->addAllowOrigin('*');
} }
return $this->makeResponseCacheable($response); if ($is_cacheable) {
$response = $this->makeResponseCacheable($response);
}
return $response;
} }
public static function getSupportedResourceTypes() { public static function getSupportedResourceTypes() {

View file

@ -23,25 +23,8 @@ final class PhabricatorConduitConsoleController
$call_uri = '/api/'.$method->getAPIMethodName(); $call_uri = '/api/'.$method->getAPIMethodName();
$status = $method->getMethodStatus();
$reason = $method->getMethodStatusDescription();
$errors = array(); $errors = array();
switch ($status) {
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$reason = nonempty($reason, pht('This method is deprecated.'));
$errors[] = pht('Deprecated Method: %s', $reason);
break;
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$reason = nonempty(
$reason,
pht(
'This method is new and unstable. Its interface is subject '.
'to change.'));
$errors[] = pht('Unstable Method: %s', $reason);
break;
}
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setAction($call_uri) ->setAction($call_uri)
->setUser($request->getUser()) ->setUser($request->getUser())
@ -127,6 +110,41 @@ final class PhabricatorConduitConsoleController
$view = id(new PHUIPropertyListView()); $view = id(new PHUIPropertyListView());
$status = $method->getMethodStatus();
$reason = $method->getMethodStatusDescription();
switch ($status) {
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$stability_icon = 'fa-exclamation-triangle yellow';
$stability_label = pht('Unstable Method');
$stability_info = nonempty(
$reason,
pht(
'This method is new and unstable. Its interface is subject '.
'to change.'));
break;
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$stability_icon = 'fa-exclamation-triangle red';
$stability_label = pht('Deprecated Method');
$stability_info = nonempty($reason, pht('This method is deprecated.'));
break;
default:
$stability_label = null;
break;
}
if ($stability_label) {
$view->addProperty(
pht('Stability'),
array(
id(new PHUIIconView())->setIcon($stability_icon),
' ',
phutil_tag('strong', array(), $stability_label.':'),
' ',
$stability_info,
));
}
$view->addProperty( $view->addProperty(
pht('Returns'), pht('Returns'),
$method->getReturnType()); $method->getReturnType());

View file

@ -65,6 +65,7 @@ final class PhabricatorConfigApplication extends PhabricatorApplication {
'cluster/' => array( 'cluster/' => array(
'databases/' => 'PhabricatorConfigClusterDatabasesController', 'databases/' => 'PhabricatorConfigClusterDatabasesController',
'notifications/' => 'PhabricatorConfigClusterNotificationsController', 'notifications/' => 'PhabricatorConfigClusterNotificationsController',
'repositories/' => 'PhabricatorConfigClusterRepositoriesController',
), ),
), ),
); );

View file

@ -102,14 +102,7 @@ final class PhabricatorBinariesSetupCheck extends PhabricatorSetupCheck {
$version = null; $version = null;
switch ($vcs['versionControlSystem']) { switch ($vcs['versionControlSystem']) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$bad_versions = array( $bad_versions = array();
'< 2.7.4' => pht(
'Prior to 2.7.4, Git contains two remote code execution '.
'vulnerabilities which allow an attacker to take control of a '.
'system by crafting a commit which affects very long paths, '.
'then pushing it or tricking a victim into fetching it. This '.
'is a severe security vulnerability.'),
);
list($err, $stdout, $stderr) = exec_manual('git --version'); list($err, $stdout, $stderr) = exec_manual('git --version');
$version = trim(substr($stdout, strlen('git version '))); $version = trim(substr($stdout, strlen('git version ')));
break; break;

View file

@ -0,0 +1,343 @@
<?php
final class PhabricatorConfigClusterRepositoriesController
extends PhabricatorConfigController {
public function handleRequest(AphrontRequest $request) {
$nav = $this->buildSideNavView();
$nav->selectFilter('cluster/repositories/');
$title = pht('Repository Servers');
$crumbs = $this
->buildApplicationCrumbs($nav)
->addTextCrumb(pht('Repository Servers'));
$repository_status = $this->buildClusterRepositoryStatus();
$view = id(new PHUITwoColumnView())
->setNavigation($nav)
->setMainColumn($repository_status);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildClusterRepositoryStatus() {
$viewer = $this->getViewer();
Javelin::initBehavior('phabricator-tooltips');
$all_services = id(new AlmanacServiceQuery())
->setViewer($viewer)
->withServiceTypes(
array(
AlmanacClusterRepositoryServiceType::SERVICETYPE,
))
->needBindings(true)
->needProperties(true)
->execute();
$all_services = mpull($all_services, null, 'getPHID');
$all_repositories = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withHosted(PhabricatorRepositoryQuery::HOSTED_PHABRICATOR)
->withTypes(
array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
))
->execute();
$all_repositories = mpull($all_repositories, null, 'getPHID');
$all_versions = id(new PhabricatorRepositoryWorkingCopyVersion())
->loadAll();
$all_devices = $this->getDevices($all_services, false);
$all_active_devices = $this->getDevices($all_services, true);
$leader_versions = $this->getLeaderVersionsByRepository(
$all_repositories,
$all_versions,
$all_active_devices);
$push_times = $this->loadLeaderPushTimes($leader_versions);
$repository_groups = mgroup($all_repositories, 'getAlmanacServicePHID');
$repository_versions = mgroup($all_versions, 'getRepositoryPHID');
$rows = array();
foreach ($all_services as $service) {
$service_phid = $service->getPHID();
if ($service->getAlmanacPropertyValue('closed')) {
$status_icon = 'fa-folder';
$status_tip = pht('Closed');
} else {
$status_icon = 'fa-folder-open green';
$status_tip = pht('Open');
}
$status_icon = id(new PHUIIconView())
->setIcon($status_icon)
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $status_tip,
));
$devices = idx($all_devices, $service_phid, array());
$active_devices = idx($all_active_devices, $service_phid, array());
$device_icon = 'fa-server green';
$device_label = pht(
'%s Active',
phutil_count($active_devices));
$device_status = array(
id(new PHUIIconView())->setIcon($device_icon),
' ',
$device_label,
);
$repositories = idx($repository_groups, $service_phid, array());
$repository_status = pht(
'%s',
phutil_count($repositories));
$no_leader = array();
$full_sync = array();
$partial_sync = array();
$no_sync = array();
$lag = array();
// Threshold in seconds before we start complaining that repositories
// are not synchronized when there is only one leader.
$threshold = phutil_units('5 minutes in seconds');
$messages = array();
foreach ($repositories as $repository) {
$repository_phid = $repository->getPHID();
$leader_version = idx($leader_versions, $repository_phid);
if ($leader_version === null) {
$no_leader[] = $repository;
$messages[] = pht(
'Repository %s has an ambiguous leader.',
$viewer->renderHandle($repository_phid)->render());
continue;
}
$versions = idx($repository_versions, $repository_phid, array());
$leaders = 0;
foreach ($versions as $version) {
if ($version->getRepositoryVersion() == $leader_version) {
$leaders++;
}
}
if ($leaders == count($active_devices)) {
$full_sync[] = $repository;
} else {
$push_epoch = idx($push_times, $repository_phid);
if ($push_epoch) {
$duration = (PhabricatorTime::getNow() - $push_epoch);
$lag[] = $duration;
} else {
$duration = null;
}
if ($leaders >= 2 || ($duration && ($duration < $threshold))) {
$partial_sync[] = $repository;
} else {
$no_sync[] = $repository;
if ($push_epoch) {
$messages[] = pht(
'Repository %s has unreplicated changes (for %s).',
$viewer->renderHandle($repository_phid)->render(),
phutil_format_relative_time($duration));
} else {
$messages[] = pht(
'Repository %s has unreplicated changes.',
$viewer->renderHandle($repository_phid)->render());
}
}
}
}
$with_lag = false;
if ($no_leader) {
$replication_icon = 'fa-times red';
$replication_label = pht('Ambiguous Leader');
} else if ($no_sync) {
$replication_icon = 'fa-refresh yellow';
$replication_label = pht('Unsynchronized');
$with_lag = true;
} else if ($partial_sync) {
$replication_icon = 'fa-refresh green';
$replication_label = pht('Partial');
$with_lag = true;
} else if ($full_sync) {
$replication_icon = 'fa-check green';
$replication_label = pht('Synchronized');
} else {
$replication_icon = 'fa-times grey';
$replication_label = pht('No Repositories');
}
if ($with_lag && $lag) {
$lag_status = phutil_format_relative_time(max($lag));
$lag_status = pht(' (%s)', $lag_status);
} else {
$lag_status = null;
}
$replication_status = array(
id(new PHUIIconView())->setIcon($replication_icon),
' ',
$replication_label,
$lag_status,
);
$messages = phutil_implode_html(phutil_tag('br'), $messages);
$rows[] = array(
$status_icon,
$viewer->renderHandle($service->getPHID()),
$device_status,
$repository_status,
$replication_status,
$messages,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(
pht('No repository cluster services are configured.'))
->setHeaders(
array(
null,
pht('Service'),
pht('Devices'),
pht('Repos'),
pht('Sync'),
pht('Messages'),
))
->setColumnClasses(
array(
null,
'pri',
null,
null,
null,
'wide',
));
$doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories');
$header = id(new PHUIHeaderView())
->setHeader(pht('Cluster Repository Status'))
->addActionLink(
id(new PHUIButtonView())
->setIcon('fa-book')
->setHref($doc_href)
->setTag('a')
->setText(pht('Documentation')));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setTable($table);
}
private function getDevices(
array $all_services,
$only_active) {
$devices = array();
foreach ($all_services as $service) {
$map = array();
foreach ($service->getBindings() as $binding) {
if ($only_active && $binding->getIsDisabled()) {
continue;
}
$device = $binding->getDevice();
$device_phid = $device->getPHID();
$map[$device_phid] = $device;
}
$devices[$service->getPHID()] = $map;
}
return $devices;
}
private function getLeaderVersionsByRepository(
array $all_repositories,
array $all_versions,
array $active_devices) {
$version_map = mgroup($all_versions, 'getRepositoryPHID');
$result = array();
foreach ($all_repositories as $repository_phid => $repository) {
$service_phid = $repository->getAlmanacServicePHID();
if (!$service_phid) {
continue;
}
$devices = idx($active_devices, $service_phid);
if (!$devices) {
continue;
}
$versions = idx($version_map, $repository_phid, array());
$versions = mpull($versions, null, 'getDevicePHID');
$versions = array_select_keys($versions, array_keys($devices));
if (!$versions) {
continue;
}
$leader = (int)max(mpull($versions, 'getRepositoryVersion'));
$result[$repository_phid] = $leader;
}
return $result;
}
private function loadLeaderPushTimes(array $leader_versions) {
$viewer = $this->getViewer();
if (!$leader_versions) {
return array();
}
$events = id(new PhabricatorRepositoryPushEventQuery())
->setViewer($viewer)
->withIDs($leader_versions)
->execute();
$events = mpull($events, null, 'getID');
$result = array();
foreach ($leader_versions as $key => $version) {
$event = idx($events, $version);
if (!$event) {
continue;
}
$result[$key] = $event->getEpoch();
}
return $result;
}
}

View file

@ -25,6 +25,7 @@ abstract class PhabricatorConfigController extends PhabricatorController {
$nav->addLabel(pht('Cluster')); $nav->addLabel(pht('Cluster'));
$nav->addFilter('cluster/databases/', pht('Database Servers')); $nav->addFilter('cluster/databases/', pht('Database Servers'));
$nav->addFilter('cluster/notifications/', pht('Notification Servers')); $nav->addFilter('cluster/notifications/', pht('Notification Servers'));
$nav->addFilter('cluster/repositories/', pht('Repository Servers'));
$nav->addLabel(pht('Welcome')); $nav->addLabel(pht('Welcome'));
$nav->addFilter('welcome/', pht('Welcome Screen')); $nav->addFilter('welcome/', pht('Welcome Screen'));
$nav->addLabel(pht('Modules')); $nav->addLabel(pht('Modules'));

View file

@ -161,7 +161,7 @@ final class PhabricatorDashboardEditController
$form->appendControl( $form->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setValue($v_projects) ->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource())); ->setDatasource(new PhabricatorProjectDatasource()));

View file

@ -240,7 +240,7 @@ final class PhabricatorDashboardPanelEditController
$form->appendControl( $form->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setValue($v_projects) ->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource())); ->setDatasource(new PhabricatorProjectDatasource()));

View file

@ -8,7 +8,7 @@ final class DifferentialProjectsField
} }
public function getFieldName() { public function getFieldName() {
return pht('Projects'); return pht('Tags');
} }
public function getFieldDescription() { public function getFieldDescription() {
@ -76,6 +76,7 @@ final class DifferentialProjectsField
public function getCommitMessageLabels() { public function getCommitMessageLabels() {
return array( return array(
'Tags',
'Project', 'Project',
'Projects', 'Projects',
); );

View file

@ -179,7 +179,7 @@ final class DifferentialRevisionSearchEngine
->setValue($repository_phids)) ->setValue($repository_phids))
->appendControl( ->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setDatasource(new PhabricatorProjectLogicalDatasource()) ->setDatasource(new PhabricatorProjectLogicalDatasource())
->setValue($projects)) ->setValue($projects))

View file

@ -55,8 +55,10 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
=> 'DiffusionCommitController', => 'DiffusionCommitController',
'/diffusion/' => array( '/diffusion/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' $this->getQueryRoutePattern()
=> 'DiffusionRepositoryListController', => 'DiffusionRepositoryListController',
$this->getEditRoutePattern('editpro/') =>
'DiffusionRepositoryEditproController',
'new/' => 'DiffusionRepositoryNewController', 'new/' => 'DiffusionRepositoryNewController',
'(?P<edit>create)/' => 'DiffusionRepositoryCreateController', '(?P<edit>create)/' => 'DiffusionRepositoryCreateController',
'(?P<edit>import)/' => 'DiffusionRepositoryCreateController', '(?P<edit>import)/' => 'DiffusionRepositoryCreateController',

View file

@ -131,7 +131,8 @@ final class DiffusionHistoryQueryConduitAPIMethod
hgsprintf('reverse(ancestors(%s))', $commit_hash), hgsprintf('reverse(ancestors(%s))', $commit_hash),
$path_arg); $path_arg);
$stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout); $stdout = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
$stdout);
$lines = explode("\n", trim($stdout)); $lines = explode("\n", trim($stdout));
$lines = array_slice($lines, $offset); $lines = array_slice($lines, $offset);

View file

@ -42,6 +42,9 @@ final class DiffusionQueryCommitsConduitAPIMethod
->executeOne(); ->executeOne();
if ($repository) { if ($repository) {
$query->withRepository($repository); $query->withRepository($repository);
if ($bypass_cache) {
$repository->synchronizeWorkingCopyBeforeRead();
}
} }
} }

View file

@ -145,6 +145,12 @@ abstract class DiffusionQueryConduitAPIMethod
$this->setDiffusionRequest($drequest); $this->setDiffusionRequest($drequest);
// TODO: Allow web UI queries opt out of this if they don't care about
// 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();
return $this->getResult($request); return $this->getResult($request);
} }
} }

View file

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

View file

@ -0,0 +1,18 @@
<?php
final class DiffusionRepositorySearchConduitAPIMethod
extends PhabricatorSearchEngineAPIMethod {
public function getAPIMethodName() {
return 'diffusion.repository.search';
}
public function newSearchEngine() {
return new PhabricatorRepositorySearchEngine();
}
public function getMethodSummary() {
return pht('Read information about repositories.');
}
}

View file

@ -50,8 +50,7 @@ final class DiffusionPushEventViewController
$updates_table = id(new DiffusionPushLogListView()) $updates_table = id(new DiffusionPushLogListView())
->setUser($viewer) ->setUser($viewer)
->setLogs($logs) ->setLogs($logs);
->setHandles($this->loadViewerHandles(mpull($logs, 'getPusherPHID')));
$update_box = id(new PHUIObjectBoxView()) $update_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('All Pushed Updates')) ->setHeaderText(pht('All Pushed Updates'))

View file

@ -162,9 +162,15 @@ final class DiffusionRepositoryCreateController
$activate = $form->getPage('done') $activate = $form->getPage('done')
->getControl('activate')->getValue(); ->getControl('activate')->getValue();
if ($activate == 'start') {
$initial_status = PhabricatorRepository::STATUS_ACTIVE;
} else {
$initial_status = PhabricatorRepository::STATUS_INACTIVE;
}
$xactions[] = id(clone $template) $xactions[] = id(clone $template)
->setTransactionType($type_activate) ->setTransactionType($type_activate)
->setNewValue(($activate == 'start')); ->setNewValue($initial_status);
if ($service) { if ($service) {
$xactions[] = id(clone $template) $xactions[] = id(clone $template)

View file

@ -16,12 +16,19 @@ final class DiffusionRepositoryEditActivateController
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
if ($request->isFormPost()) { if ($request->isFormPost()) {
if (!$repository->isTracked()) {
$new_status = PhabricatorRepository::STATUS_ACTIVE;
} else {
$new_status = PhabricatorRepository::STATUS_INACTIVE;
}
$xaction = id(new PhabricatorRepositoryTransaction()) $xaction = id(new PhabricatorRepositoryTransaction())
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ACTIVATE) ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ACTIVATE)
->setNewValue(!$repository->isTracked()); ->setNewValue($new_status);
$editor = id(new PhabricatorRepositoryEditor()) $editor = id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true) ->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request) ->setContentSourceFromRequest($request)
->setActor($viewer) ->setActor($viewer)
->applyTransactions($repository, array($xaction)); ->applyTransactions($repository, array($xaction));

View file

@ -0,0 +1,12 @@
<?php
final class DiffusionRepositoryEditproController
extends DiffusionRepositoryEditController {
public function handleRequest(AphrontRequest $request) {
return id(new DiffusionRepositoryEditEngine())
->setController($this)
->buildResponse();
}
}

View file

@ -30,7 +30,8 @@ final class DiffusionRepositoryManageController
foreach ($panels as $panel) { foreach ($panels as $panel) {
$panel $panel
->setViewer($viewer) ->setViewer($viewer)
->setRepository($repository); ->setRepository($repository)
->setController($this);
} }
$selected = $request->getURIData('panel'); $selected = $request->getURIData('panel');
@ -63,10 +64,30 @@ final class DiffusionRepositoryManageController
$repository->getPathURI('manage/')); $repository->getPathURI('manage/'));
$crumbs->addTextCrumb($panel->getManagementPanelLabel()); $crumbs->addTextCrumb($panel->getManagementPanelLabel());
$header_text = pht(
'%s: %s',
$repository->getDisplayName(),
$panel->getManagementPanelLabel());
$header = id(new PHUIHeaderView())
->setHeader($header_text)
->setHeaderIcon('fa-pencil');
if ($repository->isTracked()) {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
} else {
$header->setStatus('fa-ban', 'dark', pht('Inactive'));
}
$view = id(new PHUITwoColumnView()) $view = id(new PHUITwoColumnView())
->setHeader($header)
->setNavigation($nav) ->setNavigation($nav)
->setMainColumn($content); ->setMainColumn($content);
$curtain = $panel->buildManagementPanelCurtain();
if ($curtain) {
$view->setCurtain($curtain);
}
return $this->newPage() return $this->newPage()
->setTitle($title) ->setTitle($title)
->setCrumbs($crumbs) ->setCrumbs($crumbs)
@ -95,5 +116,14 @@ final class DiffusionRepositoryManageController
return $nav; return $nav;
} }
public function newTimeline(PhabricatorRepository $repository) {
$timeline = $this->buildTransactionTimeline(
$repository,
new PhabricatorRepositoryTransactionQuery());
$timeline->setShouldTerminate(true);
return $timeline;
}
} }

View file

@ -538,10 +538,35 @@ final class DiffusionServeController extends DiffusionController {
$command = csprintf('%s', $bin); $command = csprintf('%s', $bin);
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$did_write_lock = false;
if ($this->isReadOnlyRequest($repository)) {
$repository->synchronizeWorkingCopyBeforeRead();
} else {
$did_write_lock = true;
$repository->synchronizeWorkingCopyBeforeWrite($viewer);
}
$caught = null;
try {
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true) ->setEnv($env, true)
->write($input) ->write($input)
->resolve(); ->resolve();
} catch (Exception $ex) {
$caught = $ex;
}
if ($did_write_lock) {
$repository->synchronizeWorkingCopyAfterWrite();
}
unset($unguarded);
if ($caught) {
throw $caught;
}
if ($err) { if ($err) {
if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) { if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) {

View file

@ -0,0 +1,172 @@
<?php
final class DiffusionRepositoryEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'diffusion.repository';
public function isEngineConfigurable() {
return false;
}
public function getEngineName() {
return pht('Repositories');
}
public function getSummaryHeader() {
return pht('Edit Repositories');
}
public function getSummaryText() {
return pht('Creates and edits repositories.');
}
public function getEngineApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
protected function newEditableObject() {
$viewer = $this->getViewer();
return PhabricatorRepository::initializeNewRepository($viewer);
}
protected function newObjectQuery() {
return new PhabricatorRepositoryQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create Repository');
}
protected function getObjectCreateButtonText($object) {
return pht('Create Repository');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Repository: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return $object->getDisplayName();
}
protected function getObjectCreateShortText() {
return pht('Create Repository');
}
protected function getObjectName() {
return pht('Repository');
}
protected function getObjectViewURI($object) {
return $object->getPathURI('manage/');
}
protected function getCreateNewObjectPolicy() {
return $this->getApplication()->getPolicy(
DiffusionCreateRepositoriesCapability::CAPABILITY);
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($object)
->execute();
return array(
id(new PhabricatorSelectEditField())
->setKey('vcs')
->setLabel(pht('Version Control System'))
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_VCS)
->setIsConduitOnly(true)
->setIsCopyable(true)
->setOptions(PhabricatorRepositoryType::getAllRepositoryTypes())
->setDescription(pht('Underlying repository version control system.'))
->setConduitDescription(
pht(
'Choose which version control system to use when creating a '.
'repository.'))
->setConduitTypeDescription(pht('Version control system selection.'))
->setValue($object->getVersionControlSystem()),
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setIsRequired(true)
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_NAME)
->setDescription(pht('The repository name.'))
->setConduitDescription(pht('Rename the repository.'))
->setConduitTypeDescription(pht('New repository name.'))
->setValue($object->getName()),
id(new PhabricatorTextEditField())
->setKey('callsign')
->setLabel(pht('Callsign'))
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_CALLSIGN)
->setDescription(pht('The repository callsign.'))
->setConduitDescription(pht('Change the repository callsign.'))
->setConduitTypeDescription(pht('New repository callsign.'))
->setValue($object->getCallsign()),
id(new PhabricatorTextEditField())
->setKey('shortName')
->setLabel(pht('Short Name'))
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_SLUG)
->setDescription(pht('Short, unique repository name.'))
->setConduitDescription(pht('Change the repository short name.'))
->setConduitTypeDescription(pht('New short name for the repository.'))
->setValue($object->getRepositorySlug()),
id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DESCRIPTION)
->setDescription(pht('Repository description.'))
->setConduitDescription(pht('Change the repository description.'))
->setConduitTypeDescription(pht('New repository description.'))
->setValue($object->getDetail('description')),
id(new PhabricatorTextEditField())
->setKey('encoding')
->setLabel(pht('Text Encoding'))
->setIsCopyable(true)
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ENCODING)
->setDescription(pht('Default text encoding.'))
->setConduitDescription(pht('Change the default text encoding.'))
->setConduitTypeDescription(pht('New text encoding.'))
->setValue($object->getDetail('encoding')),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ACTIVATE)
->setIsConduitOnly(true)
->setOptions(PhabricatorRepository::getStatusNameMap())
->setDescription(pht('Active or inactive status.'))
->setConduitDescription(pht('Active or deactivate the repository.'))
->setConduitTypeDescription(pht('New repository status.'))
->setValue($object->getStatus()),
id(new PhabricatorTextEditField())
->setKey('defaultBranch')
->setLabel(pht('Default Branch'))
->setTransactionType(
PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH)
->setIsCopyable(true)
->setDescription(pht('Default branch name.'))
->setConduitDescription(pht('Set the default branch name.'))
->setConduitTypeDescription(pht('New default branch name.'))
->setValue($object->getDetail('default-branch')),
id(new PhabricatorPolicyEditField())
->setKey('policy.push')
->setLabel(pht('Push Policy'))
->setAliases(array('push'))
->setIsCopyable(true)
->setCapability(DiffusionPushCapability::CAPABILITY)
->setPolicies($policies)
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY)
->setDescription(
pht('Controls who can push changes to the repository.'))
->setConduitDescription(
pht('Change the push policy of the repository.'))
->setConduitTypeDescription(pht('New policy PHID or constant.'))
->setValue($object->getPolicy(DiffusionPushCapability::CAPABILITY)),
);
}
}

View file

@ -1058,8 +1058,16 @@ final class DiffusionCommitHookEngine extends Phobject {
// up. // up.
$phid = id(new PhabricatorRepositoryPushLog())->generatePHID(); $phid = id(new PhabricatorRepositoryPushLog())->generatePHID();
$device = AlmanacKeys::getLiveDevice();
if ($device) {
$device_phid = $device->getPHID();
} else {
$device_phid = null;
}
return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer()) return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer())
->setPHID($phid) ->setPHID($phid)
->setDevicePHID($device_phid)
->setRepositoryPHID($this->getRepository()->getPHID()) ->setRepositoryPHID($this->getRepository()->getPHID())
->attachRepository($this->getRepository()) ->attachRepository($this->getRepository())
->setEpoch(time()); ->setEpoch(time());

View file

@ -0,0 +1,135 @@
<?php
final class DiffusionRepositoryBasicsManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'basics';
public function getManagementPanelLabel() {
return pht('Basics');
}
public function getManagementPanelOrder() {
return 100;
}
protected function buildManagementPanelActions() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$edit_uri = $repository->getPathURI('manage/');
$activate_uri = $repository->getPathURI('edit/activate/');
$delete_uri = $repository->getPathURI('edit/delete/');
$encoding_uri = $repository->getPathURI('edit/encoding/');
if ($repository->isTracked()) {
$activate_icon = 'fa-pause';
$activate_label = pht('Deactivate Repository');
} else {
$activate_icon = 'fa-play';
$activate_label = pht('Activate Repository');
}
return array(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Basic Information'))
->setHref($edit_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit),
id(new PhabricatorActionView())
->setIcon('fa-text-width')
->setName(pht('Edit Text Encoding'))
->setHref($encoding_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit),
id(new PhabricatorActionView())
->setHref($activate_uri)
->setIcon($activate_icon)
->setName($activate_label)
->setDisabled(!$can_edit)
->setWorkflow(true),
id(new PhabricatorActionView())
->setName(pht('Delete Repository'))
->setIcon('fa-times')
->setHref($delete_uri)
->setDisabled(true)
->setWorkflow(true),
);
}
public function buildManagementPanelContent() {
$result = array();
$result[] = $this->newBox(pht('Repository Basics'), $this->buildBasics());
$description = $this->buildDescription();
if ($description) {
$result[] = $this->newBox(pht('Description'), $description);
}
return $result;
}
private function buildBasics() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer)
->setActionList($this->newActions());
$name = $repository->getName();
$view->addProperty(pht('Name'), $name);
$type = PhabricatorRepositoryType::getNameForRepositoryType(
$repository->getVersionControlSystem());
$view->addProperty(pht('Type'), $type);
$callsign = $repository->getCallsign();
if (!strlen($callsign)) {
$callsign = phutil_tag('em', array(), pht('No Callsign'));
}
$view->addProperty(pht('Callsign'), $callsign);
$short_name = $repository->getRepositorySlug();
if ($short_name === null) {
$short_name = $repository->getCloneName();
$short_name = phutil_tag('em', array(), $short_name);
}
$view->addProperty(pht('Short Name'), $short_name);
$encoding = $repository->getDetail('encoding');
if (!$encoding) {
$encoding = phutil_tag('em', array(), pht('Use Default (UTF-8)'));
}
$view->addProperty(pht('Encoding'), $encoding);
return $view;
}
private function buildDescription() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$description = $repository->getDetail('description');
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
if (!strlen($description)) {
$description = phutil_tag('em', array(), pht('No description provided.'));
} else {
$description = new PHUIRemarkupView($viewer, $description);
}
$view->addTextContent($description);
return $view;
}
}

View file

@ -10,7 +10,7 @@ final class DiffusionRepositoryClusterManagementPanel
} }
public function getManagementPanelOrder() { public function getManagementPanelOrder() {
return 12345; return 600;
} }
public function buildManagementPanelContent() { public function buildManagementPanelContent() {
@ -104,6 +104,29 @@ final class DiffusionRepositoryClusterManagementPanel
->setIcon('fa-pencil grey'); ->setIcon('fa-pencil grey');
} }
$write_properties = null;
if ($version) {
$write_properties = $version->getWriteProperties();
if ($write_properties) {
try {
$write_properties = phutil_json_decode($write_properties);
} catch (Exception $ex) {
$write_properties = null;
}
}
}
if ($write_properties) {
$writer_phid = idx($write_properties, 'userPHID');
$last_writer = $viewer->renderHandle($writer_phid);
$writer_epoch = idx($write_properties, 'epoch');
$writer_epoch = phabricator_datetime($writer_epoch, $viewer);
} else {
$last_writer = null;
$writer_epoch = null;
}
$rows[] = array( $rows[] = array(
$binding_icon, $binding_icon,
phutil_tag( phutil_tag(
@ -114,6 +137,8 @@ final class DiffusionRepositoryClusterManagementPanel
$device->getName()), $device->getName()),
$version_number, $version_number,
$is_writing, $is_writing,
$last_writer,
$writer_epoch,
); );
} }
} }
@ -126,6 +151,8 @@ final class DiffusionRepositoryClusterManagementPanel
pht('Device'), pht('Device'),
pht('Version'), pht('Version'),
pht('Writing'), pht('Writing'),
pht('Last Writer'),
pht('Last Write At'),
)) ))
->setColumnClasses( ->setColumnClasses(
array( array(
@ -133,6 +160,8 @@ final class DiffusionRepositoryClusterManagementPanel
null, null,
null, null,
'right wide', 'right wide',
null,
'date',
)); ));
$doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories'); $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories');
@ -160,6 +189,7 @@ final class DiffusionRepositoryClusterManagementPanel
return id(new PHUIObjectBoxView()) return id(new PHUIObjectBoxView())
->setHeader($header) ->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table); ->setTable($table);
} }

View file

@ -0,0 +1,21 @@
<?php
final class DiffusionRepositoryHistoryManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'history';
public function getManagementPanelLabel() {
return pht('History');
}
public function getManagementPanelOrder() {
return 900;
}
public function buildManagementPanelContent() {
return $this->newTimeline();
}
}

View file

@ -5,6 +5,7 @@ abstract class DiffusionRepositoryManagementPanel
private $viewer; private $viewer;
private $repository; private $repository;
private $controller;
final public function setViewer(PhabricatorUser $viewer) { final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer; $this->viewer = $viewer;
@ -24,6 +25,11 @@ abstract class DiffusionRepositoryManagementPanel
return $this->repository; return $this->repository;
} }
final public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
final public function getManagementPanelKey() { final public function getManagementPanelKey() {
return $this->getPhobjectClassConstant('PANELKEY'); return $this->getPhobjectClassConstant('PANELKEY');
} }
@ -32,6 +38,47 @@ abstract class DiffusionRepositoryManagementPanel
abstract public function getManagementPanelOrder(); abstract public function getManagementPanelOrder();
abstract public function buildManagementPanelContent(); abstract public function buildManagementPanelContent();
protected function buildManagementPanelActions() {
return array();
}
final protected function newActions() {
$actions = $this->buildManagementPanelActions();
if (!$actions) {
return null;
}
$viewer = $this->getViewer();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($actions as $action) {
$action_list->addAction($action);
}
return $action_list;
}
public function buildManagementPanelCurtain() {
// TODO: Delete or fix this, curtains always render in the left gutter
// at the moment.
return null;
$actions = $this->newActions();
if (!$actions) {
return null;
}
$viewer = $this->getViewer();
$curtain = id(new PHUICurtainView())
->setViewer($viewer)
->setActionList($actions);
return $curtain;
}
public static function getAllPanels() { public static function getAllPanels() {
return id(new PhutilClassMapQuery()) return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__) ->setAncestorClass(__CLASS__)
@ -40,4 +87,15 @@ abstract class DiffusionRepositoryManagementPanel
->execute(); ->execute();
} }
final protected function newBox($header_text, $body) {
return id(new PHUIObjectBoxView())
->setHeaderText($header_text)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($body);
}
final protected function newTimeline() {
return $this->controller->newTimeline($this->getRepository());
}
} }

View file

@ -0,0 +1,73 @@
<?php
final class DiffusionRepositoryPoliciesManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'policies';
public function getManagementPanelLabel() {
return pht('Policies');
}
public function getManagementPanelOrder() {
return 300;
}
protected function buildManagementPanelActions() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$edit_uri = $repository->getPathURI('manage/');
return array(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Policies'))
->setHref($edit_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());
$descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
$viewer,
$repository);
$view_parts = array();
if (PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
$repository);
$view_parts[] = $viewer->renderHandle($space_phid);
}
$view_parts[] = $descriptions[PhabricatorPolicyCapability::CAN_VIEW];
$view->addProperty(
pht('Visible To'),
phutil_implode_html(" \xC2\xB7 ", $view_parts));
$view->addProperty(
pht('Editable By'),
$descriptions[PhabricatorPolicyCapability::CAN_EDIT]);
$pushable = $repository->isHosted()
? $descriptions[DiffusionPushCapability::CAPABILITY]
: phutil_tag('em', array(), pht('Not a Hosted Repository'));
$view->addProperty(pht('Pushable By'), $pushable);
return $this->newBox(pht('Policies'), $view);
}
}

View file

@ -0,0 +1,457 @@
<?php
final class DiffusionRepositoryStatusManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'status';
public function getManagementPanelLabel() {
return pht('Status');
}
public function getManagementPanelOrder() {
return 200;
}
protected function buildManagementPanelActions() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$update_uri = $repository->getPathURI('edit/update/');
return array(
id(new PhabricatorActionView())
->setIcon('fa-refresh')
->setName(pht('Update Now'))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setHref($update_uri),
);
}
public function buildManagementPanelContent() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer)
->setActionList($this->newActions());
$view->addProperty(
pht('Update Frequency'),
$this->buildRepositoryUpdateInterval($repository));
list($status, $raw_error) = $this->buildRepositoryStatus($repository);
$view->addProperty(pht('Status'), $status);
if ($raw_error) {
$view->addSectionHeader(pht('Raw Error'));
$view->addTextContent($raw_error);
}
return $this->newBox(pht('Status'), $view);
}
private function buildRepositoryUpdateInterval(
PhabricatorRepository $repository) {
$smart_wait = $repository->loadUpdateInterval();
$doc_href = PhabricatorEnv::getDoclink(
'Diffusion User Guide: Repository Updates');
return array(
phutil_format_relative_time_detailed($smart_wait),
" \xC2\xB7 ",
phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Learn More')),
);
}
private function buildRepositoryStatus(
PhabricatorRepository $repository) {
$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())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Repository Active')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'bluegrey')
->setTarget(pht('Repository Inactive'))
->setNote(
pht('Activate this repository to begin or resume import.')));
return $view;
}
$binaries = array();
$svnlook_check = false;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svn';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
if ($repository->isHosted()) {
if ($repository->getServeOverHTTP() != PhabricatorRepository::SERVE_OFF) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git-http-backend';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svnserve';
$binaries[] = 'svnadmin';
$binaries[] = 'svnlook';
$svnlook_check = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
}
if ($repository->getServeOverSSH() != PhabricatorRepository::SERVE_OFF) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git-receive-pack';
$binaries[] = 'git-upload-pack';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svnserve';
$binaries[] = 'svnadmin';
$binaries[] = 'svnlook';
$svnlook_check = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
}
}
$binaries = array_unique($binaries);
if (!$is_cluster) {
// We're only checking for binaries if we aren't running with a cluster
// configuration. In theory, we could check for binaries on the
// repository host machine, but we'd need to make this more complicated
// to do that.
foreach ($binaries as $binary) {
$where = Filesystem::resolveBinary($binary);
if (!$where) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(
pht('Missing Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(pht(
"Unable to find this binary in the webserver's PATH. You may ".
"need to configure %s.",
$this->getEnvConfigLink())));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(
pht('Found Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(phutil_tag('tt', array(), $where)));
}
}
// This gets checked generically above. However, for svn commit hooks, we
// need this to be in environment.append-paths because subversion strips
// PATH.
if ($svnlook_check) {
$where = Filesystem::resolveBinary('svnlook');
if ($where) {
$path = substr($where, 0, strlen($where) - strlen('svnlook'));
$dirs = PhabricatorEnv::getEnvConfig('environment.append-paths');
$in_path = false;
foreach ($dirs as $dir) {
if (Filesystem::isDescendant($path, $dir)) {
$in_path = true;
break;
}
}
if (!$in_path) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(
pht('Missing Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(pht(
'Unable to find this binary in `%s`. '.
'You need to configure %s and include %s.',
'environment.append-paths',
$this->getEnvConfigLink(),
$path)));
}
}
}
}
$doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd');
$daemon_instructions = pht(
'Use %s to start daemons. See %s.',
phutil_tag('tt', array(), 'bin/phd start'),
phutil_tag(
'a',
array(
'href' => $doc_href,
),
pht('Managing Daemons with phd')));
$pull_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->withDaemonClasses(array('PhabricatorRepositoryPullLocalDaemon'))
->setLimit(1)
->execute();
if ($pull_daemon) {
// TODO: In a cluster environment, we need a daemon on this repository's
// host, specifically, and we aren't checking for that right now. This
// is a reasonable proxy for things being more-or-less correctly set up,
// though.
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Pull Daemon Running')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Pull Daemon Not Running'))
->setNote($daemon_instructions));
}
$task_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->withDaemonClasses(array('PhabricatorTaskmasterDaemon'))
->setLimit(1)
->execute();
if ($task_daemon) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Task Daemon Running')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Task Daemon Not Running'))
->setNote($daemon_instructions));
}
if ($is_cluster) {
// Just omit this status check for now in cluster environments. We
// could make a service call and pull it from the repository host
// eventually.
} else if ($repository->usesLocalWorkingCopy()) {
$local_parent = dirname($repository->getLocalPath());
if (Filesystem::pathExists($local_parent)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Storage Directory OK'))
->setNote(phutil_tag('tt', array(), $local_parent)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('No Storage Directory'))
->setNote(
pht(
'Storage directory %s does not exist, or is not readable by '.
'the webserver. Create this directory or make it readable.',
phutil_tag('tt', array(), $local_parent))));
return $view;
}
$local_path = $repository->getLocalPath();
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_INIT);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Initialization Error'))
->setNote($message->getParameter('message')));
return $view;
case PhabricatorRepositoryStatusMessage::CODE_OKAY:
if (Filesystem::pathExists($local_path)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Working Copy OK'))
->setNote(phutil_tag('tt', array(), $local_path)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Working Copy Error'))
->setNote(
pht(
'Working copy %s has been deleted, or is not '.
'readable by the webserver. Make this directory '.
'readable. If it has been deleted, the daemons should '.
'restore it automatically.',
phutil_tag('tt', array(), $local_path))));
return $view;
}
break;
case PhabricatorRepositoryStatusMessage::CODE_WORKING:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green')
->setTarget(pht('Initializing Working Copy'))
->setNote(pht('Daemons are initializing the working copy.')));
return $view;
default:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Unknown Init Status'))
->setNote($message->getStatusCode()));
return $view;
}
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange')
->setTarget(pht('No Working Copy Yet'))
->setNote(
pht('Waiting for daemons to build a working copy.')));
return $view;
}
}
$raw_error = null;
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$message = $message->getParameter('message');
$suggestion = null;
if (preg_match('/Permission denied \(publickey\)./', $message)) {
$suggestion = pht(
'Public Key Error: This error usually indicates that the '.
'keypair you have configured does not have permission to '.
'access the repository.');
}
$raw_error = $message;
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Update Error'))
->setNote($suggestion));
return $view;
case PhabricatorRepositoryStatusMessage::CODE_OKAY:
$ago = (PhabricatorTime::getNow() - $message->getEpoch());
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Updates OK'))
->setNote(
pht(
'Last updated %s (%s ago).',
phabricator_datetime($message->getEpoch(), $viewer),
phutil_format_relative_time_detailed($ago))));
break;
}
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange')
->setTarget(pht('Waiting For Update'))
->setNote(
pht('Waiting for daemons to read updates.')));
}
if ($repository->isImporting()) {
$ratio = $repository->loadImportProgress();
$percentage = sprintf('%.2f%%', 100 * $ratio);
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green')
->setTarget(pht('Importing'))
->setNote(
pht('%s Complete', $percentage)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Fully Imported')));
}
if (idx($messages, PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_UP, 'indigo')
->setTarget(pht('Prioritized'))
->setNote(pht('This repository will be updated soon!')));
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
if ($raw_error !== null) {
if (!$can_edit) {
$raw_message = pht(
'You must be able to edit a repository to see raw error messages '.
'because they sometimes disclose sensitive information.');
$raw_message = phutil_tag('em', array(), $raw_message);
} else {
$raw_message = phutil_escape_html_newlines($raw_error);
}
} else {
$raw_message = null;
}
return array($view, $raw_message);
}
}

View file

@ -0,0 +1,126 @@
<?php
final class DiffusionRepositoryURIsManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'uris';
public function getManagementPanelLabel() {
return pht('Clone / Fetch / Mirror');
}
public function getManagementPanelOrder() {
return 300;
}
public function buildManagementPanelContent() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$repository->attachURIs(array());
$uris = $repository->getURIs();
Javelin::initBehavior('phabricator-tooltips');
$rows = array();
foreach ($uris as $uri) {
$uri_name = $uri->getDisplayURI();
if ($uri->getIsDisabled()) {
$status_icon = 'fa-times grey';
} else {
$status_icon = 'fa-check green';
}
$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;
}
$uri_io = array(
id(new PHUIIconView())->setIcon($io_icon),
' ',
$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;
}
$uri_display = array(
id(new PHUIIconView())->setIcon($display_icon),
' ',
$display_label,
);
$rows[] = array(
$uri_status,
$uri_name,
$uri_io,
$uri_display,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('This repository has no URIs.'))
->setHeaders(
array(
null,
pht('URI'),
pht('I/O'),
pht('Display'),
))
->setColumnClasses(
array(
null,
'pri wide',
null,
null,
));
$doc_href = PhabricatorEnv::getDoclink(
'Diffusion User Guide: Repository URIs');
$header = id(new PHUIHeaderView())
->setHeader(pht('Repository URIs'))
->addActionLink(
id(new PHUIButtonView())
->setIcon('fa-book')
->setHref($doc_href)
->setTag('a')
->setText(pht('Documentation')));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
}
}

View file

@ -0,0 +1,233 @@
<?php
abstract class DiffusionCommandEngine extends Phobject {
private $repository;
private $protocol;
private $credentialPHID;
private $argv;
private $passthru;
private $connectAsDevice;
private $sudoAsDaemon;
public static function newCommandEngine(PhabricatorRepository $repository) {
$engines = self::newCommandEngines();
foreach ($engines as $engine) {
if ($engine->canBuildForRepository($repository)) {
return id(clone $engine)
->setRepository($repository);
}
}
throw new Exception(
pht(
'No registered command engine can build commands for this '.
'repository ("%s").',
$repository->getDisplayName()));
}
private static function newCommandEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
abstract protected function canBuildForRepository(
PhabricatorRepository $repository);
abstract protected function newFormattedCommand($pattern, array $argv);
abstract protected function newCustomEnvironment();
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->repository;
}
public function setProtocol($protocol) {
$this->protocol = $protocol;
return $this;
}
public function getProtocol() {
return $this->protocol;
}
public function setCredentialPHID($credential_phid) {
$this->credentialPHID = $credential_phid;
return $this;
}
public function getCredentialPHID() {
return $this->credentialPHID;
}
public function setArgv(array $argv) {
$this->argv = $argv;
return $this;
}
public function getArgv() {
return $this->argv;
}
public function setPassthru($passthru) {
$this->passthru = $passthru;
return $this;
}
public function getPassthru() {
return $this->passthru;
}
public function setConnectAsDevice($connect_as_device) {
$this->connectAsDevice = $connect_as_device;
return $this;
}
public function getConnectAsDevice() {
return $this->connectAsDevice;
}
public function setSudoAsDaemon($sudo_as_daemon) {
$this->sudoAsDaemon = $sudo_as_daemon;
return $this;
}
public function getSudoAsDaemon() {
return $this->sudoAsDaemon;
}
public function newFuture() {
$argv = $this->newCommandArgv();
$env = $this->newCommandEnvironment();
if ($this->getSudoAsDaemon()) {
$command = call_user_func_array('csprintf', $argv);
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$argv = array('%C', $command);
}
if ($this->getPassthru()) {
$future = newv('PhutilExecPassthru', $argv);
} else {
$future = newv('ExecFuture', $argv);
}
$future->setEnv($env);
return $future;
}
private function newCommandArgv() {
$argv = $this->argv;
$pattern = $argv[0];
$argv = array_slice($argv, 1);
list($pattern, $argv) = $this->newFormattedCommand($pattern, $argv);
return array_merge(array($pattern), $argv);
}
private function newCommandEnvironment() {
$env = $this->newCommonEnvironment() + $this->newCustomEnvironment();
foreach ($env as $key => $value) {
if ($value === null) {
unset($env[$key]);
}
}
return $env;
}
private function newCommonEnvironment() {
$repository = $this->getRepository();
$env = array();
// NOTE: Force the language to "en_US.UTF-8", which overrides locale
// settings. This makes stuff print in English instead of, e.g., French,
// so we can parse the output of some commands, error messages, etc.
$env['LANG'] = 'en_US.UTF-8';
// Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155.
$env['PHABRICATOR_ENV'] = PhabricatorEnv::getSelectedEnvironmentName();
$as_device = $this->getConnectAsDevice();
$credential_phid = $this->getCredentialPHID();
if ($as_device) {
$device = AlmanacKeys::getLiveDevice();
if (!$device) {
throw new Exception(
pht(
'Attempting to build a reposiory command (for repository "%s") '.
'as device, but this host ("%s") is not configured as a cluster '.
'device.',
$repository->getDisplayName(),
php_uname('n')));
}
if ($credential_phid) {
throw new Exception(
pht(
'Attempting to build a repository command (for repository "%s"), '.
'but the CommandEngine is configured to connect as both the '.
'current cluster device ("%s") and with a specific credential '.
'("%s"). These options are mutually exclusive. Connections must '.
'authenticate as one or the other, not both.',
$repository->getDisplayName(),
$device->getName(),
$credential_phid));
}
}
if ($this->isAnySSHProtocol()) {
if ($credential_phid) {
$env['PHABRICATOR_CREDENTIAL'] = $credential_phid;
}
if ($as_device) {
$env['PHABRICATOR_AS_DEVICE'] = 1;
}
}
return $env;
}
protected function isSSHProtocol() {
return ($this->getProtocol() == 'ssh');
}
protected function isSVNProtocol() {
return ($this->getProtocol() == 'svn');
}
protected function isSVNSSHProtocol() {
return ($this->getProtocol() == 'svn+ssh');
}
protected function isHTTPProtocol() {
return ($this->getProtocol() == 'http');
}
protected function isHTTPSProtocol() {
return ($this->getProtocol() == 'https');
}
protected function isAnyHTTPProtocol() {
return ($this->isHTTPProtocol() || $this->isHTTPSProtocol());
}
protected function isAnySSHProtocol() {
return ($this->isSSHProtocol() || $this->isSVNSSHProtocol());
}
protected function getSSHWrapper() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/bin/ssh-connect';
}
}

View file

@ -0,0 +1,37 @@
<?php
final class DiffusionGitCommandEngine
extends DiffusionCommandEngine {
protected function canBuildForRepository(
PhabricatorRepository $repository) {
return $repository->isGit();
}
protected function newFormattedCommand($pattern, array $argv) {
$pattern = "git {$pattern}";
return array($pattern, $argv);
}
protected function newCustomEnvironment() {
$env = array();
// NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if
// it can not read $HOME. For many users, $HOME points at /root (this
// seems to be a default result of Apache setup). Instead, explicitly
// point $HOME at a readable, empty directory so that Git looks for the
// config file it's after, fails to locate it, and moves on. This is
// really silly, but seems like the least damaging approach to
// mitigating the issue.
$root = dirname(phutil_get_library_root('phabricator'));
$env['HOME'] = $root.'/support/empty/';
if ($this->isAnySSHProtocol()) {
$env['GIT_SSH'] = $this->getSSHWrapper();
}
return $env;
}
}

View file

@ -0,0 +1,70 @@
<?php
final class DiffusionMercurialCommandEngine
extends DiffusionCommandEngine {
protected function canBuildForRepository(
PhabricatorRepository $repository) {
return $repository->isHg();
}
protected function newFormattedCommand($pattern, array $argv) {
$args = array();
if ($this->isAnySSHProtocol()) {
$pattern = "hg --config ui.ssh=%s {$pattern}";
$args[] = $this->getSSHWrapper();
} else {
$pattern = "hg {$pattern}";
}
return array($pattern, array_merge($args, $argv));
}
protected function newCustomEnvironment() {
$env = array();
// NOTE: This overrides certain configuration, extensions, and settings
// which make Mercurial commands do random unusual things.
$env['HGPLAIN'] = 1;
return $env;
}
/**
* Sanitize output of an `hg` command invoked with the `--debug` flag to make
* it usable.
*
* @param string Output from `hg --debug ...`
* @return string Usable output.
*/
public static function filterMercurialDebugOutput($stdout) {
// When hg commands are run with `--debug` and some config file isn't
// trusted, Mercurial prints out a warning to stdout, twice, after Feb 2011.
//
// http://selenic.com/pipermail/mercurial-devel/2011-February/028541.html
//
// After Jan 2015, it may also fail to write to a revision branch cache.
$ignore = array(
'ignoring untrusted configuration option',
"couldn't write revision branch cache:",
);
foreach ($ignore as $key => $pattern) {
$ignore[$key] = preg_quote($pattern, '/');
}
$ignore = '('.implode('|', $ignore).')';
$lines = preg_split('/(?<=\n)/', $stdout);
$regex = '/'.$ignore.'.*\n$/';
foreach ($lines as $key => $line) {
$lines[$key] = preg_replace($regex, '', $line);
}
return implode('', $lines);
}
}

View file

@ -0,0 +1,54 @@
<?php
final class DiffusionSubversionCommandEngine
extends DiffusionCommandEngine {
protected function canBuildForRepository(
PhabricatorRepository $repository) {
return $repository->isSVN();
}
protected function newFormattedCommand($pattern, array $argv) {
$flags = array();
$args = array();
$flags[] = '--non-interactive';
if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) {
$flags[] = '--no-auth-cache';
if ($this->isAnyHTTPProtocol()) {
$flags[] = '--trust-server-cert';
}
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$flags[] = '--username %P';
$args[] = $key->getUsernameEnvelope();
$flags[] = '--password %P';
$args[] = $key->getPasswordEnvelope();
}
}
$flags = implode(' ', $flags);
$pattern = "svn {$flags} {$pattern}";
return array($pattern, array_merge($args, $argv));
}
protected function newCustomEnvironment() {
$env = array();
if ($this->isAnySSHProtocol()) {
$env['SVN_SSH'] = $this->getSSHWrapper();
}
return $env;
}
}

View file

@ -0,0 +1,155 @@
<?php
final class DiffusionCommandEngineTestCase extends PhabricatorTestCase {
public function testCommandEngine() {
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
$type_svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
$root = dirname(phutil_get_library_root('phabricator'));
$ssh_wrapper = $root.'/bin/ssh-connect';
$home = $root.'/support/empty/';
// Plain commands.
$this->assertCommandEngineFormat(
'git xyz',
array(
'LANG' => 'en_US.UTF-8',
'HOME' => $home,
),
array(
'vcs' => $type_git,
'argv' => 'xyz',
));
$this->assertCommandEngineFormat(
'hg xyz',
array(
'LANG' => 'en_US.UTF-8',
'HGPLAIN' => '1',
),
array(
'vcs' => $type_hg,
'argv' => 'xyz',
));
$this->assertCommandEngineFormat(
'svn --non-interactive xyz',
array(
'LANG' => 'en_US.UTF-8',
),
array(
'vcs' => $type_svn,
'argv' => 'xyz',
));
// Commands with SSH.
$this->assertCommandEngineFormat(
'git xyz',
array(
'LANG' => 'en_US.UTF-8',
'HOME' => $home,
'GIT_SSH' => $ssh_wrapper,
),
array(
'vcs' => $type_git,
'argv' => 'xyz',
'protocol' => 'ssh',
));
$this->assertCommandEngineFormat(
(string)csprintf('hg --config ui.ssh=%s xyz', $ssh_wrapper),
array(
'LANG' => 'en_US.UTF-8',
'HGPLAIN' => '1',
),
array(
'vcs' => $type_hg,
'argv' => 'xyz',
'protocol' => 'ssh',
));
$this->assertCommandEngineFormat(
'svn --non-interactive xyz',
array(
'LANG' => 'en_US.UTF-8',
'SVN_SSH' => $ssh_wrapper,
),
array(
'vcs' => $type_svn,
'argv' => 'xyz',
'protocol' => 'ssh',
));
// Commands with HTTP.
$this->assertCommandEngineFormat(
'git xyz',
array(
'LANG' => 'en_US.UTF-8',
'HOME' => $home,
),
array(
'vcs' => $type_git,
'argv' => 'xyz',
'protocol' => 'https',
));
$this->assertCommandEngineFormat(
'hg xyz',
array(
'LANG' => 'en_US.UTF-8',
'HGPLAIN' => '1',
),
array(
'vcs' => $type_hg,
'argv' => 'xyz',
'protocol' => 'https',
));
$this->assertCommandEngineFormat(
'svn --non-interactive --no-auth-cache --trust-server-cert xyz',
array(
'LANG' => 'en_US.UTF-8',
),
array(
'vcs' => $type_svn,
'argv' => 'xyz',
'protocol' => 'https',
));
}
private function assertCommandEngineFormat(
$command,
array $env,
array $inputs) {
$repository = id(new PhabricatorRepository())
->setVersionControlSystem($inputs['vcs']);
$future = DiffusionCommandEngine::newCommandEngine($repository)
->setArgv((array)$inputs['argv'])
->setProtocol(idx($inputs, 'protocol'))
->newFuture();
$command_string = $future->getCommand();
$actual_command = $command_string->getUnmaskedString();
$this->assertEqual($command, $actual_command);
$actual_environment = $future->getEnv();
$compare_environment = array_select_keys(
$actual_environment,
array_keys($env));
$this->assertEqual($env, $compare_environment);
}
}

View file

@ -50,7 +50,9 @@ final class DiffusionLowLevelParentsQuery
list($stdout) = $repository->execxLocalCommand( list($stdout) = $repository->execxLocalCommand(
'log --debug --limit 1 --template={parents} --rev %s', 'log --debug --limit 1 --template={parents} --rev %s',
$this->identifier); $this->identifier);
$stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout);
$stdout = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
$stdout);
$hashes = preg_split('/\s+/', trim($stdout)); $hashes = preg_split('/\s+/', trim($stdout));
foreach ($hashes as $key => $value) { foreach ($hashes as $key => $value) {

View file

@ -21,22 +21,31 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
if ($this->shouldProxy()) { if ($this->shouldProxy()) {
$command = $this->getProxyCommand(); $command = $this->getProxyCommand();
$is_proxy = true; $did_synchronize = false;
} else { } else {
$command = csprintf('git-receive-pack %s', $repository->getLocalPath()); $command = csprintf('git-receive-pack %s', $repository->getLocalPath());
$is_proxy = false;
$repository->synchronizeWorkingCopyBeforeWrite(); $did_synchronize = true;
$viewer = $this->getUser();
$repository->synchronizeWorkingCopyBeforeWrite($viewer);
} }
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = id(new ExecFuture('%C', $command)) $caught = null;
->setEnv($this->getEnvironment()); try {
$err = $this->executeRepositoryCommand($command);
} catch (Exception $ex) {
$caught = $ex;
}
$err = $this->newPassthruCommand() // We've committed the write (or rejected it), so we can release the lock
->setIOChannel($this->getIOChannel()) // without waiting for the client to receive the acknowledgement.
->setCommandChannelFromExecFuture($future) if ($did_synchronize) {
->execute(); $repository->synchronizeWorkingCopyAfterWrite();
}
if ($caught) {
throw $caught;
}
if (!$err) { if (!$err) {
$repository->writeStatusMessage( $repository->writeStatusMessage(
@ -45,11 +54,20 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
$this->waitForGitClient(); $this->waitForGitClient();
} }
if (!$is_proxy) {
$repository->synchronizeWorkingCopyAfterWrite();
}
return $err; return $err;
} }
private function executeRepositoryCommand($command) {
$repository = $this->getRepository();
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = id(new ExecFuture('%C', $command))
->setEnv($this->getEnvironment());
return $this->newPassthruCommand()
->setIOChannel($this->getIOChannel())
->setCommandChannelFromExecFuture($future)
->execute();
}
} }

View file

@ -16,12 +16,16 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
protected function executeRepositoryOperations() { protected function executeRepositoryOperations() {
$repository = $this->getRepository(); $repository = $this->getRepository();
$skip_sync = $this->shouldSkipReadSynchronization();
if ($this->shouldProxy()) { if ($this->shouldProxy()) {
$command = $this->getProxyCommand(); $command = $this->getProxyCommand();
} else { } else {
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath()); $command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
if (!$skip_sync) {
$repository->synchronizeWorkingCopyBeforeRead(); $repository->synchronizeWorkingCopyBeforeRead();
} }
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = id(new ExecFuture('%C', $command)) $future = id(new ExecFuture('%C', $command))

View file

@ -62,16 +62,13 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
protected function getProxyCommand() { protected function getProxyCommand() {
$uri = new PhutilURI($this->proxyURI); $uri = new PhutilURI($this->proxyURI);
$username = PhabricatorEnv::getEnvConfig('cluster.instance'); $username = AlmanacKeys::getClusterSSHUser();
if (!strlen($username)) { if ($username === null) {
$username = PhabricatorEnv::getEnvConfig('diffusion.ssh-user');
if (!strlen($username)) {
throw new Exception( throw new Exception(
pht( pht(
'Unable to determine the username to connect with when trying '. 'Unable to determine the username to connect with when trying '.
'to proxy an SSH request within the Phabricator cluster.')); 'to proxy an SSH request within the Phabricator cluster.'));
} }
}
$port = $uri->getPort(); $port = $uri->getPort();
$host = $uri->getDomain(); $host = $uri->getDomain();
@ -204,6 +201,14 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
$repository = $this->getRepository(); $repository = $this->getRepository();
$viewer = $this->getUser(); $viewer = $this->getUser();
if ($viewer->isOmnipotent()) {
throw new Exception(
pht(
'This request is authenticated as a cluster device, but is '.
'performing a write. Writes must be performed with a real '.
'user account.'));
}
switch ($repository->getServeOverSSH()) { switch ($repository->getServeOverSSH()) {
case PhabricatorRepository::SERVE_READONLY: case PhabricatorRepository::SERVE_READONLY:
if ($protocol_command !== null) { if ($protocol_command !== null) {
@ -239,4 +244,18 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
return $this->hasWriteAccess; return $this->hasWriteAccess;
} }
protected function shouldSkipReadSynchronization() {
$viewer = $this->getUser();
// Currently, the only case where devices interact over SSH without
// assuming user credentials is when synchronizing before a read. These
// synchronizing reads do not themselves need to be synchronized.
if ($viewer->isOmnipotent()) {
return true;
}
return false;
}
} }

View file

@ -3,7 +3,6 @@
final class DiffusionPushLogListView extends AphrontView { final class DiffusionPushLogListView extends AphrontView {
private $logs; private $logs;
private $handles;
public function setLogs(array $logs) { public function setLogs(array $logs) {
assert_instances_of($logs, 'PhabricatorRepositoryPushLog'); assert_instances_of($logs, 'PhabricatorRepositoryPushLog');
@ -11,15 +10,20 @@ final class DiffusionPushLogListView extends AphrontView {
return $this; return $this;
} }
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function render() { public function render() {
$logs = $this->logs; $logs = $this->logs;
$viewer = $this->getUser(); $viewer = $this->getViewer();
$handles = $this->handles;
$handle_phids = array();
foreach ($logs as $log) {
$handle_phids[] = $log->getPusherPHID();
$device_phid = $log->getDevicePHID();
if ($device_phid) {
$handle_phids[] = $device_phid;
}
}
$handles = $viewer->loadHandles($handle_phids);
// Figure out which repositories are editable. We only let you see remote // Figure out which repositories are editable. We only let you see remote
// IPs if you have edit capability on a repository. // IPs if you have edit capability on a repository.
@ -38,6 +42,7 @@ final class DiffusionPushLogListView extends AphrontView {
} }
$rows = array(); $rows = array();
$any_host = false;
foreach ($logs as $log) { foreach ($logs as $log) {
$repository = $log->getRepository(); $repository = $log->getRepository();
@ -59,6 +64,14 @@ final class DiffusionPushLogListView extends AphrontView {
$log->getRefOldShort()); $log->getRefOldShort());
} }
$device_phid = $log->getDevicePHID();
if ($device_phid) {
$device = $viewer->renderHandle($device_phid);
$any_host = true;
} else {
$device = null;
}
$rows[] = array( $rows[] = array(
phutil_tag( phutil_tag(
'a', 'a',
@ -72,9 +85,10 @@ final class DiffusionPushLogListView extends AphrontView {
'href' => $repository->getURI(), 'href' => $repository->getURI(),
), ),
$repository->getDisplayName()), $repository->getDisplayName()),
$handles[$log->getPusherPHID()]->renderLink(), $viewer->renderHandle($log->getPusherPHID()),
$remote_address, $remote_address,
$log->getPushEvent()->getRemoteProtocol(), $log->getPushEvent()->getRemoteProtocol(),
$device,
$log->getRefType(), $log->getRefType(),
$log->getRefName(), $log->getRefName(),
$old_ref_link, $old_ref_link,
@ -100,6 +114,7 @@ final class DiffusionPushLogListView extends AphrontView {
pht('Pusher'), pht('Pusher'),
pht('From'), pht('From'),
pht('Via'), pht('Via'),
pht('Host'),
pht('Type'), pht('Type'),
pht('Name'), pht('Name'),
pht('Old'), pht('Old'),
@ -116,10 +131,20 @@ final class DiffusionPushLogListView extends AphrontView {
'', '',
'', '',
'', '',
'',
'wide', 'wide',
'n', 'n',
'n', 'n',
'right', 'right',
))
->setColumnVisibility(
array(
true,
true,
true,
true,
true,
$any_host,
)); ));
return $table; return $table;

View file

@ -75,7 +75,7 @@ final class DivinerBookEditController extends DivinerController {
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectDatasource()) ->setDatasource(new PhabricatorProjectDatasource())
->setName('projectPHIDs') ->setName('projectPHIDs')
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setValue($book->getProjectPHIDs())) ->setValue($book->getProjectPHIDs()))
->appendControl( ->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())

View file

@ -102,7 +102,7 @@ final class PhabricatorFeedSearchEngine
); );
if ($this->requireViewer()->isLoggedIn()) { if ($this->requireViewer()->isLoggedIn()) {
$names['projects'] = pht('Projects'); $names['projects'] = pht('Tags');
} }
return $names; return $names;

View file

@ -22,25 +22,21 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
$req_domain = $request->getHost(); $req_domain = $request->getHost();
$main_domain = id(new PhutilURI($base_uri))->getDomain(); $main_domain = id(new PhutilURI($base_uri))->getDomain();
if (!strlen($alt) || $main_domain == $alt_domain) { if (!strlen($alt) || $main_domain == $alt_domain) {
// No alternate domain. // No alternate domain.
$should_redirect = false; $should_redirect = false;
$use_viewer = $viewer;
$is_alternate_domain = false; $is_alternate_domain = false;
} else if ($req_domain != $alt_domain) { } else if ($req_domain != $alt_domain) {
// Alternate domain, but this request is on the main domain. // Alternate domain, but this request is on the main domain.
$should_redirect = true; $should_redirect = true;
$use_viewer = $viewer;
$is_alternate_domain = false; $is_alternate_domain = false;
} else { } else {
// Alternate domain, and on the alternate domain. // Alternate domain, and on the alternate domain.
$should_redirect = false; $should_redirect = false;
$use_viewer = PhabricatorUser::getOmnipotentUser();
$is_alternate_domain = true; $is_alternate_domain = true;
} }
$response = $this->loadFile($use_viewer); $response = $this->loadFile();
if ($response) { if ($response) {
return $response; return $response;
} }
@ -112,7 +108,21 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
return $response; return $response;
} }
private function loadFile(PhabricatorUser $viewer) { private function loadFile() {
// Access to files is provided by knowledge of a per-file secret key in
// the URI. Knowledge of this secret is sufficient to retrieve the file.
// For some requests, we also have a valid viewer. However, for many
// requests (like alternate domain requests or Git LFS requests) we will
// not. Even if we do have a valid viewer, use the omnipotent viewer to
// make this logic simpler and more consistent.
// Beyond making the policy check itself more consistent, this also makes
// sure we're consitent about returning HTTP 404 on bad requests instead
// of serving HTTP 200 with a login page, which can mislead some clients.
$viewer = PhabricatorUser::getOmnipotentUser();
$file = id(new PhabricatorFileQuery()) $file = id(new PhabricatorFileQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs(array($this->phid)) ->withPHIDs(array($this->phid))

View file

@ -201,7 +201,7 @@ final class FundInitiativeEditController
->setValue($v_risk)) ->setValue($v_risk))
->appendControl( ->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setValue($v_projects) ->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource())) ->setDatasource(new PhabricatorProjectDatasource()))

View file

@ -58,7 +58,7 @@ final class ManiphestExcelDefaultFormat extends ManiphestExcelFormat {
pht('Date Created'), pht('Date Created'),
pht('Date Updated'), pht('Date Updated'),
pht('Title'), pht('Title'),
pht('Projects'), pht('Tags'),
pht('URI'), pht('URI'),
pht('Description'), pht('Description'),
); );

View file

@ -37,8 +37,10 @@ final class PhabricatorApplicationApplicationPHIDType
foreach ($handles as $phid => $handle) { foreach ($handles as $phid => $handle) {
$application = $objects[$phid]; $application = $objects[$phid];
$handle->setName($application->getName()); $handle
$handle->setURI($application->getApplicationURI()); ->setName($application->getName())
->setURI($application->getApplicationURI())
->setIcon($application->getIcon());
} }
} }

View file

@ -6,12 +6,17 @@ final class OwnersQueryConduitAPIMethod extends OwnersConduitAPIMethod {
return 'owners.query'; return 'owners.query';
} }
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return pht('Obsolete; use "owners.search" instead.');
}
public function getMethodDescription() { public function getMethodDescription() {
return pht( return pht('Query for Owners packages. Obsoleted by "owners.search".');
'Query for packages by one of the following: repository/path, '.
'packages with a given user or project owner, or packages affiliated '.
'with a user (owned by either the user or a project they are a member '.
'of.) You should only provide at most one search query.');
} }
protected function defineParamTypes() { protected function defineParamTypes() {

View file

@ -316,7 +316,7 @@ final class PholioMockEditController extends PholioController {
->setUser($viewer)) ->setUser($viewer))
->appendControl( ->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setValue($v_projects) ->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource())) ->setDatasource(new PhabricatorProjectDatasource()))

View file

@ -59,7 +59,7 @@ final class PhabricatorPhrictionApplication extends PhabricatorApplication {
'new/' => 'PhrictionNewController', 'new/' => 'PhrictionNewController',
'move/(?P<id>[1-9]\d*)/' => 'PhrictionMoveController', 'move/(?P<id>[1-9]\d*)/' => 'PhrictionMoveController',
'preview/' => 'PhabricatorMarkupPreviewController', 'preview/(?P<slug>.+/)' => 'PhrictionMarkupPreviewController',
'diff/(?P<id>[1-9]\d*)/' => 'PhrictionDiffController', 'diff/(?P<id>[1-9]\d*)/' => 'PhrictionDiffController',
), ),
); );

View file

@ -272,7 +272,7 @@ final class PhrictionEditController
$preview = id(new PHUIRemarkupPreviewPanel()) $preview = id(new PHUIRemarkupPreviewPanel())
->setHeader($content->getTitle()) ->setHeader($content->getTitle())
->setPreviewURI('/phriction/preview/') ->setPreviewURI('/phriction/preview/'.$document->getSlug())
->setControlID('document-textarea') ->setControlID('document-textarea')
->setPreviewType(PHUIRemarkupPreviewPanel::DOCUMENT); ->setPreviewType(PHUIRemarkupPreviewPanel::DOCUMENT);

View file

@ -0,0 +1,28 @@
<?php
final class PhrictionMarkupPreviewController
extends PhabricatorController {
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$text = $request->getStr('text');
$slug = $request->getURIData('slug');
$output = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())
->setPreserveLinebreaks(true)
->setDisableCache(true)
->setContent($text),
'default',
$viewer,
array(
'phriction.isPreview' => true,
'phriction.slug' => $slug,
));
return id(new AphrontAjaxResponse())
->setContent($output);
}
}

View file

@ -2,6 +2,8 @@
final class PhrictionRemarkupRule extends PhutilRemarkupRule { final class PhrictionRemarkupRule extends PhutilRemarkupRule {
const KEY_RULE_PHRICTION_LINK = 'phriction.link';
public function getPriority() { public function getPriority() {
return 175.0; return 175.0;
} }
@ -15,11 +17,128 @@ final class PhrictionRemarkupRule extends PhutilRemarkupRule {
public function markupDocumentLink(array $matches) { public function markupDocumentLink(array $matches) {
$link = trim($matches[1]); $link = trim($matches[1]);
$name = trim(idx($matches, 2, $link));
// Handle relative links.
if ((substr($link, 0, 2) === './') || (substr($link, 0, 3) === '../')) {
$base = null;
$context = $this->getEngine()->getConfig('contextObject');
if ($context !== null && $context instanceof PhrictionContent) {
// Handle content when it's being rendered in document view.
$base = $context->getSlug();
}
if ($context !== null && is_array($context) &&
idx($context, 'phriction.isPreview')) {
// Handle content when it's a preview for the Phriction editor.
$base = idx($context, 'phriction.slug');
}
if ($base !== null) {
$base_parts = explode('/', rtrim($base, '/'));
$rel_parts = explode('/', rtrim($link, '/'));
foreach ($rel_parts as $part) {
if ($part === '.') {
// Consume standalone dots in a relative path, and do
// nothing with them.
} else if ($part === '..') {
if (count($base_parts) > 0) {
array_pop($base_parts);
}
} else {
array_push($base_parts, $part);
}
}
$link = implode('/', $base_parts).'/';
}
}
$name = trim(idx($matches, 2, ''));
if (empty($matches[2])) { if (empty($matches[2])) {
$name = explode('/', trim($name, '/')); $name = null;
}
// Link is now used for slug detection, so append a slash if one
// is needed.
$link = rtrim($link, '/').'/';
$engine = $this->getEngine();
$token = $engine->storeText('x');
$metadata = $engine->getTextMetadata(
self::KEY_RULE_PHRICTION_LINK,
array());
$metadata[] = array(
'token' => $token,
'link' => $link,
'explicitName' => $name,
);
$engine->setTextMetadata(self::KEY_RULE_PHRICTION_LINK, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata = $engine->getTextMetadata(
self::KEY_RULE_PHRICTION_LINK,
array());
if (!$metadata) {
return;
}
$slugs = ipull($metadata, 'link');
foreach ($slugs as $key => $slug) {
$slugs[$key] = PhabricatorSlug::normalize($slug);
}
// We have to make two queries here to distinguish between
// documents the user can't see, and documents that don't
// exist.
$visible_documents = id(new PhrictionDocumentQuery())
->setViewer($engine->getConfig('viewer'))
->withSlugs($slugs)
->needContent(true)
->execute();
$existant_documents = id(new PhrictionDocumentQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSlugs($slugs)
->execute();
$visible_documents = mpull($visible_documents, null, 'getSlug');
$existant_documents = mpull($existant_documents, null, 'getSlug');
foreach ($metadata as $spec) {
$link = $spec['link'];
$slug = PhabricatorSlug::normalize($link);
$name = $spec['explicitName'];
$class = 'phriction-link';
// If the name is something meaningful to humans, we'll render this
// in text as: "Title" <link>. Otherwise, we'll just render: <link>.
$is_interesting_name = (bool)strlen($name);
if (idx($existant_documents, $slug) === null) {
// The target document doesn't exist.
if ($name === null) {
$name = explode('/', trim($link, '/'));
$name = end($name); $name = end($name);
} }
$class = 'phriction-link-missing';
} else if (idx($visible_documents, $slug) === null) {
// The document exists, but the user can't see it.
if ($name === null) {
$name = explode('/', trim($link, '/'));
$name = end($name);
}
$class = 'phriction-link-lock';
} else {
if ($name === null) {
// Use the title of the document if no name is set.
$name = $visible_documents[$slug]
->getContent()
->getTitle();
$is_interesting_name = true;
}
}
$uri = new PhutilURI($link); $uri = new PhutilURI($link);
$slug = $uri->getPath(); $slug = $uri->getPath();
@ -34,18 +153,36 @@ final class PhrictionRemarkupRule extends PhutilRemarkupRule {
if ($this->getEngine()->getState('toc')) { if ($this->getEngine()->getState('toc')) {
$text = $name; $text = $name;
} else if ($text_mode || $mail_mode) { } else if ($text_mode || $mail_mode) {
return PhabricatorEnv::getProductionURI($href); $href = PhabricatorEnv::getProductionURI($href);
if ($is_interesting_name) {
$text = pht('"%s" <%s>', $name, $href);
} else { } else {
$text = pht('<%s>', $href);
}
} else {
if ($class === 'phriction-link-lock') {
$name = array(
$this->newTag(
'i',
array(
'class' => 'phui-icon-view phui-font-fa fa-lock',
),
''),
' ',
$name,
);
}
$text = $this->newTag( $text = $this->newTag(
'a', 'a',
array( array(
'href' => $href, 'href' => $href,
'class' => 'phriction-link', 'class' => $class,
), ),
$name); $name);
} }
return $this->getEngine()->storeText($text); $this->getEngine()->overwriteStoredText($spec['token'], $text);
}
} }
} }

View file

@ -27,7 +27,8 @@ final class PhrictionContent extends PhrictionDAO
return PhabricatorMarkupEngine::renderOneObject( return PhabricatorMarkupEngine::renderOneObject(
$this, $this,
self::MARKUP_FIELD_BODY, self::MARKUP_FIELD_BODY,
$viewer); $viewer,
$this);
} }
protected function getConfiguration() { protected function getConfiguration() {

View file

@ -165,7 +165,7 @@ final class PhabricatorPhurlURLEditController
->setError($error_alias); ->setError($error_alias);
$projects = id(new AphrontFormTokenizerControl()) $projects = id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setValue($projects) ->setValue($projects)
->setUser($viewer) ->setUser($viewer)

View file

@ -156,7 +156,7 @@ final class PonderQuestionEditController extends PonderController {
$form->appendControl( $form->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setValue($v_projects) ->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource())); ->setDatasource(new PhabricatorProjectDatasource()));

View file

@ -7,10 +7,10 @@ final class PhabricatorRepositoryType extends Phobject {
const REPOSITORY_TYPE_MERCURIAL = 'hg'; const REPOSITORY_TYPE_MERCURIAL = 'hg';
public static function getAllRepositoryTypes() { public static function getAllRepositoryTypes() {
static $map = array( $map = array(
self::REPOSITORY_TYPE_GIT => 'Git', self::REPOSITORY_TYPE_GIT => pht('Git'),
self::REPOSITORY_TYPE_SVN => 'Subversion', self::REPOSITORY_TYPE_MERCURIAL => pht('Mercurial'),
self::REPOSITORY_TYPE_MERCURIAL => 'Mercurial', self::REPOSITORY_TYPE_SVN => pht('Subversion'),
); );
return $map; return $map;
} }

View file

@ -248,25 +248,7 @@ final class PhabricatorRepositoryEditor
$object->setCallsign($xaction->getNewValue()); $object->setCallsign($xaction->getNewValue());
return; return;
case PhabricatorRepositoryTransaction::TYPE_ENCODING: case PhabricatorRepositoryTransaction::TYPE_ENCODING:
// Make sure the encoding is valid by converting to UTF-8. This tests $object->setDetail('encoding', $xaction->getNewValue());
// that the user has mbstring installed, and also that they didn't type
// a garbage encoding name. Note that we're converting from UTF-8 to
// the target encoding, because mbstring is fine with converting from
// a nonsense encoding.
$encoding = $xaction->getNewValue();
if (strlen($encoding)) {
try {
phutil_utf8_convert('.', $encoding, 'UTF-8');
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
"Error setting repository encoding '%s': %s'",
$encoding,
$ex->getMessage()),
$ex);
}
}
$object->setDetail('encoding', $encoding);
break; break;
} }
} }
@ -461,6 +443,117 @@ final class PhabricatorRepositoryEditor
} }
break; break;
case PhabricatorRepositoryTransaction::TYPE_VCS:
$vcs_map = PhabricatorRepositoryType::getAllRepositoryTypes();
$current_vcs = $object->getVersionControlSystem();
if (!$this->getIsNewObject()) {
foreach ($xactions as $xaction) {
if ($xaction->getNewValue() == $current_vcs) {
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Immutable'),
pht(
'You can not change the version control system an existing '.
'repository uses. It can only be set when a repository is '.
'first created.'),
$xaction);
}
} else {
$value = $object->getVersionControlSystem();
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if (empty($vcs_map[$value])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Specified version control system must be a VCS '.
'recognized by Phabricator: %s.',
implode(', ', array_keys($vcs_map))),
$xaction);
}
}
if (!strlen($value)) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht(
'When creating a repository, you must specify a valid '.
'underlying version control system: %s.',
implode(', ', array_keys($vcs_map))),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Repository name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
$status_map = PhabricatorRepository::getStatusMap();
foreach ($xactions as $xaction) {
$status = $xaction->getNewValue();
if (empty($status_map[$status])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Repository status "%s" is not valid.',
$status),
$xaction);
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_ENCODING:
foreach ($xactions as $xaction) {
// Make sure the encoding is valid by converting to UTF-8. This tests
// that the user has mbstring installed, and also that they didn't
// type a garbage encoding name. Note that we're converting from
// UTF-8 to the target encoding, because mbstring is fine with
// converting from a nonsense encoding.
$encoding = $xaction->getNewValue();
if (!strlen($encoding)) {
continue;
}
try {
phutil_utf8_convert('.', $encoding, 'UTF-8');
} catch (Exception $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Repository encoding "%s" is not valid: %s',
$encoding,
$ex->getMessage()),
$xaction);
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_SLUG: case PhabricatorRepositoryTransaction::TYPE_SLUG:
foreach ($xactions as $xaction) { foreach ($xactions as $xaction) {
$old = $xaction->getOldValue(); $old = $xaction->getOldValue();
@ -590,6 +683,10 @@ final class PhabricatorRepositoryEditor
$object->save(); $object->save();
} }
if ($this->getIsNewObject()) {
$object->synchronizeWorkingCopyAfterCreation();
}
return $xactions; return $xactions;
} }

View file

@ -96,6 +96,8 @@ final class PhabricatorRepositoryPullEngine
} }
if ($repository->isHosted()) { if ($repository->isHosted()) {
$repository->synchronizeWorkingCopyBeforeRead();
if ($is_git) { if ($is_git) {
$this->installGitHook(); $this->installGitHook();
} else if ($is_svn) { } else if ($is_svn) {

View file

@ -43,6 +43,7 @@ final class PhabricatorRepositoryManagementLookupUsersWorkflow
)), )),
'diffusion.querycommits', 'diffusion.querycommits',
array( array(
'repositoryPHID' => $repo->getPHID(),
'phids' => array($commit->getPHID()), 'phids' => array($commit->getPHID()),
'bypassCache' => true, 'bypassCache' => true,
)); ));

View file

@ -0,0 +1,186 @@
<?php
final class PhabricatorRepositoryManagementThawWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('thaw')
->setExamples('**thaw** [options] __repository__ ...')
->setSynopsis(
pht(
'Resolve issues with frozen cluster repositories. Very advanced '.
'and dangerous.'))
->setArguments(
array(
array(
'name' => 'demote',
'param' => 'device',
'help' => pht(
'Demote a device, discarding local changes. Clears stuck '.
'write locks and recovers from lost leaders.'),
),
array(
'name' => 'promote',
'param' => 'device',
'help' => pht(
'Promote a device, discarding changes on other devices. '.
'Resolves ambiguous leadership and recovers from demotion '.
'mistakes.'),
),
array(
'name' => 'force',
'help' => pht('Run operations without asking for confirmation.'),
),
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 thaw.'));
}
$promote = $args->getArg('promote');
$demote = $args->getArg('demote');
if (!$promote && !$demote) {
throw new PhutilArgumentUsageException(
pht('You must choose a device to --promote or --demote.'));
}
if ($promote && $demote) {
throw new PhutilArgumentUsageException(
pht('Specify either --promote or --demote, but not both.'));
}
$device_name = nonempty($promote, $demote);
$device = id(new AlmanacDeviceQuery())
->setViewer($viewer)
->withNames(array($device_name))
->executeOne();
if (!$device) {
throw new PhutilArgumentUsageException(
pht('No device "%s" exists.', $device_name));
}
if ($promote) {
$risk_message = pht(
'Promoting a device can cause the loss of any repository data which '.
'only exists on other devices. The version of the repository on the '.
'promoted device will become authoritative.');
} else {
$risk_message = pht(
'Demoting a device can cause the loss of any repository data which '.
'only exists on the demoted device. The version of the repository '.
'on some other device will become authoritative.');
}
echo tsprintf(
"**<bg:red> %s </bg>** %s\n",
pht('DATA AT RISK'),
$risk_message);
$is_force = $args->getArg('force');
$prompt = pht('Accept the possibilty of permanent data loss?');
if (!$is_force && !phutil_console_confirm($prompt)) {
throw new PhutilArgumentUsageException(
pht('User aborted the workflow.'));
}
foreach ($repositories as $repository) {
$repository_phid = $repository->getPHID();
$write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock(
$repository_phid);
echo tsprintf(
"%s\n",
pht(
'Waiting to acquire write lock for "%s"...',
$repository->getDisplayName()));
$write_lock->lock(phutil_units('5 minutes in seconds'));
try {
$service = $repository->loadAlmanacService();
if (!$service) {
throw new PhutilArgumentUsageException(
pht(
'Repository "%s" is not a cluster repository: it is not '.
'bound to an Almanac service.',
$repository->getDisplayName()));
}
$bindings = $service->getActiveBindings();
$bindings = mpull($bindings, null, 'getDevicePHID');
if (empty($bindings[$device->getPHID()])) {
throw new PhutilArgumentUsageException(
pht(
'Repository "%s" has no active binding to device "%s". Only '.
'actively bound devices can be promoted or demoted.',
$repository->getDisplayName(),
$device->getName()));
}
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository->getPHID());
$versions = mpull($versions, null, 'getDevicePHID');
$versions = array_select_keys($versions, array_keys($bindings));
if ($versions && $promote) {
throw new PhutilArgumentUsageException(
pht(
'Unable to promote "%s" for repository "%s": the leaders for '.
'this cluster are not ambiguous.',
$device->getName(),
$repository->getDisplayName()));
}
if ($promote) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository->getPHID(),
$device->getPHID(),
0);
echo tsprintf(
"%s\n",
pht(
'Promoted "%s" to become a leader for "%s".',
$device->getName(),
$repository->getDisplayName()));
}
if ($demote) {
PhabricatorRepositoryWorkingCopyVersion::demoteDevice(
$repository->getPHID(),
$device->getPHID());
echo tsprintf(
"%s\n",
pht(
'Demoted "%s" from leadership of repository "%s".',
$device->getName(),
$repository->getDisplayName()));
}
} catch (Exception $ex) {
$write_lock->unlock();
throw $ex;
}
$write_lock->unlock();
}
return 0;
}
}

View file

@ -92,20 +92,13 @@ final class PhabricatorRepositoryPushLogSearchEngine
return parent::buildSavedQueryFromBuiltin($query_key); return parent::buildSavedQueryFromBuiltin($query_key);
} }
protected function getRequiredHandlePHIDsForResultList(
array $logs,
PhabricatorSavedQuery $query) {
return mpull($logs, 'getPusherPHID');
}
protected function renderResultList( protected function renderResultList(
array $logs, array $logs,
PhabricatorSavedQuery $query, PhabricatorSavedQuery $query,
array $handles) { array $handles) {
$table = id(new DiffusionPushLogListView()) $table = id(new DiffusionPushLogListView())
->setUser($this->requireViewer()) ->setViewer($this->requireViewer())
->setHandles($handles)
->setLogs($logs); ->setLogs($logs);
return id(new PhabricatorApplicationSearchResultView()) return id(new PhabricatorApplicationSearchResultView())

View file

@ -13,7 +13,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
PhabricatorMarkupInterface, PhabricatorMarkupInterface,
PhabricatorDestructibleInterface, PhabricatorDestructibleInterface,
PhabricatorProjectInterface, PhabricatorProjectInterface,
PhabricatorSpacesInterface { PhabricatorSpacesInterface,
PhabricatorConduitResultInterface {
/** /**
* Shortest hash we'll recognize in raw "a829f32" form. * Shortest hash we'll recognize in raw "a829f32" form.
@ -45,6 +46,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
const BECAUSE_BRANCH_NOT_AUTOCLOSE = 'auto/noclose'; const BECAUSE_BRANCH_NOT_AUTOCLOSE = 'auto/noclose';
const BECAUSE_AUTOCLOSE_FORCED = 'auto/forced'; const BECAUSE_AUTOCLOSE_FORCED = 'auto/forced';
const STATUS_ACTIVE = 'active';
const STATUS_INACTIVE = 'inactive';
protected $name; protected $name;
protected $callsign; protected $callsign;
protected $repositorySlug; protected $repositorySlug;
@ -62,10 +66,12 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
private $commitCount = self::ATTACHABLE; private $commitCount = self::ATTACHABLE;
private $mostRecentCommit = self::ATTACHABLE; private $mostRecentCommit = self::ATTACHABLE;
private $projectPHIDs = self::ATTACHABLE; private $projectPHIDs = self::ATTACHABLE;
private $uris = self::ATTACHABLE;
private $clusterWriteLock; private $clusterWriteLock;
private $clusterWriteVersion; private $clusterWriteVersion;
public static function initializeNewRepository(PhabricatorUser $actor) { public static function initializeNewRepository(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery()) $app = id(new PhabricatorApplicationQuery())
->setViewer($actor) ->setViewer($actor)
@ -129,6 +135,31 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
PhabricatorRepositoryRepositoryPHIDType::TYPECONST); PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
} }
public static function getStatusMap() {
return array(
self::STATUS_ACTIVE => array(
'name' => pht('Active'),
'isTracked' => 1,
),
self::STATUS_INACTIVE => array(
'name' => pht('Inactive'),
'isTracked' => 0,
),
);
}
public static function getStatusNameMap() {
return ipull(self::getStatusMap(), 'name');
}
public function getStatus() {
if ($this->isTracked()) {
return self::STATUS_ACTIVE;
} else {
return self::STATUS_INACTIVE;
}
}
public function toDictionary() { public function toDictionary() {
return array( return array(
'id' => $this->getID(), 'id' => $this->getID(),
@ -143,7 +174,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
'isActive' => $this->isTracked(), 'isActive' => $this->isTracked(),
'isHosted' => $this->isHosted(), 'isHosted' => $this->isHosted(),
'isImporting' => $this->isImporting(), 'isImporting' => $this->isImporting(),
'encoding' => $this->getDetail('encoding'), 'encoding' => $this->getDefaultTextEncoding(),
'staging' => array( 'staging' => array(
'supported' => $this->supportsStaging(), 'supported' => $this->supportsStaging(),
'prefix' => 'phabricator', 'prefix' => 'phabricator',
@ -152,6 +183,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
); );
} }
public function getDefaultTextEncoding() {
return $this->getDetail('encoding', 'UTF-8');
}
public function getMonogram() { public function getMonogram() {
$callsign = $this->getCallsign(); $callsign = $this->getCallsign();
if (strlen($callsign)) { if (strlen($callsign)) {
@ -452,19 +487,22 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
} }
private function newRemoteCommandFuture(array $argv) { private function newRemoteCommandFuture(array $argv) {
$argv = $this->formatRemoteCommand($argv); return $this->newRemoteCommandEngine($argv)
$future = newv('ExecFuture', $argv); ->newFuture();
$future->setEnv($this->getRemoteCommandEnvironment());
return $future;
} }
private function newRemoteCommandPassthru(array $argv) { private function newRemoteCommandPassthru(array $argv) {
$argv = $this->formatRemoteCommand($argv); return $this->newRemoteCommandEngine($argv)
$passthru = newv('PhutilExecPassthru', $argv); ->setPassthru(true)
$passthru->setEnv($this->getRemoteCommandEnvironment()); ->newFuture();
return $passthru;
} }
private function newRemoteCommandEngine(array $argv) {
return DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->setCredentialPHID($this->getCredentialPHID())
->setProtocol($this->getRemoteProtocol());
}
/* -( Local Command Execution )-------------------------------------------- */ /* -( Local Command Execution )-------------------------------------------- */
@ -492,9 +530,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
private function newLocalCommandFuture(array $argv) { private function newLocalCommandFuture(array $argv) {
$this->assertLocalExists(); $this->assertLocalExists();
$argv = $this->formatLocalCommand($argv); $future = DiffusionCommandEngine::newCommandEngine($this)
$future = newv('ExecFuture', $argv); ->setArgv($argv)
$future->setEnv($this->getLocalCommandEnvironment()); ->newFuture();
if ($this->usesLocalWorkingCopy()) { if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath()); $future->setCWD($this->getLocalPath());
@ -506,9 +544,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
private function newLocalCommandPassthru(array $argv) { private function newLocalCommandPassthru(array $argv) {
$this->assertLocalExists(); $this->assertLocalExists();
$argv = $this->formatLocalCommand($argv); $future = DiffusionCommandEngine::newCommandEngine($this)
$future = newv('PhutilExecPassthru', $argv); ->setArgv($argv)
$future->setEnv($this->getLocalCommandEnvironment()); ->setPassthru(true)
->newFuture();
if ($this->usesLocalWorkingCopy()) { if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath()); $future->setCWD($this->getLocalPath());
@ -517,199 +556,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return $future; return $future;
} }
/* -( Command Infrastructure )--------------------------------------------- */
private function getSSHWrapper() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/bin/ssh-connect';
}
private function getCommonCommandEnvironment() {
$env = array(
// NOTE: Force the language to "en_US.UTF-8", which overrides locale
// settings. This makes stuff print in English instead of, e.g., French,
// so we can parse the output of some commands, error messages, etc.
'LANG' => 'en_US.UTF-8',
// Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155.
'PHABRICATOR_ENV' => PhabricatorEnv::getSelectedEnvironmentName(),
);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if
// it can not read $HOME. For many users, $HOME points at /root (this
// seems to be a default result of Apache setup). Instead, explicitly
// point $HOME at a readable, empty directory so that Git looks for the
// config file it's after, fails to locate it, and moves on. This is
// really silly, but seems like the least damaging approach to
// mitigating the issue.
$root = dirname(phutil_get_library_root('phabricator'));
$env['HOME'] = $root.'/support/empty/';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: This overrides certain configuration, extensions, and settings
// which make Mercurial commands do random unusual things.
$env['HGPLAIN'] = 1;
break;
default:
throw new Exception(pht('Unrecognized version control system.'));
}
return $env;
}
private function getLocalCommandEnvironment() {
return $this->getCommonCommandEnvironment();
}
private function getRemoteCommandEnvironment() {
$env = $this->getCommonCommandEnvironment();
if ($this->shouldUseSSH()) {
// NOTE: This is read by `bin/ssh-connect`, and tells it which credentials
// to use.
$env['PHABRICATOR_CREDENTIAL'] = $this->getCredentialPHID();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// Force SVN to use `bin/ssh-connect`.
$env['SVN_SSH'] = $this->getSSHWrapper();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// Force Git to use `bin/ssh-connect`.
$env['GIT_SSH'] = $this->getSSHWrapper();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// We force Mercurial through `bin/ssh-connect` too, but it uses a
// command-line flag instead of an environmental variable.
break;
default:
throw new Exception(pht('Unrecognized version control system.'));
}
}
return $env;
}
private function formatRemoteCommand(array $args) {
$pattern = $args[0];
$args = array_slice($args, 1);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
if ($this->shouldUseHTTP() || $this->shouldUseSVNProtocol()) {
$flags = array();
$flag_args = array();
$flags[] = '--non-interactive';
$flags[] = '--no-auth-cache';
if ($this->shouldUseHTTP()) {
$flags[] = '--trust-server-cert';
}
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$flags[] = '--username %P';
$flags[] = '--password %P';
$flag_args[] = $key->getUsernameEnvelope();
$flag_args[] = $key->getPasswordEnvelope();
}
$flags = implode(' ', $flags);
$pattern = "svn {$flags} {$pattern}";
$args = array_mergev(array($flag_args, $args));
} else {
$pattern = "svn --non-interactive {$pattern}";
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$pattern = "git {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if ($this->shouldUseSSH()) {
$pattern = "hg --config ui.ssh=%s {$pattern}";
array_unshift(
$args,
$this->getSSHWrapper());
} else {
$pattern = "hg {$pattern}";
}
break;
default:
throw new Exception(pht('Unrecognized version control system.'));
}
array_unshift($args, $pattern);
return $args;
}
private function formatLocalCommand(array $args) {
$pattern = $args[0];
$args = array_slice($args, 1);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$pattern = "svn --non-interactive {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$pattern = "git {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$pattern = "hg {$pattern}";
break;
default:
throw new Exception(pht('Unrecognized version control system.'));
}
array_unshift($args, $pattern);
return $args;
}
/**
* Sanitize output of an `hg` command invoked with the `--debug` flag to make
* it usable.
*
* @param string Output from `hg --debug ...`
* @return string Usable output.
*/
public static function filterMercurialDebugOutput($stdout) {
// When hg commands are run with `--debug` and some config file isn't
// trusted, Mercurial prints out a warning to stdout, twice, after Feb 2011.
//
// http://selenic.com/pipermail/mercurial-devel/2011-February/028541.html
//
// After Jan 2015, it may also fail to write to a revision branch cache.
$ignore = array(
'ignoring untrusted configuration option',
"couldn't write revision branch cache:",
);
foreach ($ignore as $key => $pattern) {
$ignore[$key] = preg_quote($pattern, '/');
}
$ignore = '('.implode('|', $ignore).')';
$lines = preg_split('/(?<=\n)/', $stdout);
$regex = '/'.$ignore.'.*\n$/';
foreach ($lines as $key => $line) {
$lines[$key] = preg_replace($regex, '', $line);
}
return implode('', $lines);
}
public function getURI() { public function getURI() {
$callsign = $this->getCallsign(); $callsign = $this->getCallsign();
if (strlen($callsign)) { if (strlen($callsign)) {
@ -991,7 +837,20 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
} }
public function isTracked() { public function isTracked() {
return $this->getDetail('tracking-enabled', false); $status = $this->getDetail('tracking-enabled');
$map = self::getStatusMap();
$spec = idx($map, $status);
if (!$spec) {
if ($status) {
$status = self::STATUS_ACTIVE;
} else {
$status = self::STATUS_INACTIVE;
}
$spec = idx($map, $status);
}
return (bool)idx($spec, 'isTracked', false);
} }
public function getDefaultBranch() { public function getDefaultBranch() {
@ -1489,8 +1348,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
$uri->setPath($uri->getPath().$this->getCloneName().'/'); $uri->setPath($uri->getPath().$this->getCloneName().'/');
} }
$ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user'); $ssh_user = AlmanacKeys::getClusterSSHUser();
if ($ssh_user) { if (strlen($ssh_user)) {
$uri->setUser($ssh_user); $uri->setUser($ssh_user);
} }
@ -2068,34 +1927,12 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
$never_proxy, $never_proxy,
array $protocols) { array $protocols) {
$service_phid = $this->getAlmanacServicePHID(); $service = $this->loadAlmanacService();
if (!$service_phid) { if (!$service) {
// No service, so this is a local repository.
return null; return null;
} }
$service = id(new AlmanacServiceQuery()) $bindings = $service->getActiveBindings();
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($service_phid))
->needBindings(true)
->needProperties(true)
->executeOne();
if (!$service) {
throw new Exception(
pht(
'The Almanac service for this repository is invalid or could not '.
'be loaded.'));
}
$service_type = $service->getServiceImplementation();
if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
throw new Exception(
pht(
'The Almanac service for this repository does not have the correct '.
'service type.'));
}
$bindings = $service->getBindings();
if (!$bindings) { if (!$bindings) {
throw new Exception( throw new Exception(
pht( pht(
@ -2131,16 +1968,14 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
} }
} }
$protocol = $binding->getAlmanacPropertyValue('protocol'); $uri = $this->getClusterRepositoryURIFromBinding($binding);
if ($protocol === null) {
$protocol = 'https';
}
$protocol = $uri->getProtocol();
if (empty($protocol_map[$protocol])) { if (empty($protocol_map[$protocol])) {
continue; continue;
} }
$uris[] = $protocol.'://'.$iface->renderDisplayAddress().'/'; $uris[] = $uri;
} }
if (!$uris) { if (!$uris) {
@ -2265,13 +2100,119 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return $client; return $client;
} }
/* -( Repository URIs )---------------------------------------------------- */
public function attachURIs(array $uris) {
$custom_map = array();
foreach ($uris as $key => $uri) {
$builtin_key = $uri->getRepositoryURIBuiltinKey();
if ($builtin_key !== null) {
$custom_map[$builtin_key] = $key;
}
}
$builtin_uris = $this->newBuiltinURIs();
$seen_builtins = array();
foreach ($builtin_uris as $builtin_uri) {
$builtin_key = $builtin_uri->getRepositoryURIBuiltinKey();
$seen_builtins[$builtin_key] = true;
// If this builtin URI is disabled, don't attach it and remove the
// persisted version if it exists.
if ($builtin_uri->getIsDisabled()) {
if (isset($custom_map[$builtin_key])) {
unset($uris[$custom_map[$builtin_key]]);
}
continue;
}
// If we don't have a persisted version of the URI, add the builtin
// version.
if (empty($custom_map[$builtin_key])) {
$uris[] = $builtin_uri;
}
}
// Remove any builtins which no longer exist.
foreach ($custom_map as $builtin_key => $key) {
if (empty($seen_builtins[$builtin_key])) {
unset($uris[$key]);
}
}
$this->uris = $uris;
return $this;
}
public function getURIs() {
return $this->assertAttached($this->uris);
}
protected function newBuiltinURIs() {
$has_callsign = ($this->getCallsign() !== null);
$has_shortname = ($this->getRepositorySlug() !== null);
$identifier_map = array(
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true,
);
$allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
$base_uri = PhabricatorEnv::getURI('/');
$base_uri = new PhutilURI($base_uri);
$has_https = ($base_uri->getProtocol() == 'https');
$has_https = ($has_https && $allow_http);
$has_http = !PhabricatorEnv::getEnvConfig('security.require-https');
$has_http = ($has_http && $allow_http);
// TODO: Maybe allow users to disable this by default somehow?
$has_ssh = true;
$protocol_map = array(
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh,
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https,
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http,
);
$uris = array();
foreach ($protocol_map as $protocol => $proto_supported) {
foreach ($identifier_map as $identifier => $id_supported) {
$uris[] = PhabricatorRepositoryURI::initializeNewURI($this)
->setBuiltinProtocol($protocol)
->setBuiltinIdentifier($identifier)
->setIsDisabled(!$proto_supported || !$id_supported);
}
}
return $uris;
}
/* -( Cluster Synchronization )-------------------------------------------- */ /* -( Cluster Synchronization )-------------------------------------------- */
private function shouldEnableSynchronization() { private function shouldEnableSynchronization() {
// TODO: This mostly works, but isn't stable enough for production yet. $service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
return false; 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(); $device = AlmanacKeys::getLiveDevice();
if (!$device) { if (!$device) {
@ -2282,6 +2223,37 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
} }
/**
* 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 * @task sync
*/ */
@ -2310,15 +2282,15 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
if ($this_version) { if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion(); $this_version = (int)$this_version->getRepositoryVersion();
} else { } else {
$this_version = 0; $this_version = -1;
} }
if ($versions) { if ($versions) {
$max_version = (int)max(mpull($versions, 'getRepositoryVersion')); // This is the normal case, where we have some version information and
} else { // can identify which nodes are leaders. If the current node is not a
$max_version = 0; // 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) { if ($max_version > $this_version) {
$fetchable = array(); $fetchable = array();
foreach ($versions as $version) { foreach ($versions as $version) {
@ -2327,8 +2299,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
} }
} }
// TODO: Actualy fetch the newer version from one of the nodes which has $this->synchronizeWorkingCopyFromDevices($fetchable);
// it.
PhabricatorRepositoryWorkingCopyVersion::updateVersion( PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid, $repository_phid,
@ -2336,16 +2307,74 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
$max_version); $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(); $read_lock->unlock();
return $max_version; return $result_version;
} }
/** /**
* @task sync * @task sync
*/ */
public function synchronizeWorkingCopyBeforeWrite() { public function synchronizeWorkingCopyBeforeWrite(
PhabricatorUser $actor) {
if (!$this->shouldEnableSynchronization()) { if (!$this->shouldEnableSynchronization()) {
return; return;
} }
@ -2368,18 +2397,29 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
continue; continue;
} }
// TODO: This should provide more help so users can resolve the issue.
throw new Exception( throw new Exception(
pht( pht(
'An incomplete write was previously performed to this repository; '. 'An previous write to this repository was interrupted; refusing '.
'refusing new writes.')); '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(); $max_version = $this->synchronizeWorkingCopyBeforeRead();
} catch (Exception $ex) {
$write_lock->unlock();
throw $ex;
}
PhabricatorRepositoryWorkingCopyVersion::willWrite( PhabricatorRepositoryWorkingCopyVersion::willWrite(
$repository_phid, $repository_phid,
$device_phid); $device_phid,
array(
'userPHID' => $actor->getPHID(),
'epoch' => PhabricatorTime::getNow(),
'devicePHID' => $device_phid,
));
$this->clusterWriteVersion = $max_version; $this->clusterWriteVersion = $max_version;
$this->clusterWriteLock = $write_lock; $this->clusterWriteLock = $write_lock;
@ -2434,6 +2474,147 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
} }
/**
* @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(
AlmanacBinding $binding) {
$protocol = $binding->getAlmanacPropertyValue('protocol');
if ($protocol === null) {
$protocol = 'https';
}
$iface = $binding->getInterface();
$address = $iface->renderDisplayAddress();
$path = $this->getURI();
return id(new PhutilURI("{$protocol}://{$address}"))
->setPath($path);
}
public function loadAlmanacService() {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
// No service, so this is a local repository.
return null;
}
$service = id(new AlmanacServiceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($service_phid))
->needBindings(true)
->needProperties(true)
->executeOne();
if (!$service) {
throw new Exception(
pht(
'The Almanac service for this repository is invalid or could not '.
'be loaded.'));
}
$service_type = $service->getServiceImplementation();
if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
throw new Exception(
pht(
'The Almanac service for this repository does not have the correct '.
'service type.'));
}
return $service;
}
/* -( Symbols )-------------------------------------------------------------*/ /* -( Symbols )-------------------------------------------------------------*/
public function getSymbolSources() { public function getSymbolSources() {
@ -2626,4 +2807,42 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return $this->spacePHID; return $this->spacePHID;
} }
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The repository name.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('vcs')
->setType('string')
->setDescription(
pht('The VCS this repository uses ("git", "hg" or "svn").')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('callsign')
->setType('string')
->setDescription(pht('The repository callsign, if it has one.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('shortName')
->setType('string')
->setDescription(pht('Unique short name, if the repository has one.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'vcs' => $this->getVersionControlSystem(),
'callsign' => $this->getCallsign(),
'shortName' => $this->getRepositorySlug(),
);
}
public function getConduitSearchAttachments() {
return array();
}
} }

View file

@ -34,6 +34,7 @@ final class PhabricatorRepositoryPushLog
protected $epoch; protected $epoch;
protected $pusherPHID; protected $pusherPHID;
protected $pushEventPHID; protected $pushEventPHID;
protected $devicePHID;
protected $refType; protected $refType;
protected $refNameHash; protected $refNameHash;
protected $refNameRaw; protected $refNameRaw;
@ -81,6 +82,7 @@ final class PhabricatorRepositoryPushLog
'refNew' => 'text40', 'refNew' => 'text40',
'mergeBase' => 'text40?', 'mergeBase' => 'text40?',
'changeFlags' => 'uint32', 'changeFlags' => 'uint32',
'devicePHID' => 'phid?',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_repository' => array( 'key_repository' => array(

View file

@ -12,16 +12,9 @@ final class PhabricatorRepositoryTransaction
const TYPE_TRACK_ONLY = 'repo:track-only'; const TYPE_TRACK_ONLY = 'repo:track-only';
const TYPE_AUTOCLOSE_ONLY = 'repo:autoclose-only'; const TYPE_AUTOCLOSE_ONLY = 'repo:autoclose-only';
const TYPE_SVN_SUBPATH = 'repo:svn-subpath'; const TYPE_SVN_SUBPATH = 'repo:svn-subpath';
const TYPE_UUID = 'repo:uuid';
const TYPE_NOTIFY = 'repo:notify'; const TYPE_NOTIFY = 'repo:notify';
const TYPE_AUTOCLOSE = 'repo:autoclose'; const TYPE_AUTOCLOSE = 'repo:autoclose';
const TYPE_REMOTE_URI = 'repo:remote-uri';
const TYPE_LOCAL_PATH = 'repo:local-path';
const TYPE_HOSTING = 'repo:hosting';
const TYPE_PROTOCOL_HTTP = 'repo:serve-http';
const TYPE_PROTOCOL_SSH = 'repo:serve-ssh';
const TYPE_PUSH_POLICY = 'repo:push-policy'; const TYPE_PUSH_POLICY = 'repo:push-policy';
const TYPE_CREDENTIAL = 'repo:credential';
const TYPE_DANGEROUS = 'repo:dangerous'; const TYPE_DANGEROUS = 'repo:dangerous';
const TYPE_SLUG = 'repo:slug'; const TYPE_SLUG = 'repo:slug';
const TYPE_SERVICE = 'repo:service'; const TYPE_SERVICE = 'repo:service';
@ -37,6 +30,13 @@ final class PhabricatorRepositoryTransaction
const TYPE_SSH_KEYFILE = 'repo:ssh-keyfile'; const TYPE_SSH_KEYFILE = 'repo:ssh-keyfile';
const TYPE_HTTP_LOGIN = 'repo:http-login'; const TYPE_HTTP_LOGIN = 'repo:http-login';
const TYPE_HTTP_PASS = 'repo:http-pass'; const TYPE_HTTP_PASS = 'repo:http-pass';
const TYPE_CREDENTIAL = 'repo:credential';
const TYPE_PROTOCOL_HTTP = 'repo:serve-http';
const TYPE_PROTOCOL_SSH = 'repo:serve-ssh';
const TYPE_HOSTING = 'repo:hosting';
const TYPE_LOCAL_PATH = 'repo:local-path';
const TYPE_REMOTE_URI = 'repo:remote-uri';
const TYPE_UUID = 'repo:uuid';
public function getApplicationName() { public function getApplicationName() {
return 'repository'; return 'repository';
@ -134,7 +134,13 @@ final class PhabricatorRepositoryTransaction
'%s created this repository.', '%s created this repository.',
$this->renderHandleLink($author_phid)); $this->renderHandleLink($author_phid));
case self::TYPE_ACTIVATE: case self::TYPE_ACTIVATE:
if ($new) { // TODO: Old versions of this transaction use a boolean value, but
// should be migrated.
$is_deactivate =
(!$new) ||
($new == PhabricatorRepository::STATUS_INACTIVE);
if (!$is_deactivate) {
return pht( return pht(
'%s activated this repository.', '%s activated this repository.',
$this->renderHandleLink($author_phid)); $this->renderHandleLink($author_phid));

View file

@ -0,0 +1,301 @@
<?php
final class PhabricatorRepositoryURI
extends PhabricatorRepositoryDAO {
protected $repositoryPHID;
protected $uri;
protected $builtinProtocol;
protected $builtinIdentifier;
protected $credentialPHID;
protected $ioType;
protected $displayType;
protected $isDisabled;
private $repository = self::ATTACHABLE;
const BUILTIN_PROTOCOL_SSH = 'ssh';
const BUILTIN_PROTOCOL_HTTP = 'http';
const BUILTIN_PROTOCOL_HTTPS = 'https';
const BUILTIN_IDENTIFIER_ID = 'id';
const BUILTIN_IDENTIFIER_SHORTNAME = 'shortname';
const BUILTIN_IDENTIFIER_CALLSIGN = 'callsign';
const DISPLAY_DEFAULT = 'default';
const DISPLAY_NEVER = 'never';
const DISPLAY_ALWAYS = 'always';
const IO_DEFAULT = 'default';
const IO_OBSERVE = 'observe';
const IO_MIRROR = 'mirror';
const IO_NONE = 'none';
const IO_READ = 'read';
const IO_READWRITE = 'readwrite';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'uri' => 'text255',
'builtinProtocol' => 'text32?',
'builtinIdentifier' => 'text32?',
'credentialPHID' => 'phid?',
'ioType' => 'text32',
'displayType' => 'text32',
'isDisabled' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_builtin' => array(
'columns' => array(
'repositoryPHID',
'builtinProtocol',
'builtinIdentifier',
),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public static function initializeNewURI(PhabricatorRepository $repository) {
return id(new self())
->attachRepository($repository)
->setRepositoryPHID($repository->getPHID())
->setIoType(self::IO_DEFAULT)
->setDisplayType(self::DISPLAY_DEFAULT)
->setIsDisabled(0);
}
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function getRepositoryURIBuiltinKey() {
if (!$this->getBuiltinProtocol()) {
return null;
}
$parts = array(
$this->getBuiltinProtocol(),
$this->getBuiltinIdentifier(),
);
return implode('.', $parts);
}
public function isBuiltin() {
return (bool)$this->getBuiltinProtocol();
}
public function getEffectiveDisplayType() {
$display = $this->getDisplayType();
if ($display != self::IO_DEFAULT) {
return $display;
}
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;
}
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();
$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];
}
$best_identifier = max($have_identifiers);
$this_identifier = $identifier_value[$this->getBuiltinIdentifier()];
if ($this_identifier < $best_identifier) {
return self::DISPLAY_NEVER;
}
}
return self::DISPLAY_ALWAYS;
}
}
public function getEffectiveIOType() {
$io = $this->getIoType();
if ($io != self::IO_DEFAULT) {
return $io;
}
if ($this->isBuiltin()) {
$repository = $this->getRepository();
$other_uris = $repository->getURIs();
$any_observe = false;
foreach ($other_uris as $other_uri) {
if ($other_uri->getIoType() == self::IO_OBSERVE) {
$any_observe = true;
break;
}
}
if ($any_observe) {
return self::IO_READ;
} else {
return self::IO_READWRITE;
}
}
return self::IO_IGNORE;
}
public function getDisplayURI() {
$uri = new PhutilURI($this->getURI());
$protocol = $this->getForcedProtocol();
if ($protocol) {
$uri->setProtocol($protocol);
}
$user = $this->getForcedUser();
if ($user) {
$uri->setUser($user);
}
$host = $this->getForcedHost();
if ($host) {
$uri->setDomain($host);
}
$port = $this->getForcedPort();
if ($port) {
$uri->setPort($port);
}
$path = $this->getForcedPath();
if ($path) {
$uri->setPath($path);
}
return $uri;
}
private function getForcedProtocol() {
switch ($this->getBuiltinProtocol()) {
case self::BUILTIN_PROTOCOL_SSH:
return 'ssh';
case self::BUILTIN_PROTOCOL_HTTP:
return 'http';
case self::BUILTIN_PROTOCOL_HTTPS:
return 'https';
default:
return null;
}
}
private function getForcedUser() {
switch ($this->getBuiltinProtocol()) {
case self::BUILTIN_PROTOCOL_SSH:
return AlmanacKeys::getClusterSSHUser();
default:
return null;
}
}
private function getForcedHost() {
$phabricator_uri = PhabricatorEnv::getURI('/');
$phabricator_uri = new PhutilURI($phabricator_uri);
$phabricator_host = $phabricator_uri->getDomain();
switch ($this->getBuiltinProtocol()) {
case self::BUILTIN_PROTOCOL_SSH:
$ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host');
if ($ssh_host !== null) {
return $ssh_host;
}
return $phabricator_host;
case self::BUILTIN_PROTOCOL_HTTP:
case self::BUILTIN_PROTOCOL_HTTPS:
return $phabricator_host;
default:
return null;
}
}
private function getForcedPort() {
switch ($this->getBuiltinProtocol()) {
case self::BUILTIN_PROTOCOL_SSH:
return PhabricatorEnv::getEnvConfig('diffusion.ssh-port');
case self::BUILTIN_PROTOCOL_HTTP:
case self::BUILTIN_PROTOCOL_HTTPS:
default:
return null;
}
}
private function getForcedPath() {
if (!$this->isBuiltin()) {
return null;
}
$repository = $this->getRepository();
$id = $repository->getID();
$callsign = $repository->getCallsign();
$short_name = $repository->getRepositorySlug();
$clone_name = $repository->getCloneName();
if ($repository->isGit()) {
$suffix = '.git';
} else if ($repository->isHg()) {
$suffix = '/';
} else {
$suffix = '';
}
switch ($this->getBuiltinIdentifier()) {
case self::BUILTIN_IDENTIFIER_ID:
return "/diffusion/{$id}/{$clone_name}{$suffix}";
case self::BUILTIN_IDENTIFIER_SHORTNAME:
return "/source/{$short_name}{$suffix}";
case self::BUILTIN_IDENTIFIER_CALLSIGN:
return "/diffusion/{$callsign}/{$clone_name}{$suffix}";
default:
return null;
}
}
}

View file

@ -7,6 +7,7 @@ final class PhabricatorRepositoryWorkingCopyVersion
protected $devicePHID; protected $devicePHID;
protected $repositoryVersion; protected $repositoryVersion;
protected $isWriting; protected $isWriting;
protected $writeProperties;
protected function getConfiguration() { protected function getConfiguration() {
return array( return array(
@ -14,6 +15,7 @@ final class PhabricatorRepositoryWorkingCopyVersion
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'repositoryVersion' => 'uint32', 'repositoryVersion' => 'uint32',
'isWriting' => 'bool', 'isWriting' => 'bool',
'writeProperties' => 'text?',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_workingcopy' => array( 'key_workingcopy' => array(
@ -66,7 +68,10 @@ final class PhabricatorRepositoryWorkingCopyVersion
* lock is released by default. This is a durable lock which stays locked * lock is released by default. This is a durable lock which stays locked
* by default. * by default.
*/ */
public static function willWrite($repository_phid, $device_phid) { public static function willWrite(
$repository_phid,
$device_phid,
array $write_properties) {
$version = new self(); $version = new self();
$conn_w = $version->establishConnection('w'); $conn_w = $version->establishConnection('w');
$table = $version->getTableName(); $table = $version->getTableName();
@ -74,16 +79,19 @@ final class PhabricatorRepositoryWorkingCopyVersion
queryfx( queryfx(
$conn_w, $conn_w,
'INSERT INTO %T 'INSERT INTO %T
(repositoryPHID, devicePHID, repositoryVersion, isWriting) (repositoryPHID, devicePHID, repositoryVersion, isWriting,
writeProperties)
VALUES VALUES
(%s, %s, %d, %d) (%s, %s, %d, %d, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
isWriting = VALUES(isWriting)', isWriting = VALUES(isWriting),
writeProperties = VALUES(writeProperties)',
$table, $table,
$repository_phid, $repository_phid,
$device_phid, $device_phid,
0,
1, 1,
1); phutil_json_encode($write_properties));
} }
@ -101,7 +109,9 @@ final class PhabricatorRepositoryWorkingCopyVersion
queryfx( queryfx(
$conn_w, $conn_w,
'UPDATE %T SET repositoryVersion = %d, isWriting = 0 'UPDATE %T SET
repositoryVersion = %d,
isWriting = 0
WHERE WHERE
repositoryPHID = %s AND repositoryPHID = %s AND
devicePHID = %s AND devicePHID = %s AND
@ -122,6 +132,7 @@ final class PhabricatorRepositoryWorkingCopyVersion
$repository_phid, $repository_phid,
$device_phid, $device_phid,
$new_version) { $new_version) {
$version = new self(); $version = new self();
$conn_w = $version->establishConnection('w'); $conn_w = $version->establishConnection('w');
$table = $version->getTableName(); $table = $version->getTableName();
@ -142,4 +153,23 @@ final class PhabricatorRepositoryWorkingCopyVersion
} }
/**
* Explicitly demote a device.
*/
public static function demoteDevice(
$repository_phid,
$device_phid) {
$version = new self();
$conn_w = $version->establishConnection('w');
$table = $version->getTableName();
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryPHID = %s AND devicePHID = %s',
$table,
$repository_phid,
$device_phid);
}
} }

View file

@ -147,7 +147,8 @@ final class PhabricatorRepositoryTestCase
); );
foreach ($map as $input => $expect) { foreach ($map as $input => $expect) {
$actual = PhabricatorRepository::filterMercurialDebugOutput($input); $actual = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
$input);
$this->assertEqual($expect, $actual, $input); $this->assertEqual($expect, $actual, $input);
} }
} }

View file

@ -23,6 +23,7 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker
)), )),
'diffusion.querycommits', 'diffusion.querycommits',
array( array(
'repositoryPHID' => $repository->getPHID(),
'phids' => array($commit->getPHID()), 'phids' => array($commit->getPHID()),
'bypassCache' => true, 'bypassCache' => true,
'needMessages' => true, 'needMessages' => true,

View file

@ -37,7 +37,10 @@ abstract class PhabricatorSearchEngineAPIMethod
} }
public function getMethodStatusDescription() { public function getMethodStatusDescription() {
return pht('ApplicationSearch methods are highly unstable.'); return pht(
'ApplicationSearch methods are fairly stable, but were introduced '.
'relatively recently and may continue to evolve as more applications '.
'adopt them.');
} }
final protected function defineParamTypes() { final protected function defineParamTypes() {

View file

@ -152,7 +152,7 @@ final class PhabricatorSlowvoteEditController
->setValue($v_description)) ->setValue($v_description))
->appendControl( ->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects')) ->setLabel(pht('Tags'))
->setName('projects') ->setName('projects')
->setValue($v_projects) ->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource())); ->setDatasource(new PhabricatorProjectDatasource()));

View file

@ -16,7 +16,10 @@ abstract class PhabricatorEditEngineAPIMethod
} }
public function getMethodStatusDescription() { public function getMethodStatusDescription() {
return pht('ApplicationEditor methods are highly unstable.'); return pht(
'ApplicationEditor methods are fairly stable, but were introduced '.
'relatively recently and may continue to evolve as more applications '.
'adopt them.');
} }
final protected function defineParamTypes() { final protected function defineParamTypes() {

View file

@ -99,8 +99,15 @@ class PhabricatorApplicationTransactionFeedStory
} }
} }
$view->setImage( $author_phid = $xaction->getAuthorPHID();
$this->getHandle($xaction->getAuthorPHID())->getImageURI()); $author_handle = $this->getHandle($author_phid);
$author_image = $author_handle->getImageURI();
if ($author_image) {
$view->setImage($author_image);
} else {
$view->setAuthorIcon($author_handle->getIcon());
}
return $view; return $view;
} }

View file

@ -1227,7 +1227,17 @@ abstract class PhabricatorApplicationTransaction
// Make this weaker than TYPE_COMMENT. // Make this weaker than TYPE_COMMENT.
return 0.25; return 0.25;
} }
break;
if ($this->isApplicationAuthor()) {
// When applications (most often: Herald) change subscriptions it
// is very uninteresting.
return 0.000000001;
}
// In other cases, subscriptions are more interesting than comments
// (which are shown anyway) but less interesting than any other type of
// transaction.
return 0.75;
} }
return 1.0; return 1.0;
@ -1462,6 +1472,14 @@ abstract class PhabricatorApplicationTransaction
return true; return true;
} }
private function isApplicationAuthor() {
$author_phid = $this->getAuthorPHID();
$author_type = phid_get_type($author_phid);
$application_type = PhabricatorApplicationApplicationPHIDType::TYPECONST;
return ($author_type == $application_type);
}
private function getInterestingMoves(array $moves) { private function getInterestingMoves(array $moves) {
// Remove moves which only shift the position of a task within a column. // Remove moves which only shift the position of a task within a column.
foreach ($moves as $key => $move) { foreach ($moves as $key => $move) {

View file

@ -1,5 +1,5 @@
@title Cluster: Daemons @title Cluster: Daemons
@group intro @group cluster
Configuring Phabricator to use multiple daemon hosts. Configuring Phabricator to use multiple daemon hosts.

View file

@ -1,36 +1,81 @@
@title Cluster: Databases @title Cluster: Databases
@group intro @group cluster
Configuring Phabricator to use multiple database hosts. Configuring Phabricator to use multiple database hosts.
Overview Overview
======== ========
WARNING: This feature is a very early prototype; the features this document
describes are mostly speculative fantasy.
You can deploy Phabricator with multiple database hosts, configured as a master You can deploy Phabricator with multiple database hosts, configured as a master
and a set of replicas. The advantages of doing this are: and a set of replicas. The advantages of doing this are:
- faster recovery from disasters by promoting a replica; - faster recovery from disasters by promoting a replica;
- graceful degradation if the master fails; - graceful degradation if the master fails; and
- reduced load on the master; and
- some tools to help monitor and manage replica health. - some tools to help monitor and manage replica health.
This configuration is complex, and many installs do not need to pursue it. This configuration is complex, and many installs do not need to pursue it.
Phabricator can not currently be configured into a multi-master mode, nor can
it be configured to automatically promote a replica to become the new master.
If you lose the master, Phabricator can degrade automatically into read-only If you lose the master, Phabricator can degrade automatically into read-only
mode and remain available, but can not fully recover without operational mode and remain available, but can not fully recover without operational
intervention unless the master recovers on its own. intervention unless the master recovers on its own.
Phabricator will not currently send read traffic to replicas unless the master
has failed, so configuring a replica will not currently spread any load away
from the master. Future versions of Phabricator are expected to be able to
distribute some read traffic to replicas.
Phabricator can not currently be configured into a multi-master mode, nor can
it be configured to automatically promote a replica to become the new master.
There are no current plans to support multi-master mode or autonomous failover,
although this may change in the future.
Setting up MySQL Replication Setting up MySQL Replication
============================ ============================
TODO: Write this section. To begin, set up a replica database server and configure MySQL replication.
If you aren't sure how to do this, refer to the MySQL manual for instructions.
The MySQL documentation is comprehensive and walks through the steps and
options in good detail. You should understand MySQL replication before
deploying it in production: Phabricator layers on top of it, and does not
attempt to abstract it away.
Some useful notes for configuring replication for Phabricator:
**Binlog Format**: Phabricator issues some queries which MySQL will detect as
unsafe if you use the `STATEMENT` binlog format (the default). Instead, use
`MIXED` (recommended) or `ROW` as the `binlog_format`.
**Grant `REPLICATION CLIENT` Privilege**: If you give the user that Phabricator
will use to connect to the replica database server the `REPLICATION CLIENT`
privilege, Phabricator's status console can give you more information about
replica health and state.
**Copying Data to Replicas**: Phabricator currently uses a mixture of MyISAM
and InnoDB tables, so it can be difficult to guarantee that a dump is wholly
consistent and suitable for loading into a replica because MySQL uses different
consistency mechanisms for the different storage engines.
An approach you may want to consider to limit downtime but still produce a
consistent dump is to leave Phabricator running but configured in read-only
mode while dumping:
- Stop all the daemons.
- Set `cluster.read-only` to `true` and deploy the new configuration. The
web UI should now show that Phabricator is in "Read Only" mode.
- Dump the database. You can do this with `bin/storage dump --for-replica`
to add the `--master-data` flag to the underlying command and include a
`CHANGE MASTER ...` statement in the dump.
- Once the dump finishes, turn `cluster.read-only` off again to restore
service. Continue loading the dump into the replica normally.
**Log Expiration**: You can configure MySQL to automatically clean up old
binary logs on startup with the `expire_logs_days` option. If you do not
configure this and do not explicitly purge old logs with `PURGE BINARY LOGS`,
the binary logs on disk will grow unboundedly and relatively quickly.
Once you have a working replica, continue below to tell Phabricator about it.
Configuring Replicas Configuring Replicas
@ -207,7 +252,38 @@ the new master. See the next section, "Promoting a Replica", for details.
Promoting a Replica Promoting a Replica
=================== ===================
TODO: Write this section. If you lose access to the master database, Phabricator will degrade into
read-only mode. This is described in greater detail below.
The easiest way to get out of read-only mode is to restore the master database.
If the database recovers on its own or operations staff can revive it,
Phabricator will return to full working order after a few moments.
If you can't restore the master or are unsure you will be able to restore the
master quickly, you can promote a replica to become the new master instead.
Before doing this, you should first assess how far behind the master the
replica was when the link died. Any data which was not replicated will either
be lost or become very difficult to recover after you promote a replica.
For example, if some `T1234` had been created on the master but had not yet
replicated and you promote the replica, a new `T1234` may be created on the
replica after promotion. Even if you can recover the master later, merging
the data will be difficult because each database may have conflicting changes
which can not be merged easily.
If there was a significant replication delay at the time of the failure, you
may wait to try harder or spend more time attempting to recover the master
before choosing to promote.
If you have made a choice to promote, disable replication on the replica and
mark it as the `master` in `cluster.databases`. Remove the original master and
deploy the configuration change to all surviving hosts.
Once write service is restored, you should provision, deploy, and configure a
new replica by following the steps you took the first time around. You are
critically vulnerable to a second disruption until you have restored the
redundancy.
Unreachable Masters Unreachable Masters

View file

@ -1,5 +1,5 @@
@title Cluster: Notifications @title Cluster: Notifications
@group intro @group cluster
Configuring Phabricator to use multiple notification servers. Configuring Phabricator to use multiple notification servers.

View file

@ -1,5 +1,5 @@
@title Cluster: Repositories @title Cluster: Repositories
@group intro @group cluster
Configuring Phabricator to use multiple repository hosts. Configuring Phabricator to use multiple repository hosts.
@ -19,19 +19,19 @@ advantages of doing this are:
This configuration is complex, and many installs do not need to pursue it. This configuration is complex, and many installs do not need to pursue it.
This configuration is not currently supported with Subversion. This configuration is not currently supported with Subversion or Mercurial.
Repository Hosts Repository Hosts
================ ================
Repository hosts must run a complete, fully configured copy of Phabricator, Repository hosts must run a complete, fully configured copy of Phabricator,
including a webserver. If you make repositories available over SSH, they must including a webserver. They must also run a properly configured `sshd`.
also run a properly configured `sshd`.
Generally, these hosts will run the same set of services and configuration that 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 web hosts run. If you prefer, you can overlay these services and put web and
repository services on the same hosts. 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 When a user requests information about a repository that can only be satisfied
by examining a repository working copy, the webserver receiving the request by examining a repository working copy, the webserver receiving the request
@ -57,6 +57,17 @@ If it isn't, they block the read until they can complete a fetch.
Before responding to a write, replicas obtain a global lock, perform the same Before responding to a write, replicas obtain a global lock, perform the same
version check and fetch if necessary, then allow the write to continue. version check and fetch if necessary, then allow the write to continue.
Additionally, repositories passively check other nodes for updates and
replicate changes in the background. After you push a change to a repositroy,
it will usually spread passively to all other repository nodes within a few
minutes.
Even if passive replication is slow, the active replication makes acknowledged
changes sequential to all observers: after a write is acknowledged, all
subsequent reads are guaranteed to see it. The system does not permit stale
reads, and you do not need to wait for a replication delay to see a consistent
view of the repository no matter which node you ask.
HTTP vs HTTPS HTTP vs HTTPS
============= =============
@ -84,6 +95,237 @@ 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. similar agents of other rogue nations is beyond the scope of this document.
Monitoring Services
===================
You can get an overview of repository cluster status from the
{nav Config > Repository Servers} screen. This table shows a high-level
overview of all active repository services.
**Repos**: The number of repositories hosted on this service.
**Sync**: Synchronization status of repositories on this service. This is an
at-a-glance view of service health, and can show these values:
- **Synchronized**: All nodes are fully synchronized and have the latest
version of all repositories.
- **Partial**: All repositories either have at least two leaders, or have
a very recent write which is not expected to have propagated yet.
- **Unsynchronized**: At least one repository has changes which are
only available on one node and were not pushed very recently. Data may
be at risk.
- **No Repositories**: This service has no repositories.
- **Ambiguous Leader**: At least one repository has an ambiguous leader.
If this screen identifies problems, you can drill down into repository details
to get more information about them. See the next section for details.
Monitoring Repositories
=======================
You can get a more detailed view the current status of a specific repository on
cluster devices in {nav Diffusion > (Repository) > Manage Repository > Cluster
Configuration}.
This screen shows all the configured devices which are hosting the repository
and the available version on that device.
**Version**: When a repository is mutated by a push, Phabricator increases
an internal version number for the repository. This column shows which version
is on disk on the corresponding device.
After a change is pushed, the device which received the change will have a
larger version number than the other devices. The change should be passively
replicated to the remaining devices after a brief period of time, although this
can take a while if the change was large or the network connection between
devices is slow or unreliable.
You can click the version number to see the corresponding push logs for that
change. The logs contain details about what was changed, and can help you
identify if replication is slow because a change is large or for some other
reason.
**Writing**: This shows that the device is currently holding a write lock. This
normally means that it is actively receiving a push, but can also mean that
there was a write interruption. See "Write Interruptions" below for details.
**Last Writer**: This column identifies the user who most recently pushed a
change to this device. If the write lock is currently held, this user is
the user whose change is holding the lock.
**Last Write At**: When the most recent write started. If the write lock is
currently held, this shows when the lock was acquired.
Cluster Failure Modes
=====================
There are three major cluster failure modes:
- **Write Interruptions**: A write started but did not complete, leaving
the disk state and cluster state out of sync.
- **Loss of Leaders**: None of the devices with the most up-to-date data
are reachable.
- **Ambiguous Leaders**: The internal state of the repository is unclear.
Phabricator can detect these issues, and responds by freezing the repository
(usually preventing all reads and writes) until the issue is resolved. These
conditions are normally rare and very little data is at risk, but Phabricator
errs on the side of caution and requires decisions which may result in data
loss to be confirmed by a human.
The next sections cover these failure modes and appropriate responses in
more detail. In general, you will respond to these issues by assessing the
situation and then possibly choosing to discard some data.
Write Interruptions
===================
A repository cluster can be put into an inconsistent state by an interruption
in a brief window during and immediately after a write. This looks like this:
- A change is pushed to a server.
- The server acquires a write lock and begins writing the change.
- During or immediately after the write, lightning strikes the server
and destroys it.
Phabricator can not commit changes to a working copy (stored on disk) and to
the global state (stored in a database) atomically, so there is necessarily a
narrow window between committing these two different states when some tragedy
can befall a server, leaving the global and local views of the repository state
possibly divergent.
In these cases, Phabricator fails into a frozen state where further writes
are not permitted until the failure is investigated and resolved. When a
repository is frozen in this way it remains readable.
You can use the monitoring console to review the state of a frozen repository
with a held write lock. The **Writing** column will show which device is
holding the lock, and whoever is named in the **Last Writer** column may be
able to help you figure out what happened by providing more information about
what they were doing and what they observed.
Because the push was not acknowledged, it is normally safe to resolve this
issue by demoting the device. Demoting the device will undo any changes
committed by the push, and they will be lost forever.
However, the user should have received an error anyway, and should not expect
their push to have worked. Still, data is technically at risk and you may want
to investigate further and try to understand the issue in more detail before
continuing.
There is no way to explicitly keep the write, but if it was committed to disk
you can recover it manually from the working copy on the device (for example,
by using `git format-patch`) and then push it again after recovering.
If you demote the device, the in-process write will be thrown away, even if it
was complete on disk. To demote the device and release the write lock, run this
command:
```
phabricator/ $ ./bin/repository thaw <repository> --demote <device>
```
{icon exclamation-triangle, color="yellow"} Any committed but unacknowledged
data on the device will be lost.
Loss of Leaders
===============
A more straightforward failure condition is the loss of all servers in a
cluster which have the most up-to-date copy of a repository. This looks like
this:
- There is a cluster setup with two devices, X and Y.
- A new change is pushed to server X.
- Before the change can propagate to server Y, lightning strikes server X
and destroys it.
Here, all of the "leader" devices with the most up-to-date copy of the
repository have been lost. Phabricator will freeze the repository refuse to
serve requests because it can not serve it consistently, and can not accept new
writes without data loss.
The most straightforward way to resolve this issue is to restore any leader to
service. The change will be able to replicate to other devices once a leader
comes back online.
If you are unable to restore a leader or unsure that you can restore one
quickly, you can use the monitoring console to review which changes are
present on the leaders but not present on the followers by examining the
push logs.
If you are comfortable discarding these changes, you can instruct Phabricator
that it can forget about the leaders in two ways: disable the service bindings
to all of the leader devices so they are no longer part of the cluster, or use
`bin/repository thaw` to `--demote` the leaders explicitly.
If you do this, **you will lose data**. Either action will discard any changes
on the affected leaders which have not replicated to other devices in the
cluster.
To remove a device from the cluster, disable all of the bindings to it
in Almanac, using the web UI.
{icon exclamation-triangle, color="red"} Any data which is only present on
the disabled device will be lost.
To demote a device without removing it from the cluster, run this command:
```
phabricator/ $ ./bin/repository thaw rXYZ --demote repo002.corp.net
```
{icon exclamation-triangle, color="red"} Any data which is only present on
**this** device will be lost.
Ambiguous Leaders
=================
Repository clusters can also freeze if the leader devices are ambiguous. This
can happen if you replace an entire cluster with new devices suddenly, or
make a mistake with the `--demote` flag. This generally arises from some kind
of operator error, like this:
- Someone accidentally uses `bin/repository thaw ... --demote` to demote
every device in a cluster.
- Someone accidentally deletes all the version information for a repository
from the database by making a mistake with a `DELETE` or `UPDATE` query.
- Someone accidentally disable all of the devices in a cluster, then add
entirely new ones before repositories can propagate.
When Phabricator can not tell which device in a cluster is a leader, it freezes
the cluster because it is possible that some devices have less data and others
have more, and if it choses a leader arbitrarily it may destroy some data
which you would prefer to retain.
To resolve this, you need to tell Phabricator which device has the most
up-to-date data and promote that device to become a leader. If you know all
devices have the same data, you are free to promote any device.
If you promote a device, **you may lose data** if you promote the wrong device
and some other device really had more up-to-date data. If you want to double
check, you can examine the working copies on disk before promoting by
connecting to the machines and using commands like `git log` to inspect state.
Once you have identified a device which has data you're happy with, use
`bin/repository thaw` to `--promote` the device. The data on the chosen
device will become authoritative:
```
phabricator/ $ ./bin/repository thaw rXYZ --promote repo002.corp.net
```
{icon exclamation-triangle, color="red"} Any data which is only present on
**other** devices will be lost.
Backups Backups
====== ======

View file

@ -1,5 +1,5 @@
@title Cluster: Web Servers @title Cluster: Web Servers
@group intro @group cluster
Configuring Phabricator to use multiple web servers. Configuring Phabricator to use multiple web servers.

View file

@ -65,6 +65,13 @@ Continue to "Configuring Phabricator", below.
Approach: CloudFlare Approach: CloudFlare
======== ========
WARNING: You should review all your CloudFlare settings, and be very
sure to turn off all JavaScript, HTML, CSS minification and
optimization features, including systems like "Rocket Loader". These
features will break Phabricator in strange and mysterious ways that
are unpredictable. Only allow CloudFlare to cache files, and never
optimize them.
[[ https://cloudflare.com | CloudFlare ]] is a general-purpose CDN service. [[ https://cloudflare.com | CloudFlare ]] is a general-purpose CDN service.
To set up CloudFlare, you'll need to register a second domain and go through To set up CloudFlare, you'll need to register a second domain and go through

View file

@ -0,0 +1,47 @@
@title Diffusion User Guide: URIs
@group userguide
Guide to configuring repository URIs for fetching, cloning and mirroring.
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:
**Host Repositories**: Phabricator can host repositories locally. Phabricator
maintains the writable master version of the repository, and you can push and
pull the repository. This is the most straightforward kind of repository
configuration, and similar to repositories on other services like GitHub or
Bitbucket.
**Observe Repositories**: Phabricator can create a copy of an repository which
is hosted elsewhere (like GitHub or Bitbucket) and track updates to the remote
repository. This will create a read-only copy of the repository in Phabricator.
**Mirror Repositories**: Phabricator can publish any repository to mirrors,
updating the mirrors as changes are made to the repository. This works with
both local hosted repositories and remote repositories that Phabricator is
observing.
**Proxy Repositories**: If you are observing a repository, you can allow users
to read Phabricator's copy of the repository. Phabricator supports granular
read permissions, so this can let you open a private repository up a little
bit in a flexible way.
**Import Repositories**: If you have a repository elsewhere that you want to
host on Phabricator, you can observe the remote repository first, then turn
the tracking off once the repository fully synchronizes. This allows you to
copy an existing repository and begin hosting it in Phabricator.
You can also import repositories by creating an empty hosted repository and
then pushing everything to the repository directly.
You configure the behavior of a Phabricator repository by adding and
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.

Some files were not shown because too many files have changed in this diff Show more