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

(stable) Promote 2015 Week 42

This commit is contained in:
epriestley 2015-10-17 04:09:42 -07:00
commit 064d9a9bfd
111 changed files with 3533 additions and 329 deletions

View file

@ -7,7 +7,7 @@
*/
return array(
'names' => array(
'core.pkg.css' => 'a11c3643',
'core.pkg.css' => 'c65b251d',
'core.pkg.js' => '47dc9ebb',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => '2de124c9',
@ -67,7 +67,7 @@ return array(
'rsrc/css/application/differential/table-of-contents.css' => 'ae4b7a55',
'rsrc/css/application/diffusion/diffusion-icons.css' => '2941baf1',
'rsrc/css/application/diffusion/diffusion-readme.css' => '2106ea08',
'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661',
'rsrc/css/application/diffusion/diffusion-source.css' => '075ba788',
'rsrc/css/application/feed/feed.css' => 'ecd4ec57',
'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad',
'rsrc/css/application/flag/flag.css' => '5337623f',
@ -147,7 +147,7 @@ return array(
'rsrc/css/phui/phui-status.css' => '888cedb8',
'rsrc/css/phui/phui-tag-view.css' => '402691cc',
'rsrc/css/phui/phui-text.css' => 'cf019f54',
'rsrc/css/phui/phui-timeline-view.css' => 'f1bccf73',
'rsrc/css/phui/phui-timeline-view.css' => '2efceff8',
'rsrc/css/phui/phui-two-column-view.css' => '39ecafb1',
'rsrc/css/phui/phui-workboard-view.css' => '6704d68d',
'rsrc/css/phui/phui-workpanel-view.css' => 'adec7699',
@ -521,7 +521,7 @@ return array(
'differential-table-of-contents-css' => 'ae4b7a55',
'diffusion-icons-css' => '2941baf1',
'diffusion-readme-css' => '2106ea08',
'diffusion-source-css' => '66fdf661',
'diffusion-source-css' => '075ba788',
'diviner-shared-css' => '5a337049',
'font-fontawesome' => 'd2fc4e8d',
'font-lato' => '5ab1a46a',
@ -797,7 +797,7 @@ return array(
'phui-tag-view-css' => '402691cc',
'phui-text-css' => 'cf019f54',
'phui-theme-css' => '6b451f24',
'phui-timeline-view-css' => 'f1bccf73',
'phui-timeline-view-css' => '2efceff8',
'phui-two-column-view-css' => '39ecafb1',
'phui-workboard-view-css' => '6704d68d',
'phui-workpanel-view-css' => 'adec7699',

View file

@ -24,12 +24,14 @@ foreach (new LiskMigrationIterator($table) as $doc) {
$prefix = 'projects/';
if (($slug != $prefix) && (strncmp($slug, $prefix, strlen($prefix)) === 0)) {
$parts = explode('/', $slug);
$project_slug = $parts[1].'/';
$project_slug = $parts[1];
$project_slug = PhabricatorSlug::normalizeProjectSlug($project_slug);
$project_slugs = array($project_slug);
$project = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPhrictionSlugs($project_slugs)
->withSlugs($project_slugs)
->executeOne();
if ($project) {

View file

@ -0,0 +1,14 @@
CREATE TABLE {$NAMESPACE}_drydock.drydock_authorization (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
blueprintPHID VARBINARY(64) NOT NULL,
blueprintAuthorizationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
objectPHID VARBINARY(64) NOT NULL,
objectAuthorizationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (phid),
UNIQUE KEY `key_unique` (objectPHID, blueprintPHID),
KEY `key_blueprint` (blueprintPHID, blueprintAuthorizationState),
KEY `key_object` (objectPHID, objectAuthorizationState)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_drydock.drydock_lease
ADD authorizingPHID VARBINARY(64) NOT NULL;

View file

@ -0,0 +1,16 @@
CREATE TABLE {$NAMESPACE}_drydock.drydock_repositoryoperation (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
repositoryPHID VARBINARY(64) NOT NULL,
repositoryTarget LONGBLOB NOT NULL,
operationType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
operationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (phid),
KEY `key_object` (objectPHID),
KEY `key_repository` (repositoryPHID, operationState)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -10,13 +10,14 @@ $projects = $table->loadAll();
$slug_map = array();
foreach ($projects as $project) {
$project->setPhrictionSlug($project->getName());
$slug = $project->getPhrictionSlug();
if ($slug == '/') {
$slug = PhabricatorSlug::normalizeProjectSlug($project->getName());
if (!strlen($slug)) {
$project_id = $project->getID();
echo pht("Project #%d doesn't have a meaningful name...", $project_id)."\n";
$project->setName(trim(pht('Unnamed Project %s', $project->getName())));
}
$slug_map[$slug][] = $project->getID();
}
@ -47,8 +48,8 @@ while ($update) {
foreach ($update as $key => $project) {
$id = $project->getID();
$name = $project->getName();
$project->setPhrictionSlug($name);
$slug = $project->getPhrictionSlug();
$slug = PhabricatorSlug::normalizeProjectSlug($name).'/';
echo pht("Updating project #%d '%s' (%s)... ", $id, $name, $slug);
try {
@ -87,8 +88,8 @@ function rename_project($project, $projects) {
$suffix = 2;
while (true) {
$new_name = $project->getName().' ('.$suffix.')';
$project->setPhrictionSlug($new_name);
$new_slug = $project->getPhrictionSlug();
$new_slug = PhabricatorSlug::normalizeProjectSlug($new_name).'/';
$okay = true;
foreach ($projects as $other) {

View file

@ -467,6 +467,7 @@ phutil_register_library_map(array(
'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php',
'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php',
'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php',
'DifferentialRevisionOperationController' => 'applications/differential/controller/DifferentialRevisionOperationController.php',
'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php',
'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php',
'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php',
@ -607,6 +608,7 @@ phutil_register_library_map(array(
'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php',
'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php',
'DiffusionLowLevelMercurialPathsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php',
'DiffusionLowLevelMercurialPathsQueryTests' => 'applications/diffusion/query/lowlevel/__tests__/DiffusionLowLevelMercurialPathsQueryTests.php',
'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php',
'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php',
'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php',
@ -618,6 +620,7 @@ phutil_register_library_map(array(
'DiffusionMercurialServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php',
'DiffusionMercurialWireClientSSHProtocolChannel' => 'applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php',
'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php',
'DiffusionMercurialWireProtocolTests' => 'applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php',
'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php',
'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php',
'DiffusionMirrorDeleteController' => 'applications/diffusion/controller/DiffusionMirrorDeleteController.php',
@ -686,6 +689,7 @@ phutil_register_library_map(array(
'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php',
'DiffusionRepositoryEditActionsController' => 'applications/diffusion/controller/DiffusionRepositoryEditActionsController.php',
'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php',
'DiffusionRepositoryEditAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryEditAutomationController.php',
'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php',
'DiffusionRepositoryEditBranchesController' => 'applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php',
'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php',
@ -798,6 +802,14 @@ phutil_register_library_map(array(
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php',
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
'DrydockAuthorization' => 'applications/drydock/storage/DrydockAuthorization.php',
'DrydockAuthorizationAuthorizeController' => 'applications/drydock/controller/DrydockAuthorizationAuthorizeController.php',
'DrydockAuthorizationListController' => 'applications/drydock/controller/DrydockAuthorizationListController.php',
'DrydockAuthorizationListView' => 'applications/drydock/view/DrydockAuthorizationListView.php',
'DrydockAuthorizationPHIDType' => 'applications/drydock/phid/DrydockAuthorizationPHIDType.php',
'DrydockAuthorizationQuery' => 'applications/drydock/query/DrydockAuthorizationQuery.php',
'DrydockAuthorizationSearchEngine' => 'applications/drydock/query/DrydockAuthorizationSearchEngine.php',
'DrydockAuthorizationViewController' => 'applications/drydock/controller/DrydockAuthorizationViewController.php',
'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php',
'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php',
'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php',
@ -828,6 +840,7 @@ phutil_register_library_map(array(
'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php',
'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php',
'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php',
'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php',
'DrydockLease' => 'applications/drydock/storage/DrydockLease.php',
'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php',
'DrydockLeaseActivatedLogType' => 'applications/drydock/logtype/DrydockLeaseActivatedLogType.php',
@ -838,6 +851,8 @@ phutil_register_library_map(array(
'DrydockLeaseDestroyedLogType' => 'applications/drydock/logtype/DrydockLeaseDestroyedLogType.php',
'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php',
'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php',
'DrydockLeaseNoAuthorizationsLogType' => 'applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php',
'DrydockLeaseNoBlueprintsLogType' => 'applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php',
'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php',
'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php',
'DrydockLeaseQueuedLogType' => 'applications/drydock/logtype/DrydockLeaseQueuedLogType.php',
@ -863,7 +878,16 @@ phutil_register_library_map(array(
'DrydockManagementUpdateLeaseWorkflow' => 'applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php',
'DrydockManagementUpdateResourceWorkflow' => 'applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php',
'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
'DrydockObjectAuthorizationView' => 'applications/drydock/view/DrydockObjectAuthorizationView.php',
'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php',
'DrydockRepositoryOperation' => 'applications/drydock/storage/DrydockRepositoryOperation.php',
'DrydockRepositoryOperationListController' => 'applications/drydock/controller/DrydockRepositoryOperationListController.php',
'DrydockRepositoryOperationPHIDType' => 'applications/drydock/phid/DrydockRepositoryOperationPHIDType.php',
'DrydockRepositoryOperationQuery' => 'applications/drydock/query/DrydockRepositoryOperationQuery.php',
'DrydockRepositoryOperationSearchEngine' => 'applications/drydock/query/DrydockRepositoryOperationSearchEngine.php',
'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php',
'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php',
'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php',
'DrydockResource' => 'applications/drydock/storage/DrydockResource.php',
'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php',
'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php',
@ -1437,6 +1461,7 @@ phutil_register_library_map(array(
'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php',
'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php',
'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php',
'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php',
'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php',
'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php',
'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php',
@ -2945,6 +2970,7 @@ phutil_register_library_map(array(
'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php',
'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php',
'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php',
'PhabricatorStandardCustomFieldBlueprints' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php',
'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php',
'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php',
'PhabricatorStandardCustomFieldDatasource' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php',
@ -4190,6 +4216,7 @@ phutil_register_library_map(array(
'DifferentialRevisionListController' => 'DifferentialController',
'DifferentialRevisionListView' => 'AphrontView',
'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver',
'DifferentialRevisionOperationController' => 'DifferentialController',
'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType',
'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField',
@ -4330,6 +4357,7 @@ phutil_register_library_map(array(
'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialPathsQueryTests' => 'PhabricatorTestCase',
'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelQuery' => 'Phobject',
'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery',
@ -4341,6 +4369,7 @@ phutil_register_library_map(array(
'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow',
'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel',
'DiffusionMercurialWireProtocol' => 'Phobject',
'DiffusionMercurialWireProtocolTests' => 'PhabricatorTestCase',
'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase',
'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionMirrorDeleteController' => 'DiffusionController',
@ -4409,6 +4438,7 @@ phutil_register_library_map(array(
'DiffusionRepositoryDefaultController' => 'DiffusionController',
'DiffusionRepositoryEditActionsController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditAutomationController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditBranchesController' => 'DiffusionRepositoryEditController',
'DiffusionRepositoryEditController' => 'DiffusionController',
@ -4535,6 +4565,17 @@ phutil_register_library_map(array(
'DoorkeeperTagsController' => 'PhabricatorController',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
'DrydockAuthorization' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockAuthorizationAuthorizeController' => 'DrydockController',
'DrydockAuthorizationListController' => 'DrydockController',
'DrydockAuthorizationListView' => 'AphrontView',
'DrydockAuthorizationPHIDType' => 'PhabricatorPHIDType',
'DrydockAuthorizationQuery' => 'DrydockQuery',
'DrydockAuthorizationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockAuthorizationViewController' => 'DrydockController',
'DrydockBlueprint' => array(
'DrydockDAO',
'PhabricatorApplicationTransactionInterface',
@ -4576,6 +4617,7 @@ phutil_register_library_map(array(
'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DrydockFilesystemInterface' => 'DrydockInterface',
'DrydockInterface' => 'Phobject',
'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType',
'DrydockLease' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
@ -4589,6 +4631,8 @@ phutil_register_library_map(array(
'DrydockLeaseDestroyedLogType' => 'DrydockLogType',
'DrydockLeaseListController' => 'DrydockLeaseController',
'DrydockLeaseListView' => 'AphrontView',
'DrydockLeaseNoAuthorizationsLogType' => 'DrydockLogType',
'DrydockLeaseNoBlueprintsLogType' => 'DrydockLogType',
'DrydockLeasePHIDType' => 'PhabricatorPHIDType',
'DrydockLeaseQuery' => 'DrydockQuery',
'DrydockLeaseQueuedLogType' => 'DrydockLogType',
@ -4617,7 +4661,19 @@ phutil_register_library_map(array(
'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
'DrydockObjectAuthorizationView' => 'AphrontView',
'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DrydockRepositoryOperation' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockRepositoryOperationListController' => 'DrydockController',
'DrydockRepositoryOperationPHIDType' => 'PhabricatorPHIDType',
'DrydockRepositoryOperationQuery' => 'DrydockQuery',
'DrydockRepositoryOperationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockRepositoryOperationType' => 'Phobject',
'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker',
'DrydockRepositoryOperationViewController' => 'DrydockController',
'DrydockResource' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
@ -5301,6 +5357,7 @@ phutil_register_library_map(array(
'PHUIPropertyListExample' => 'PhabricatorUIExample',
'PHUIPropertyListView' => 'AphrontView',
'PHUIRemarkupPreviewPanel' => 'AphrontTagView',
'PHUIRemarkupView' => 'AphrontView',
'PHUISpacesNamespaceContextView' => 'AphrontView',
'PHUIStatusItemView' => 'AphrontTagView',
'PHUIStatusListView' => 'AphrontTagView',
@ -7089,6 +7146,7 @@ phutil_register_library_map(array(
'PhabricatorSpacesTestCase' => 'PhabricatorTestCase',
'PhabricatorSpacesViewController' => 'PhabricatorSpacesController',
'PhabricatorStandardCustomField' => 'PhabricatorCustomField',
'PhabricatorStandardCustomFieldBlueprints' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldDatasource' => 'PhabricatorStandardCustomFieldTokenizer',

View file

@ -372,6 +372,13 @@ abstract class AphrontApplicationConfiguration extends Phobject {
$result = $this->routePath($maps, $path.'/');
if ($result) {
$slash_uri = $request->getRequestURI()->setPath($path.'/');
// We need to restore URI encoding because the webserver has
// interpreted it. For example, this allows us to redirect a path
// like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be
// resolved meaningfully by an application.
$slash_uri = phutil_escape_uri($slash_uri);
$external = strlen($request->getRequestURI()->getDomain());
return $this->buildRedirectController($slash_uri, $external);
}

View file

@ -104,14 +104,10 @@ final class PhabricatorBadgesViewController
$description = $badge->getDescription();
if (strlen($description)) {
$description = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($description),
'default',
$viewer);
$view->addSectionHeader(
pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
$view->addTextContent($description);
$view->addTextContent(
new PHUIRemarkupView($viewer, $description));
}
$badge = id(new PHUIBadgeView())

View file

@ -195,15 +195,10 @@ final class PhabricatorConduitConsoleController
pht('Errors'),
$error_description);
$description = $method->getMethodDescription();
$description = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($description),
'default',
$viewer);
$view->addSectionHeader(
pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
$view->addTextContent($description);
$view->addTextContent(
new PHUIRemarkupView($viewer, $method->getMethodDescription()));
return $view;
}

View file

@ -45,8 +45,8 @@ final class PhabricatorPygmentSetupCheck extends PhabricatorSetupCheck {
'Phabricator has %s available in %s, but the binary '.
'exited with an error code when run as %s. Check that it is '.
'installed correctly.',
phutil_tag('tt', array(), '$PATH'),
phutil_tag('tt', array(), 'pygmentize'),
phutil_tag('tt', array(), '$PATH'),
phutil_tag('tt', array(), 'pygmentize -h'));
$this

View file

@ -75,6 +75,8 @@ final class PhabricatorDifferentialApplication extends PhabricatorApplication {
=> 'DifferentialRevisionCloseDetailsController',
'update/(?P<revisionID>[1-9]\d*)/'
=> 'DifferentialDiffCreateController',
'operation/(?P<id>[1-9]\d*)/'
=> 'DifferentialRevisionOperationController',
),
'comment/' => array(
'preview/(?P<id>[1-9]\d*)/' => 'DifferentialCommentPreviewController',

View file

@ -3,19 +3,13 @@
final class DifferentialCommentPreviewController
extends DifferentialController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->withIDs(array($id))
->executeOne();
if (!$revision) {
return new Aphront404Response();
@ -119,7 +113,7 @@ final class DifferentialCommentPreviewController
$metadata['action'] = $action;
}
$draft_key = 'differential-comment-'.$this->id;
$draft_key = 'differential-comment-'.$id;
$draft = id(new PhabricatorDraft())
->setAuthorPHID($viewer->getPHID())
->setDraftKey($draft_key)

View file

@ -3,15 +3,9 @@
final class DifferentialCommentSaveController
extends DifferentialController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
if (!$request->isFormPost()) {
return new Aphront400Response();
@ -19,7 +13,7 @@ final class DifferentialCommentSaveController
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->withIDs(array($id))
->needReviewerStatus(true)
->needReviewerAuthority(true)
->executeOne();

View file

@ -2,23 +2,17 @@
final class DifferentialDiffViewController extends DifferentialController {
private $id;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->withIDs(array($id))
->executeOne();
if (!$diff) {
return new Aphront404Response();

View file

@ -3,20 +3,11 @@
final class DifferentialRevisionCloseDetailsController
extends DifferentialController {
private $phid;
public function willProcessRequest(array $data) {
$this->phid = idx($data, 'phid');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$xaction_phid = $this->phid;
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$xaction = id(new PhabricatorObjectQuery())
->withPHIDs(array($xaction_phid))
->withPHIDs(array($request->getURIData('phid')))
->setViewer($viewer)
->executeOne();
if (!$xaction) {

View file

@ -3,24 +3,18 @@
final class DifferentialRevisionEditController
extends DifferentialController {
private $id;
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if (!$this->id) {
$this->id = $request->getInt('revisionID');
if (!$id) {
$id = $request->getInt('revisionID');
}
if ($this->id) {
if ($id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->withIDs(array($id))
->needRelationships(true)
->needReviewerStatus(true)
->needActiveDiffs(true)

View file

@ -11,9 +11,8 @@ final class DifferentialRevisionLandController extends DifferentialController {
$this->strategyClass = $data['strategy'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$revision_id = $this->revisionID;

View file

@ -2,19 +2,13 @@
final class DifferentialRevisionListController extends DifferentialController {
private $queryKey;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->queryKey = idx($data, 'queryKey');
}
public function processRequest() {
public function handleRequest(AphrontRequest $request) {
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($this->queryKey)
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine(new DifferentialRevisionSearchEngine())
->setNavigation($this->buildSideNavView());

View file

@ -0,0 +1,113 @@
<?php
final class DifferentialRevisionOperationController
extends DifferentialController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($id))
->setViewer($viewer)
->needActiveDiffs(true)
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
$detail_uri = "/D{$id}";
$repository = $revision->getRepository();
if (!$repository) {
return $this->rejectOperation(
$revision,
pht('No Repository'),
pht(
'This revision is not associated with a known repository. Only '.
'revisions associated with a tracked repository can be landed '.
'automatically.'));
}
if (!$repository->canPerformAutomation()) {
return $this->rejectOperation(
$revision,
pht('No Repository Automation'),
pht(
'The repository this revision is associated with ("%s") is not '.
'configured to support automation. Configure automation for the '.
'repository to enable revisions to be landed automatically.',
$repository->getMonogram()));
}
// TODO: At some point we should allow installs to give "land reviewed
// code" permission to more users than "push any commit", because it is
// a much less powerful operation. For now, just require push so this
// doesn't do anything users can't do on their own.
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
return $this->rejectOperation(
$revision,
pht('Unable to Push'),
pht(
'You do not have permission to push to the repository this '.
'revision is associated with ("%s"), so you can not land it.',
$repository->getMonogram()));
}
if ($request->isFormPost()) {
// NOTE: The operation is locked to the current active diff, so if the
// revision is updated before the operation applies nothing sneaky
// occurs.
$diff = $revision->getActiveDiff();
$op = new DrydockLandRepositoryOperation();
$operation = DrydockRepositoryOperation::initializeNewOperation($op)
->setAuthorPHID($viewer->getPHID())
->setObjectPHID($revision->getPHID())
->setRepositoryPHID($repository->getPHID())
->setRepositoryTarget('branch:master')
->setProperty('differential.diffPHID', $diff->getPHID());
$operation->save();
$operation->scheduleUpdate();
return id(new AphrontRedirectResponse())
->setURI($detail_uri);
}
return $this->newDialog()
->setTitle(pht('Land Revision'))
->appendParagraph(
pht(
'In theory, this will do approximately what `arc land` would do. '.
'In practice, that is almost certainly not what it will actually '.
'do.'))
->appendParagraph(
pht(
'THIS FEATURE IS EXPERIMENTAL AND DANGEROUS! USE IT AT YOUR '.
'OWN RISK!'))
->addCancelButton($detail_uri)
->addSubmitButton(pht('Mutate Repository Unpredictably'));
}
private function rejectOperation(
DifferentialRevision $revision,
$title,
$body) {
$id = $revision->getID();
$detail_uri = "/D{$id}";
return $this->newDialog()
->setTitle($title)
->appendParagraph($body)
->addCancelButton($detail_uri);
}
}

View file

@ -8,15 +8,11 @@ final class DifferentialRevisionViewController extends DifferentialController {
return true;
}
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->revisionID = $request->getURIData('id');
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer_is_anonymous = !$user->isLoggedIn();
$viewer_is_anonymous = !$viewer->isLoggedIn();
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($this->revisionID))
@ -68,7 +64,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
$repository = $revision->getRepository();
} else {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->setViewer($viewer)
->withPHIDs(array($repository_phid))
->executeOne();
}
@ -117,7 +113,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
$revision->loadCommitPHIDs(),
array(
$revision->getAuthorPHID(),
$user->getPHID(),
$viewer->getPHID(),
));
foreach ($revision->getAttached() as $type => $phids) {
@ -130,7 +126,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
$revision,
PhabricatorCustomField::ROLE_VIEW);
$field_list->setViewer($user);
$field_list->setViewer($viewer);
$field_list->readFieldsFromStorage($revision);
$warning_handle_map = array();
@ -174,7 +170,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
$new = array_select_keys($changesets, $new_ids);
$query = id(new DifferentialInlineCommentQuery())
->setViewer($user)
->setViewer($viewer)
->needHidden(true)
->withRevisionPHIDs(array($revision->getPHID()));
$inlines = $query->execute();
@ -205,7 +201,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
$commit_hashes = array_unique(array_filter($commit_hashes));
if ($commit_hashes) {
$commits_for_links = id(new DiffusionCommitQuery())
->setViewer($user)
->setViewer($viewer)
->withIdentifiers($commit_hashes)
->execute();
$commits_for_links = mpull(
@ -217,7 +213,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
}
$revision_detail = id(new DifferentialRevisionDetailView())
->setUser($user)
->setUser($viewer)
->setRevision($revision)
->setDiff(end($diffs))
->setCustomFields($field_list)
@ -239,7 +235,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
}
$revision_detail->setActions($actions);
$revision_detail->setUser($user);
$revision_detail->setUser($viewer);
$revision_detail_box = $revision_detail->render();
@ -261,7 +257,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
$detail_diffs = mpull($detail_diffs, null, 'getPHID');
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($user)
->setViewer($viewer)
->withBuildablePHIDs(array_keys($detail_diffs))
->withManualBuildables(false)
->needBuilds(true)
@ -311,7 +307,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
'/differential/changeset/?view=old',
'/differential/changeset/?view=new');
$changeset_view->setUser($user);
$changeset_view->setUser($viewer);
$changeset_view->setDiff($target);
$changeset_view->setRenderingReferences($rendering_references);
$changeset_view->setVsMap($vs_map);
@ -323,7 +319,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
$changeset_view->setTitle(pht('Diff %s', $target->getID()));
$diff_history = id(new DifferentialRevisionUpdateHistoryView())
->setUser($user)
->setUser($viewer)
->setDiffs($diffs)
->setSelectedVersusDiffID($diff_vs)
->setSelectedDiffID($target->getID())
@ -331,7 +327,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
->setCommitsForLinks($commits_for_links);
$local_view = id(new DifferentialLocalCommitsView())
->setUser($user)
->setUser($viewer)
->setLocalCommits(idx($props, 'local:commits'))
->setCommitsForLinks($commits_for_links);
@ -352,13 +348,13 @@ final class DifferentialRevisionViewController extends DifferentialController {
$toc_view = $this->buildTableOfContents(
$changesets,
$visible_changesets,
$target->loadCoverageMap($user));
$target->loadCoverageMap($viewer));
$comment_form = null;
if (!$viewer_is_anonymous) {
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$viewer->getPHID(),
'differential-comment-'.$revision->getID());
$reviewers = array();
@ -394,7 +390,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
'comment/save/'.$revision->getID().'/');
$comment_form->setActionURI($action_uri);
$comment_form->setUser($user);
$comment_form->setUser($viewer);
$comment_form->setDraft($draft);
$comment_form->setReviewers(mpull($reviewers, 'getFullName', 'getPHID'));
$comment_form->setCCs(mpull($ccs, 'getFullName', 'getPHID'));
@ -461,13 +457,16 @@ final class DifferentialRevisionViewController extends DifferentialController {
// TODO: For now, just use this to get "Login to Comment".
$page_pane->appendChild(
id(new PhabricatorApplicationTransactionCommentView())
->setUser($user)
->setUser($viewer)
->setRequestURI($request->getRequestURI()));
}
$object_id = 'D'.$revision->getID();
$operations_box = $this->buildOperationsBox($revision);
$content = array(
$operations_box,
$revision_detail_box,
$diff_detail_box,
$page_pane,
@ -476,7 +475,7 @@ final class DifferentialRevisionViewController extends DifferentialController {
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($object_id, '/'.$object_id);
$prefs = $user->loadPreferences();
$prefs = $viewer->loadPreferences();
$pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE;
if ($prefs->getPreference($pref_filetree)) {
@ -1036,4 +1035,55 @@ final class DifferentialRevisionViewController extends DifferentialController {
return $view;
}
private function buildOperationsBox(DifferentialRevision $revision) {
$viewer = $this->getViewer();
// Save a query if we can't possibly have pending operations.
$repository = $revision->getRepository();
if (!$repository || !$repository->canPerformAutomation()) {
return null;
}
$operations = id(new DrydockRepositoryOperationQuery())
->setViewer($viewer)
->withObjectPHIDs(array($revision->getPHID()))
->withOperationStates(
array(
DrydockRepositoryOperation::STATE_WAIT,
DrydockRepositoryOperation::STATE_WORK,
DrydockRepositoryOperation::STATE_FAIL,
))
->execute();
if (!$operations) {
return null;
}
$operation = head(msort($operations, 'getID'));
// TODO: This is completely made up for now, give it useful information and
// a sweet progress bar.
switch ($operation->getOperationState()) {
case DrydockRepositoryOperation::STATE_WAIT:
case DrydockRepositoryOperation::STATE_WORK:
$severity = PHUIInfoView::SEVERITY_NOTICE;
$text = pht(
'Some sort of repository operation is currently running.');
break;
default:
$severity = PHUIInfoView::SEVERITY_ERROR;
$text = pht(
'Some sort of repository operation failed.');
break;
}
$info_view = id(new PHUIInfoView())
->setSeverity($severity)
->appendChild($text);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Active Operations (EXPERIMENTAL!)'))
->setInfoView($info_view);
}
}

View file

@ -37,6 +37,18 @@ final class DifferentialLandingActionMenuEventListener
return null;
}
if ($repository->canPerformAutomation()) {
$revision_id = $revision->getID();
$action = id(new PhabricatorActionView())
->setWorkflow(true)
->setName(pht('Land Revision'))
->setIcon('fa-fighter-jet')
->setHref("/differential/revision/operation/{$revision_id}/");
$this->addActionMenuItems($event, $action);
}
$strategies = id(new PhutilClassMapQuery())
->setAncestorClass('DifferentialLandingStrategy')
->execute();

View file

@ -251,6 +251,12 @@ final class DifferentialDiff
$dict['changes'] = $this->buildChangesList();
return $dict + $this->getDiffAuthorshipDict();
}
public function getDiffAuthorshipDict() {
$dict = array();
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
@ -447,12 +453,8 @@ final class DifferentialDiff
$results['repository.vcs'] = $repo->getVersionControlSystem();
$results['repository.uri'] = $repo->getPublicCloneURI();
// TODO: We're just hoping to get lucky. Instead, `arc` should store
// where it sent changes and we should only provide staging details
// if we reasonably believe they are accurate.
$staging_ref = 'refs/tags/phabricator/diff/'.$this->getID();
$results['repository.staging.uri'] = $repo->getStagingURI();
$results['repository.staging.ref'] = $staging_ref;
$results['repository.staging.ref'] = $this->getStagingRef();
}
}
@ -480,6 +482,13 @@ final class DifferentialDiff
);
}
public function getStagingRef() {
// TODO: We're just hoping to get lucky. Instead, `arc` should store
// where it sent changes and we should only provide staging details
// if we reasonably believe they are accurate.
return 'refs/tags/phabricator/diff/'.$this->getID();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */

View file

@ -102,6 +102,7 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
'update/' => 'DiffusionRepositoryEditUpdateController',
'symbol/' => 'DiffusionRepositorySymbolsController',
'staging/' => 'DiffusionRepositoryEditStagingController',
'automation/' => 'DiffusionRepositoryEditAutomationController',
),
'pathtree/(?P<dblob>.*)' => 'DiffusionPathTreeController',
'mirror/' => array(

View file

@ -0,0 +1,94 @@
<?php
final class DiffusionRepositoryEditAutomationController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
if (!$repository->supportsAutomation()) {
return new Aphront404Response();
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
$v_blueprints = $repository->getHumanReadableDetail(
'automation.blueprintPHIDs');
if ($request->isFormPost()) {
$v_blueprints = $request->getArr('blueprintPHIDs');
$xactions = array();
$template = id(new PhabricatorRepositoryTransaction());
$type_blueprints =
PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS;
$xactions[] = id(clone $template)
->setTransactionType($type_blueprints)
->setNewValue($v_blueprints);
id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($viewer)
->applyTransactions($repository, $xactions);
return id(new AphrontRedirectResponse())->setURI($edit_uri);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Automation'));
$title = pht('Edit %s', $repository->getName());
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions(
pht(
"Configure **Repository Automation** to allow Phabricator to ".
"write to this repository.".
"\n\n".
"IMPORTANT: This feature is new, experimental, and not supported. ".
"Use it at your own risk."))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Use Blueprints'))
->setName('blueprintPHIDs')
->setValue($v_blueprints)
->setDatasource(new DrydockBlueprintDatasource()))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save'))
->addCancelButton($edit_uri));
$object_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
),
array(
'title' => $title,
));
}
}

View file

@ -31,6 +31,7 @@ final class DiffusionRepositoryEditMainController
$has_branches = ($is_git || $is_hg);
$has_local = $repository->usesLocalWorkingCopy();
$supports_staging = $repository->supportsStaging();
$supports_automation = $repository->supportsAutomation();
$crumbs = $this->buildApplicationCrumbs($is_main = true);
@ -100,6 +101,13 @@ final class DiffusionRepositoryEditMainController
$this->buildStagingActions($repository));
}
$automation_properties = null;
if ($supports_automation) {
$automation_properties = $this->buildAutomationProperties(
$repository,
$this->buildAutomationActions($repository));
}
$actions_properties = $this->buildActionsProperties(
$repository,
$this->buildActionsActions($repository));
@ -171,6 +179,12 @@ final class DiffusionRepositoryEditMainController
->addPropertyList($staging_properties);
}
if ($automation_properties) {
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Automation'))
->addPropertyList($automation_properties);
}
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Text Encoding'))
->addPropertyList($encoding_properties);
@ -622,7 +636,6 @@ final class DiffusionRepositoryEditMainController
return $view;
}
private function buildStagingActions(PhabricatorRepository $repository) {
$viewer = $this->getViewer();
@ -661,6 +674,47 @@ final class DiffusionRepositoryEditMainController
return $view;
}
private function buildAutomationActions(PhabricatorRepository $repository) {
$viewer = $this->getViewer();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Automation'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/automation/'));
$view->addAction($edit);
return $view;
}
private function buildAutomationProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$blueprint_phids = $repository->getAutomationBlueprintPHIDs();
if (!$blueprint_phids) {
$blueprint_view = phutil_tag('em', array(), pht('Not Configured'));
} else {
$blueprint_view = id(new DrydockObjectAuthorizationView())
->setUser($viewer)
->setObjectPHID($repository->getPHID())
->setBlueprintPHIDs($blueprint_phids);
}
$view->addProperty(pht('Automation'), $blueprint_view);
return $view;
}
private function buildHostingActions(PhabricatorRepository $repository) {
$user = $this->getRequest()->getUser();

View file

@ -536,12 +536,14 @@ final class DiffusionServeController extends DiffusionController {
$body = strlen($stderr)."\n".$stderr;
} else {
list($length, $body) = explode("\n", $stdout, 2);
if ($cmd == 'capabilities') {
$body = DiffusionMercurialWireProtocol::filterBundle2Capability($body);
}
}
return id(new DiffusionMercurialResponse())->setContent($body);
}
private function getMercurialArguments() {
// Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
// "Why would you do this?".

View file

@ -99,4 +99,34 @@ final class DiffusionMercurialWireProtocol extends Phobject {
return true;
}
/** If the server version is running 3.4+ it will respond
* with 'bundle2' capability in the format of "bundle2=(url-encoding)".
* Until we maange to properly package up bundles to send back we
* disallow the client from knowing we speak bundle2 by removing it
* from the capabilities listing.
*
* The format of the capabilties string is: "a space separated list
* of strings representing what commands the server supports"
* @link https://www.mercurial-scm.org/wiki/CommandServer#Protocol
*
* @param string $capabilities - The string of capabilities to
* strip the bundle2 capability from. This is expected to be
* the space-separated list of strings resulting from the
* querying the 'capabilties' command.
*
* @return string The resulting space-separated list of capabilities
* which no longer contains the 'bundle2' capability. This is meant
* to replace the original $body to send back to client.
*/
public static function filterBundle2Capability($capabilities) {
$parts = explode(' ', $capabilities);
foreach ($parts as $key => $part) {
if (preg_match('/^bundle2=/', $part)) {
unset($parts[$key]);
break;
}
}
return implode(' ', $parts);
}
}

View file

@ -0,0 +1,48 @@
<?php
final class DiffusionMercurialWireProtocolTests extends PhabricatorTestCase {
public function testFilteringBundle2Capability() {
// this was the result of running 'capabilities' over
// `hg serve --stdio` on my systems with Mercurial 3.5.1, 2.6.2
$capabilities_with_bundle2_hg_351 =
'lookup changegroupsubset branchmap pushkey '.
'known getbundle unbundlehash batch stream '.
'bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512'.
'%0Aerror%3Dabort%2Cunsupportedcontent%2Cpushraced%2Cpushkey%0A'.
'hgtagsfnodes%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps '.
'unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024';
$capabilities_without_bundle2_hg_351 =
'lookup changegroupsubset branchmap pushkey '.
'known getbundle unbundlehash batch stream '.
'unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024';
$capabilities_hg_262 =
'lookup changegroupsubset branchmap pushkey '.
'known getbundle unbundlehash batch stream '.
'unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 largefiles=serve';
$cases = array(
array(
'name' => pht('Filter bundle2 from Mercurial 3.5.1'),
'input' => $capabilities_with_bundle2_hg_351,
'expect' => $capabilities_without_bundle2_hg_351,
),
array(
'name' => pht('Filter bundle does not affect Mercurial 2.6.2'),
'input' => $capabilities_hg_262,
'expect' => $capabilities_hg_262,
),
);
foreach ($cases as $case) {
$actual = DiffusionMercurialWireProtocol::filterBundle2Capability(
$case['input']);
$this->assertEqual($case['expect'], $actual, $case['name']);
}
}
}

View file

@ -24,10 +24,17 @@ final class DiffusionLowLevelMercurialPathsQuery
$path = $this->path;
$commit = $this->commit;
$hg_paths_command = 'locate --print0 --rev %s -I %s';
$hg_version = PhabricatorRepositoryVersion::getMercurialVersion();
if (PhabricatorRepositoryVersion::isMercurialFilesCommandAvailable(
$hg_version)) {
$hg_paths_command = 'files --print0 --rev %s -I %s';
}
$match_against = trim($path, '/');
$prefix = trim('./'.$match_against, '/');
list($entire_manifest) = $repository->execxLocalCommand(
'locate --print0 --rev %s -I %s',
$hg_paths_command,
hgsprintf('%s', $commit),
$prefix);
return explode("\0", $entire_manifest);

View file

@ -0,0 +1,31 @@
<?php
final class DiffusionLowLevelMercurialPathsQueryTests
extends PhabricatorTestCase {
public function testCommandByVersion() {
$cases = array(
array(
'name' => pht('Versions which should not use `files`'),
'versions' => array('2.6.2', '2.9', '3.1'),
'match' => false,
),
array(
'name' => pht('Versions which should use `files`'),
'versions' => array('3.2', '3.3', '3.5.2'),
'match' => true,
),
);
foreach ($cases as $case) {
foreach ($case['versions'] as $version) {
$actual = PhabricatorRepositoryVersion
::isMercurialFilesCommandAvailable($version);
$expect = $case['match'];
$this->assertEqual($expect, $actual, $case['name']);
}
}
}
}

View file

@ -107,8 +107,14 @@ final class DiffusionMercurialServeSSHWorkflow
$this->didSeeWrite = true;
}
$raw_message = $message['raw'];
if ($command == 'capabilities') {
$raw_message = DiffusionMercurialWireProtocol::filterBundle2Capability(
$raw_message);
}
// If we're good, return the raw message data.
return $message['raw'];
return $raw_message;
}
}

View file

@ -57,6 +57,8 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication {
'DrydockResourceListController',
'logs/(?:query/(?P<queryKey>[^/]+)/)?' =>
'DrydockLogListController',
'authorizations/(?:query/(?P<queryKey>[^/]+)/)?' =>
'DrydockAuthorizationListController',
),
'create/' => 'DrydockBlueprintCreateController',
'edit/(?:(?P<id>[1-9]\d*)/)?' => 'DrydockBlueprintEditController',
@ -81,6 +83,20 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication {
'DrydockLogListController',
),
),
'(?P<type>authorization)/' => array(
'(?P<id>[1-9]\d*)/' => array(
'' => 'DrydockAuthorizationViewController',
'(?P<action>authorize|decline)/' =>
'DrydockAuthorizationAuthorizeController',
),
),
'(?P<type>operation)/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'DrydockRepositoryOperationListController',
'(?P<id>[1-9]\d*)/' => array(
'' => 'DrydockRepositoryOperationViewController',
),
),
),
);
}

View file

@ -292,7 +292,8 @@ abstract class DrydockBlueprintImplementation extends Phobject {
}
protected function newLease(DrydockBlueprint $blueprint) {
return DrydockLease::initializeNewLease();
return DrydockLease::initializeNewLease()
->setAuthorizingPHID($blueprint->getPHID());
}
protected function requireActiveLease(DrydockLease $lease) {
@ -342,9 +343,12 @@ abstract class DrydockBlueprintImplementation extends Phobject {
$counts = queryfx_all(
$conn_r,
'SELECT status, COUNT(*) N FROM %T WHERE blueprintPHID = %s',
'SELECT status, COUNT(*) N FROM %T
WHERE blueprintPHID = %s AND status != %s
GROUP BY status',
$resource->getTableName(),
$blueprint->getPHID());
$blueprint->getPHID(),
DrydockResourceStatus::STATUS_DESTROYED);
$counts = ipull($counts, 'N', 'status');
$n_alloc = idx($counts, DrydockResourceStatus::STATUS_PENDING, 0);

View file

@ -118,10 +118,13 @@ final class DrydockWorkingCopyBlueprintImplementation
$resource_phid = $resource->getPHID();
$blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs');
$host_lease = $this->newLease($blueprint)
->setResourceType('host')
->setOwnerPHID($resource_phid)
->setAttribute('workingcopy.resourcePHID', $resource_phid);
->setAttribute('workingcopy.resourcePHID', $resource_phid)
->setAllowedBlueprintPHIDs($blueprint_phids);
$resource
->setAttribute('host.leasePHID', $host_lease->getPHID())
@ -390,5 +393,15 @@ final class DrydockWorkingCopyBlueprintImplementation
return $lease;
}
public function getFieldSpecifications() {
return array(
'blueprintPHIDs' => array(
'name' => pht('Use Blueprints'),
'type' => 'blueprints',
'required' => true,
),
) + parent::getFieldSpecifications();
}
}

View file

@ -0,0 +1,85 @@
<?php
final class DrydockAuthorizationAuthorizeController
extends DrydockController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$authorization = id(new DrydockAuthorizationQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$authorization) {
return new Aphront404Response();
}
$authorization_uri = $this->getApplicationURI("authorization/{$id}/");
$is_authorize = ($request->getURIData('action') == 'authorize');
$state_authorized = DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED;
$state_declined = DrydockAuthorization::BLUEPRINTAUTH_DECLINED;
$state = $authorization->getBlueprintAuthorizationState();
$can_authorize = ($state != $state_authorized);
$can_decline = ($state != $state_declined);
if ($is_authorize && !$can_authorize) {
return $this->newDialog()
->setTitle(pht('Already Authorized'))
->appendParagraph(
pht(
'This authorization has already been approved.'))
->addCancelButton($authorization_uri);
}
if (!$is_authorize && !$can_decline) {
return $this->newDialog()
->setTitle(pht('Already Declined'))
->appendParagraph(
pht('This authorization has already been declined.'))
->addCancelButton($authorization_uri);
}
if ($request->isFormPost()) {
if ($is_authorize) {
$new_state = $state_authorized;
} else {
$new_state = $state_declined;
}
$authorization
->setBlueprintAuthorizationState($new_state)
->save();
return id(new AphrontRedirectResponse())->setURI($authorization_uri);
}
if ($is_authorize) {
$title = pht('Approve Authorization');
$body = pht(
'Approve this authorization? The object will be able to lease and '.
'allocate resources created by this blueprint.');
$button = pht('Approve Authorization');
} else {
$title = pht('Decline Authorization');
$body = pht(
'Decline this authorization? The object will not be able to lease '.
'or allocate resources created by this blueprint.');
$button = pht('Decline Authorization');
}
return $this->newDialog()
->setTitle($title)
->appendParagraph($body)
->addSubmitButton($button)
->addCancelButton($authorization_uri);
}
}

View file

@ -0,0 +1,87 @@
<?php
final class DrydockAuthorizationListController
extends DrydockController {
private $blueprint;
public function setBlueprint(DrydockBlueprint $blueprint) {
$this->blueprint = $blueprint;
return $this;
}
public function getBlueprint() {
return $this->blueprint;
}
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$engine = new DrydockAuthorizationSearchEngine();
$id = $request->getURIData('id');
$blueprint = id(new DrydockBlueprintQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$blueprint) {
return new Aphront404Response();
}
$this->setBlueprint($blueprint);
$engine->setBlueprint($blueprint);
$querykey = $request->getURIData('queryKey');
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($querykey)
->setSearchEngine($engine)
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
public function buildSideNavView() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$engine = id(new DrydockAuthorizationSearchEngine())
->setViewer($this->getViewer());
$engine->setBlueprint($this->getBlueprint());
$engine->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$blueprint = $this->getBlueprint();
if ($blueprint) {
$id = $blueprint->getID();
$crumbs->addTextCrumb(
pht('Blueprints'),
$this->getApplicationURI('blueprint/'));
$crumbs->addTextCrumb(
$blueprint->getBlueprintName(),
$this->getApplicationURI("blueprint/{$id}/"));
$crumbs->addTextCrumb(
pht('Authorizations'),
$this->getApplicationURI("blueprint/{$id}/authorizations/"));
}
return $crumbs;
}
}

View file

@ -0,0 +1,131 @@
<?php
final class DrydockAuthorizationViewController
extends DrydockController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$authorization = id(new DrydockAuthorizationQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$authorization) {
return new Aphront404Response();
}
$id = $authorization->getID();
$title = pht('Authorization %d', $id);
$blueprint = $authorization->getBlueprint();
$blueprint_id = $blueprint->getID();
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($authorization);
$state = $authorization->getBlueprintAuthorizationState();
$icon = DrydockAuthorization::getBlueprintStateIcon($state);
$name = DrydockAuthorization::getBlueprintStateName($state);
$header->setStatus($icon, null, $name);
$actions = $this->buildActionListView($authorization);
$properties = $this->buildPropertyListView($authorization);
$properties->setActionList($actions);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
pht('Blueprints'),
$this->getApplicationURI('blueprint/'));
$crumbs->addTextCrumb(
$blueprint->getBlueprintName(),
$this->getApplicationURI("blueprint/{$blueprint_id}/"));
$crumbs->addTextCrumb($title);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
),
array(
'title' => $title,
));
}
private function buildActionListView(DrydockAuthorization $authorization) {
$viewer = $this->getViewer();
$id = $authorization->getID();
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObjectURI($this->getRequest()->getRequestURI())
->setObject($authorization);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$authorization,
PhabricatorPolicyCapability::CAN_EDIT);
$authorize_uri = $this->getApplicationURI("authorization/{$id}/authorize/");
$decline_uri = $this->getApplicationURI("authorization/{$id}/decline/");
$state_authorized = DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED;
$state_declined = DrydockAuthorization::BLUEPRINTAUTH_DECLINED;
$state = $authorization->getBlueprintAuthorizationState();
$can_authorize = $can_edit && ($state != $state_authorized);
$can_decline = $can_edit && ($state != $state_declined);
$view->addAction(
id(new PhabricatorActionView())
->setHref($authorize_uri)
->setName(pht('Approve Authorization'))
->setIcon('fa-check')
->setWorkflow(true)
->setDisabled(!$can_authorize));
$view->addAction(
id(new PhabricatorActionView())
->setHref($decline_uri)
->setName(pht('Decline Authorization'))
->setIcon('fa-times')
->setWorkflow(true)
->setDisabled(!$can_decline));
return $view;
}
private function buildPropertyListView(DrydockAuthorization $authorization) {
$viewer = $this->getViewer();
$object_phid = $authorization->getObjectPHID();
$handles = $viewer->loadHandles(array($object_phid));
$handle = $handles[$object_phid];
$view = new PHUIPropertyListView();
$view->addProperty(
pht('Authorized Object'),
$handle->renderLink($handle->getFullName()));
$view->addProperty(pht('Object Type'), $handle->getTypeName());
$object_state = $authorization->getObjectAuthorizationState();
$view->addProperty(
pht('Authorization State'),
DrydockAuthorization::getObjectStateName($object_state));
return $view;
}
}

View file

@ -51,6 +51,8 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController {
$resource_box = $this->buildResourceBox($blueprint);
$authorizations_box = $this->buildAuthorizationsBox($blueprint);
$timeline = $this->buildTransactionTimeline(
$blueprint,
new DrydockBlueprintTransactionQuery());
@ -68,6 +70,7 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController {
$crumbs,
$object_box,
$resource_box,
$authorizations_box,
$log_box,
$timeline,
),
@ -167,12 +170,78 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController {
->setTag('a')
->setHref($resources_uri)
->setIconFont('fa-search')
->setText(pht('View All Resources')));
->setText(pht('View All')));
return id(new PHUIObjectBoxView())
->setHeader($resource_header)
->setObjectList($resource_list);
}
private function buildAuthorizationsBox(DrydockBlueprint $blueprint) {
$viewer = $this->getViewer();
$limit = 25;
// If there are pending authorizations against this blueprint, make sure
// we show them first.
$pending_authorizations = id(new DrydockAuthorizationQuery())
->setViewer($viewer)
->withBlueprintPHIDs(array($blueprint->getPHID()))
->withObjectStates(
array(
DrydockAuthorization::OBJECTAUTH_ACTIVE,
))
->withBlueprintStates(
array(
DrydockAuthorization::BLUEPRINTAUTH_REQUESTED,
))
->setLimit($limit)
->execute();
$all_authorizations = id(new DrydockAuthorizationQuery())
->setViewer($viewer)
->withBlueprintPHIDs(array($blueprint->getPHID()))
->withObjectStates(
array(
DrydockAuthorization::OBJECTAUTH_ACTIVE,
))
->withBlueprintStates(
array(
DrydockAuthorization::BLUEPRINTAUTH_REQUESTED,
DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED,
))
->setLimit($limit)
->execute();
$authorizations =
mpull($pending_authorizations, null, 'getPHID') +
mpull($all_authorizations, null, 'getPHID');
$authorization_list = id(new DrydockAuthorizationListView())
->setUser($viewer)
->setAuthorizations($authorizations)
->setNoDataString(
pht('No objects have active authorizations to use this blueprint.'));
$id = $blueprint->getID();
$authorizations_uri = "blueprint/{$id}/authorizations/query/all/";
$authorizations_uri = $this->getApplicationURI($authorizations_uri);
$authorizations_header = id(new PHUIHeaderView())
->setHeader(pht('Active Authorizations'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($authorizations_uri)
->setIconFont('fa-search')
->setText(pht('View All')));
return id(new PHUIObjectBoxView())
->setHeader($authorizations_header)
->setObjectList($authorization_list);
}
}

View file

@ -15,6 +15,7 @@ final class DrydockConsoleController extends DrydockController {
$nav->addFilter('blueprint', pht('Blueprints'));
$nav->addFilter('resource', pht('Resources'));
$nav->addFilter('lease', pht('Leases'));
$nav->addFilter('operation', pht('Repository Operations'));
$nav->selectFilter(null);
@ -52,6 +53,13 @@ final class DrydockConsoleController extends DrydockController {
->setHref($this->getApplicationURI('lease/'))
->addAttribute(pht('Manage leases on resources.')));
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Repository Operations'))
->setFontIcon('fa-fighter-jet')
->setHref($this->getApplicationURI('operation/'))
->addAttribute(pht('Review the repository operation queue.')));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Console'));

View file

@ -2,12 +2,6 @@
abstract class DrydockController extends PhabricatorController {
abstract public function buildSideNavView();
public function buildApplicationMenu() {
return $this->buildSideNavView()->getMenu();
}
protected function buildLocksTab($owner_phid) {
$locks = DrydockSlotLock::loadLocks($owner_phid);
@ -105,7 +99,7 @@ abstract class DrydockController extends PhabricatorController {
->setTag('a')
->setHref($all_uri)
->setIconFont('fa-search')
->setText(pht('View All Logs')));
->setText(pht('View All')));
return id(new PHUIObjectBoxView())
->setHeader($log_header)

View file

@ -116,6 +116,14 @@ final class DrydockLeaseViewController extends DrydockLeaseController {
}
$view->addProperty(pht('Owner'), $owner_display);
$authorizing_phid = $lease->getAuthorizingPHID();
if ($authorizing_phid) {
$authorizing_display = $viewer->renderHandle($authorizing_phid);
} else {
$authorizing_display = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(pht('Authorized By'), $authorizing_display);
$resource_phid = $lease->getResourcePHID();
if ($resource_phid) {
$resource_display = $viewer->renderHandle($resource_phid);

View file

@ -0,0 +1,37 @@
<?php
final class DrydockRepositoryOperationListController
extends DrydockController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$query_key = $request->getURIData('queryKey');
$engine = new DrydockRepositoryOperationSearchEngine();
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($query_key)
->setSearchEngine($engine)
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
public function buildSideNavView() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$engine = id(new DrydockRepositoryOperationSearchEngine())
->setViewer($this->getViewer());
$engine->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
}

View file

@ -0,0 +1,89 @@
<?php
final class DrydockRepositoryOperationViewController
extends DrydockController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$operation = id(new DrydockRepositoryOperationQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$operation) {
return new Aphront404Response();
}
$id = $operation->getID();
$title = pht('Repository Operation %d', $id);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($operation);
$state = $operation->getOperationState();
$icon = DrydockRepositoryOperation::getOperationStateIcon($state);
$name = DrydockRepositoryOperation::getOperationStateName($state);
$header->setStatus($icon, null, $name);
$actions = $this->buildActionListView($operation);
$properties = $this->buildPropertyListView($operation);
$properties->setActionList($actions);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
pht('Operations'),
$this->getApplicationURI('operation/'));
$crumbs->addTextCrumb($title);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
),
array(
'title' => $title,
));
}
private function buildActionListView(DrydockRepositoryOperation $operation) {
$viewer = $this->getViewer();
$id = $operation->getID();
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObjectURI($this->getRequest()->getRequestURI())
->setObject($operation);
return $view;
}
private function buildPropertyListView(
DrydockRepositoryOperation $operation) {
$viewer = $this->getViewer();
$view = new PHUIPropertyListView();
$view->addProperty(
pht('Repository'),
$viewer->renderHandle($operation->getRepositoryPHID()));
$view->addProperty(
pht('Object'),
$viewer->renderHandle($operation->getObjectPHID()));
return $view;
}
}

View file

@ -170,7 +170,7 @@ final class DrydockResourceViewController extends DrydockResourceController {
->setTag('a')
->setHref($leases_uri)
->setIconFont('fa-search')
->setText(pht('View All Leases')));
->setText(pht('View All')));
$lease_list = id(new DrydockLeaseListView())
->setUser($viewer)

View file

@ -41,11 +41,6 @@ final class DrydockBlueprintCoreCustomField
$object->setDetail($key, $value);
}
public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) {
return;
}
public function getBlueprintFieldValue() {
return $this->getProxy()->getFieldValue();
}

View file

@ -0,0 +1,26 @@
<?php
final class DrydockLeaseNoAuthorizationsLogType extends DrydockLogType {
const LOGCONST = 'core.lease.no-authorizations';
public function getLogTypeName() {
return pht('No Authorizations');
}
public function getLogTypeIcon(array $data) {
return 'fa-map-o red';
}
public function renderLog(array $data) {
$viewer = $this->getViewer();
$authorizing_phid = idx($data, 'authorizingPHID');
return pht(
'The object which authorized this lease (%s) is not authorized to use '.
'any of the blueprints the lease lists. Approve the authorizations '.
'before using the lease.',
$viewer->renderHandle($authorizing_phid)->render());
}
}

View file

@ -0,0 +1,19 @@
<?php
final class DrydockLeaseNoBlueprintsLogType extends DrydockLogType {
const LOGCONST = 'core.lease.no-blueprints';
public function getLogTypeName() {
return pht('No Blueprints');
}
public function getLogTypeIcon(array $data) {
return 'fa-map-o red';
}
public function renderLog(array $data) {
return pht('This lease does not list any usable blueprints.');
}
}

View file

@ -28,7 +28,7 @@ final class DrydockManagementLeaseWorkflow
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$resource_type = $args->getArg('type');
if (!$resource_type) {
@ -59,6 +59,23 @@ final class DrydockManagementLeaseWorkflow
$lease = id(new DrydockLease())
->setResourceType($resource_type);
$drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
$lease->setAuthorizingPHID($drydock_phid);
// TODO: This is not hugely scalable, although this is a debugging workflow
// so maybe it's fine. Do we even need `bin/drydock lease` in the long run?
$all_blueprints = id(new DrydockBlueprintQuery())
->setViewer($viewer)
->execute();
$allowed_phids = mpull($all_blueprints, 'getPHID');
if (!$allowed_phids) {
throw new Exception(
pht(
'No blueprints exist which can plausibly allocate resources to '.
'satisfy the requested lease.'));
}
$lease->setAllowedBlueprintPHIDs($allowed_phids);
if ($attributes) {
$lease->setAttributes($attributes);
}

View file

@ -0,0 +1,151 @@
<?php
final class DrydockLandRepositoryOperation
extends DrydockRepositoryOperationType {
const OPCONST = 'land';
public function getOperationDescription(
DrydockRepositoryOperation $operation,
PhabricatorUser $viewer) {
return pht('Land Revision');
}
public function applyOperation(
DrydockRepositoryOperation $operation,
DrydockInterface $interface) {
$viewer = $this->getViewer();
$repository = $operation->getRepository();
$cmd = array();
$arg = array();
$object = $operation->getObject();
if ($object instanceof DifferentialRevision) {
$revision = $object;
$diff_phid = $operation->getProperty('differential.diffPHID');
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withPHIDs(array($diff_phid))
->executeOne();
if (!$diff) {
throw new Exception(
pht(
'Unable to load diff "%s".',
$diff_phid));
}
$diff_revid = $diff->getRevisionID();
$revision_id = $revision->getID();
if ($diff_revid != $revision_id) {
throw new Exception(
pht(
'Diff ("%s") has wrong revision ID ("%s", expected "%s").',
$diff_phid,
$diff_revid,
$revision_id));
}
$cmd[] = 'git fetch --no-tags -- %s +%s:%s';
$arg[] = $repository->getStagingURI();
$arg[] = $diff->getStagingRef();
$arg[] = $diff->getStagingRef();
$merge_src = $diff->getStagingRef();
$dict = $diff->getDiffAuthorshipDict();
$author_name = idx($dict, 'authorName');
$author_email = idx($dict, 'authorEmail');
$api_method = 'differential.getcommitmessage';
$api_params = array(
'revision_id' => $revision->getID(),
);
$commit_message = id(new ConduitCall($api_method, $api_params))
->setUser($viewer)
->execute();
} else {
throw new Exception(
pht(
'Invalid or unknown object ("%s") for land operation, expected '.
'Differential Revision.',
$operation->getObjectPHID()));
}
$target = $operation->getRepositoryTarget();
list($type, $name) = explode(':', $target, 2);
switch ($type) {
case 'branch':
$push_dst = 'refs/heads/'.$name;
$merge_dst = 'refs/remotes/origin/'.$name;
break;
default:
throw new Exception(
pht(
'Unknown repository operation target type "%s" (in target "%s").',
$type,
$target));
}
$committer_info = $this->getCommitterInfo($operation);
$cmd[] = 'git checkout %s';
$arg[] = $merge_dst;
$cmd[] = 'git merge --no-stat --squash --ff-only -- %s';
$arg[] = $merge_src;
$cmd[] = 'git -c user.name=%s -c user.email=%s commit --author %s -m %s';
$arg[] = $committer_info['name'];
$arg[] = $committer_info['email'];
$arg[] = "{$author_name} <{$author_email}>";
$arg[] = $commit_message;
$cmd[] = 'git push origin -- %s:%s';
$arg[] = 'HEAD';
$arg[] = $push_dst;
$cmd = implode(' && ', $cmd);
$argv = array_merge(array($cmd), $arg);
$result = call_user_func_array(
array($interface, 'execx'),
$argv);
}
private function getCommitterInfo(DrydockRepositoryOperation $operation) {
$viewer = $this->getViewer();
$committer_name = null;
$author_phid = $operation->getAuthorPHID();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($author_phid))
->executeOne();
if ($object) {
if ($object instanceof PhabricatorUser) {
$committer_name = $object->getUsername();
}
}
if (!strlen($committer_name)) {
$committer_name = pht('autocommitter');
}
// TODO: Probably let users choose a VCS email address in settings. For
// now just make something up so we don't leak anyone's stuff.
return array(
'name' => $committer_name,
'email' => 'autocommitter@example.com',
);
}
}

View file

@ -0,0 +1,35 @@
<?php
abstract class DrydockRepositoryOperationType extends Phobject {
private $viewer;
abstract public function applyOperation(
DrydockRepositoryOperation $operation,
DrydockInterface $interface);
abstract public function getOperationDescription(
DrydockRepositoryOperation $operation,
PhabricatorUser $viewer);
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function getOperationConstant() {
return $this->getPhobjectClassConstant('OPCONST', 32);
}
final public static function getAllOperationTypes() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getOperationConstant')
->execute();
}
}

View file

@ -0,0 +1,37 @@
<?php
final class DrydockAuthorizationPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'DRYA';
public function getTypeName() {
return pht('Drydock Authorization');
}
public function newObject() {
return new DrydockAuthorization();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new DrydockAuthorizationQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$authorization = $objects[$phid];
$id = $authorization->getID();
$handle->setName(pht('Drydock Authorization %d', $id));
$handle->setURI("/drydock/authorization/{$id}/");
}
}
}

View file

@ -8,6 +8,14 @@ final class DrydockBlueprintPHIDType extends PhabricatorPHIDType {
return pht('Blueprint');
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDrydockApplication';
}
public function getTypeIcon() {
return 'fa-map-o';
}
public function newObject() {
return new DrydockBlueprint();
}
@ -28,9 +36,12 @@ final class DrydockBlueprintPHIDType extends PhabricatorPHIDType {
foreach ($handles as $phid => $handle) {
$blueprint = $objects[$phid];
$id = $blueprint->getID();
$name = $blueprint->getBlueprintName();
$handle->setName($blueprint->getBlueprintName());
$handle->setURI("/drydock/blueprint/{$id}/");
$handle
->setName($name)
->setFullName(pht('Blueprint %d: %s', $id, $name))
->setURI("/drydock/blueprint/{$id}/");
}
}

View file

@ -8,6 +8,14 @@ final class DrydockLeasePHIDType extends PhabricatorPHIDType {
return pht('Drydock Lease');
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDrydockApplication';
}
public function getTypeIcon() {
return 'fa-link';
}
public function newObject() {
return new DrydockLease();
}

View file

@ -0,0 +1,37 @@
<?php
final class DrydockRepositoryOperationPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'DRYO';
public function getTypeName() {
return pht('Drydock Repository Operation');
}
public function newObject() {
return new DrydockRepositoryOperation();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new DrydockRepositoryOperationQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$operation = $objects[$phid];
$id = $operation->getID();
$handle->setName(pht('Drydock Repository Operation %d', $id));
$handle->setURI("/drydock/operation/{$id}/");
}
}
}

View file

@ -8,6 +8,14 @@ final class DrydockResourcePHIDType extends PhabricatorPHIDType {
return pht('Drydock Resource');
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDrydockApplication';
}
public function getTypeIcon() {
return 'fa-map';
}
public function newObject() {
return new DrydockResource();
}

View file

@ -0,0 +1,146 @@
<?php
final class DrydockAuthorizationQuery extends DrydockQuery {
private $ids;
private $phids;
private $blueprintPHIDs;
private $objectPHIDs;
private $blueprintStates;
private $objectStates;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBlueprintPHIDs(array $phids) {
$this->blueprintPHIDs = $phids;
return $this;
}
public function withObjectPHIDs(array $phids) {
$this->objectPHIDs = $phids;
return $this;
}
public function withBlueprintStates(array $states) {
$this->blueprintStates = $states;
return $this;
}
public function withObjectStates(array $states) {
$this->objectStates = $states;
return $this;
}
public function newResultObject() {
return new DrydockAuthorization();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $authorizations) {
$blueprint_phids = mpull($authorizations, 'getBlueprintPHID');
if ($blueprint_phids) {
$blueprints = id(new DrydockBlueprintQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($blueprint_phids)
->execute();
$blueprints = mpull($blueprints, null, 'getPHID');
} else {
$blueprints = array();
}
foreach ($authorizations as $key => $authorization) {
$blueprint = idx($blueprints, $authorization->getBlueprintPHID());
if (!$blueprint) {
$this->didRejectResult($authorization);
unset($authorizations[$key]);
continue;
}
$authorization->attachBlueprint($blueprint);
}
$object_phids = mpull($authorizations, 'getObjectPHID');
if ($object_phids) {
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($object_phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
} else {
$objects = array();
}
foreach ($authorizations as $key => $authorization) {
$object = idx($objects, $authorization->getObjectPHID());
if (!$object) {
$this->didRejectResult($authorization);
unset($authorizations[$key]);
continue;
}
$authorization->attachObject($object);
}
return $authorizations;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->blueprintPHIDs !== null) {
$where[] = qsprintf(
$conn,
'blueprintPHID IN (%Ls)',
$this->blueprintPHIDs);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->blueprintStates !== null) {
$where[] = qsprintf(
$conn,
'blueprintAuthorizationState IN (%Ls)',
$this->blueprintStates);
}
if ($this->objectStates !== null) {
$where[] = qsprintf(
$conn,
'objectAuthorizationState IN (%Ls)',
$this->objectStates);
}
return $where;
}
}

View file

@ -0,0 +1,87 @@
<?php
final class DrydockAuthorizationSearchEngine
extends PhabricatorApplicationSearchEngine {
private $blueprint;
public function setBlueprint(DrydockBlueprint $blueprint) {
$this->blueprint = $blueprint;
return $this;
}
public function getBlueprint() {
return $this->blueprint;
}
public function getResultTypeDescription() {
return pht('Drydock Authorizations');
}
public function getApplicationClassName() {
return 'PhabricatorDrydockApplication';
}
public function canUseInPanelContext() {
return false;
}
public function newQuery() {
$query = new DrydockAuthorizationQuery();
$blueprint = $this->getBlueprint();
$query->withBlueprintPHIDs(array($blueprint->getPHID()));
return $query;
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
return $query;
}
protected function buildCustomSearchFields() {
return array();
}
protected function getURI($path) {
$blueprint = $this->getBlueprint();
$id = $blueprint->getID();
return "/drydock/blueprint/{$id}/authorizations/".$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Authorizations'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $authorizations,
PhabricatorSavedQuery $query,
array $handles) {
$list = id(new DrydockAuthorizationListView())
->setUser($this->requireViewer())
->setAuthorizations($authorizations);
$result = new PhabricatorApplicationSearchResultView();
$result->setTable($list);
return $result;
}
}

View file

@ -7,6 +7,7 @@ final class DrydockBlueprintQuery extends DrydockQuery {
private $blueprintClasses;
private $datasourceQuery;
private $disabled;
private $authorizedPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -33,10 +34,19 @@ final class DrydockBlueprintQuery extends DrydockQuery {
return $this;
}
public function withAuthorizedPHIDs(array $phids) {
$this->authorizedPHIDs = $phids;
return $this;
}
public function newResultObject() {
return new DrydockBlueprint();
}
protected function getPrimaryTableAlias() {
return 'blueprint';
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
@ -63,39 +73,66 @@ final class DrydockBlueprintQuery extends DrydockQuery {
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
'blueprint.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
'blueprint.phid IN (%Ls)',
$this->phids);
}
if ($this->datasourceQuery !== null) {
$where[] = qsprintf(
$conn,
'blueprintName LIKE %>',
'blueprint.blueprintName LIKE %>',
$this->datasourceQuery);
}
if ($this->blueprintClasses !== null) {
$where[] = qsprintf(
$conn,
'className IN (%Ls)',
'blueprint.className IN (%Ls)',
$this->blueprintClasses);
}
if ($this->disabled !== null) {
$where[] = qsprintf(
$conn,
'isDisabled = %d',
'blueprint.isDisabled = %d',
(int)$this->disabled);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->authorizedPHIDs !== null) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->authorizedPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T authorization
ON authorization.blueprintPHID = blueprint.phid
AND authorization.objectPHID IN (%Ls)
AND authorization.objectAuthorizationState = %s
AND authorization.blueprintAuthorizationState = %s',
id(new DrydockAuthorization())->getTableName(),
$this->authorizedPHIDs,
DrydockAuthorization::OBJECTAUTH_ACTIVE,
DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED);
}
return $joins;
}
}

View file

@ -0,0 +1,145 @@
<?php
final class DrydockRepositoryOperationQuery extends DrydockQuery {
private $ids;
private $phids;
private $objectPHIDs;
private $repositoryPHIDs;
private $operationStates;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
public function withOperationStates(array $states) {
$this->operationStates = $states;
return $this;
}
public function newResultObject() {
return new DrydockRepositoryOperation();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $operations) {
$implementations = DrydockRepositoryOperationType::getAllOperationTypes();
foreach ($operations as $key => $operation) {
$impl = idx($implementations, $operation->getOperationType());
if (!$impl) {
$this->didRejectResult($operation);
unset($operations[$key]);
continue;
}
$impl = clone $impl;
$operation->attachImplementation($impl);
}
$repository_phids = mpull($operations, 'getRepositoryPHID');
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
} else {
$repositories = array();
}
foreach ($operations as $key => $operation) {
$repository = idx($repositories, $operation->getRepositoryPHID());
if (!$repository) {
$this->didRejectResult($operation);
unset($operations[$key]);
continue;
}
$operation->attachRepository($repository);
}
return $operations;
}
protected function didFilterPage(array $operations) {
$object_phids = mpull($operations, 'getObjectPHID');
if ($object_phids) {
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($object_phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
} else {
$objects = array();
}
foreach ($operations as $key => $operation) {
$object = idx($objects, $operation->getObjectPHID());
$operation->attachObject($object);
}
return $operations;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->operationStates !== null) {
$where[] = qsprintf(
$conn,
'operationState IN (%Ls)',
$this->operationStates);
}
return $where;
}
}

View file

@ -0,0 +1,99 @@
<?php
final class DrydockRepositoryOperationSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Drydock Repository Operations');
}
public function getApplicationClassName() {
return 'PhabricatorDrydockApplication';
}
public function newQuery() {
return id(new DrydockRepositoryOperationQuery());
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
return $query;
}
protected function buildCustomSearchFields() {
return array(
);
}
protected function getURI($path) {
return '/drydock/operation/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Operations'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $operations,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($operations, 'DrydockRepositoryOperation');
$viewer = $this->requireViewer();
$view = new PHUIObjectItemListView();
foreach ($operations as $operation) {
$id = $operation->getID();
$item = id(new PHUIObjectItemView())
->setHeader($operation->getOperationDescription($viewer))
->setHref($this->getApplicationURI("operation/{$id}/"))
->setObjectName(pht('Repository Operation %d', $id));
$state = $operation->getOperationState();
$icon = DrydockRepositoryOperation::getOperationStateIcon($state);
$name = DrydockRepositoryOperation::getOperationStateName($state);
$item->addIcon($icon, $name);
$item->addByline(
array(
pht('Via:'),
' ',
$viewer->renderHandle($operation->getAuthorPHID()),
));
$item->addAttribute(
$viewer->renderHandle(
$operation->getObjectPHID()));
$item->addAttribute(
$viewer->renderHandle(
$operation->getRepositoryPHID()));
$view->addItem($item);
}
$result = id(new PhabricatorApplicationSearchResultView())
->setObjectList($view)
->setNoDataString(pht('No matching operations.'));
return $result;
}
}

View file

@ -0,0 +1,202 @@
<?php
final class DrydockAuthorization extends DrydockDAO
implements
PhabricatorPolicyInterface {
const OBJECTAUTH_ACTIVE = 'active';
const OBJECTAUTH_INACTIVE = 'inactive';
const BLUEPRINTAUTH_REQUESTED = 'requested';
const BLUEPRINTAUTH_AUTHORIZED = 'authorized';
const BLUEPRINTAUTH_DECLINED = 'declined';
protected $blueprintPHID;
protected $blueprintAuthorizationState;
protected $objectPHID;
protected $objectAuthorizationState;
private $blueprint = self::ATTACHABLE;
private $object = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'blueprintAuthorizationState' => 'text32',
'objectAuthorizationState' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_unique' => array(
'columns' => array('objectPHID', 'blueprintPHID'),
'unique' => true,
),
'key_blueprint' => array(
'columns' => array('blueprintPHID', 'blueprintAuthorizationState'),
),
'key_object' => array(
'columns' => array('objectPHID', 'objectAuthorizationState'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DrydockAuthorizationPHIDType::TYPECONST);
}
public function attachBlueprint(DrydockBlueprint $blueprint) {
$this->blueprint = $blueprint;
return $this;
}
public function getBlueprint() {
return $this->assertAttached($this->blueprint);
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public static function getBlueprintStateIcon($state) {
$map = array(
self::BLUEPRINTAUTH_REQUESTED => 'fa-exclamation-circle indigo',
self::BLUEPRINTAUTH_AUTHORIZED => 'fa-check-circle green',
self::BLUEPRINTAUTH_DECLINED => 'fa-times red',
);
return idx($map, $state, null);
}
public static function getBlueprintStateName($state) {
$map = array(
self::BLUEPRINTAUTH_REQUESTED => pht('Requested'),
self::BLUEPRINTAUTH_AUTHORIZED => pht('Authorized'),
self::BLUEPRINTAUTH_DECLINED => pht('Declined'),
);
return idx($map, $state, pht('<Unknown: %s>', $state));
}
public static function getObjectStateName($state) {
$map = array(
self::OBJECTAUTH_ACTIVE => pht('Active'),
self::OBJECTAUTH_INACTIVE => pht('Inactive'),
);
return idx($map, $state, pht('<Unknown: %s>', $state));
}
/**
* Apply external authorization effects after a user chagnes the value of a
* blueprint selector control an object.
*
* @param PhabricatorUser User applying the change.
* @param phid Object PHID change is being applied to.
* @param list<phid> Old blueprint PHIDs.
* @param list<phid> New blueprint PHIDs.
* @return void
*/
public static function applyAuthorizationChanges(
PhabricatorUser $viewer,
$object_phid,
array $old,
array $new) {
$old_phids = array_fuse($old);
$new_phids = array_fuse($new);
$rem_phids = array_diff_key($old_phids, $new_phids);
$add_phids = array_diff_key($new_phids, $old_phids);
$altered_phids = $rem_phids + $add_phids;
if (!$altered_phids) {
return;
}
$authorizations = id(new DrydockAuthorizationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withObjectPHIDs(array($object_phid))
->withBlueprintPHIDs($altered_phids)
->execute();
$authorizations = mpull($authorizations, null, 'getBlueprintPHID');
$state_active = self::OBJECTAUTH_ACTIVE;
$state_inactive = self::OBJECTAUTH_INACTIVE;
$state_requested = self::BLUEPRINTAUTH_REQUESTED;
// Disable the object side of the authorization for any existing
// authorizations.
foreach ($rem_phids as $rem_phid) {
$authorization = idx($authorizations, $rem_phid);
if (!$authorization) {
continue;
}
$authorization
->setObjectAuthorizationState($state_inactive)
->save();
}
// For new authorizations, either add them or reactivate them depending
// on the current state.
foreach ($add_phids as $add_phid) {
$needs_update = false;
$authorization = idx($authorizations, $add_phid);
if (!$authorization) {
$authorization = id(new DrydockAuthorization())
->setObjectPHID($object_phid)
->setObjectAuthorizationState($state_active)
->setBlueprintPHID($add_phid)
->setBlueprintAuthorizationState($state_requested);
$needs_update = true;
} else {
$current_state = $authorization->getObjectAuthorizationState();
if ($current_state != $state_active) {
$authorization->setObjectAuthorizationState($state_active);
$needs_update = true;
}
}
if ($needs_update) {
$authorization->save();
}
}
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBlueprint()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBlueprint()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'An authorization inherits the policies of the blueprint it '.
'authorizes access to.');
}
}

View file

@ -7,6 +7,7 @@ final class DrydockLease extends DrydockDAO
protected $resourceType;
protected $until;
protected $ownerPHID;
protected $authorizingPHID;
protected $attributes = array();
protected $status = DrydockLeaseStatus::STATUS_PENDING;
@ -141,6 +142,25 @@ final class DrydockLease extends DrydockDAO
pht('Only new leases may be queued for activation!'));
}
if (!$this->getAuthorizingPHID()) {
throw new Exception(
pht(
'Trying to queue a lease for activation without an authorizing '.
'object. Use "%s" to specify the PHID of the authorizing object. '.
'The authorizing object must be approved to use the allowed '.
'blueprints.',
'setAuthorizingPHID()'));
}
if (!$this->getAllowedBlueprintPHIDs()) {
throw new Exception(
pht(
'Trying to queue a lease for activation without any allowed '.
'Blueprints. Use "%s" to specify allowed blueprints. The '.
'authorizing object must be approved to use the allowed blueprints.',
'setAllowedBlueprintPHIDs()'));
}
$this
->setStatus(DrydockLeaseStatus::STATUS_PENDING)
->save();
@ -376,6 +396,15 @@ final class DrydockLease extends DrydockDAO
return $this;
}
public function setAllowedBlueprintPHIDs(array $phids) {
$this->setAttribute('internal.blueprintPHIDs', $phids);
return $this;
}
public function getAllowedBlueprintPHIDs() {
return $this->getAttribute('internal.blueprintPHIDs', array());
}
private function didActivate() {
$viewer = PhabricatorUser::getOmnipotentUser();
$need_update = false;

View file

@ -0,0 +1,170 @@
<?php
/**
* Represents a request to perform a repository operation like a merge or
* cherry-pick.
*/
final class DrydockRepositoryOperation extends DrydockDAO
implements
PhabricatorPolicyInterface {
const STATE_WAIT = 'wait';
const STATE_WORK = 'work';
const STATE_DONE = 'done';
const STATE_FAIL = 'fail';
protected $authorPHID;
protected $objectPHID;
protected $repositoryPHID;
protected $repositoryTarget;
protected $operationType;
protected $operationState;
protected $properties = array();
private $repository = self::ATTACHABLE;
private $object = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
public static function initializeNewOperation(
DrydockRepositoryOperationType $op) {
return id(new DrydockRepositoryOperation())
->setOperationState(self::STATE_WAIT)
->setOperationType($op->getOperationConstant());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'repositoryTarget' => 'bytes',
'operationType' => 'text32',
'operationState' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
'key_repository' => array(
'columns' => array('repositoryPHID', 'operationState'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DrydockRepositoryOperationPHIDType::TYPECONST);
}
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function attachImplementation(DrydockRepositoryOperationType $impl) {
$this->implementation = $impl;
return $this;
}
public function getImplementation() {
return $this->implementation;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public static function getOperationStateIcon($state) {
$map = array(
self::STATE_WAIT => 'fa-clock-o',
self::STATE_WORK => 'fa-refresh blue',
self::STATE_DONE => 'fa-check green',
self::STATE_FAIL => 'fa-times red',
);
return idx($map, $state, null);
}
public static function getOperationStateName($state) {
$map = array(
self::STATE_WAIT => pht('Waiting'),
self::STATE_WORK => pht('Working'),
self::STATE_DONE => pht('Done'),
self::STATE_FAIL => pht('Failed'),
);
return idx($map, $state, pht('<Unknown: %s>', $state));
}
public function scheduleUpdate() {
PhabricatorWorker::scheduleTask(
'DrydockRepositoryOperationUpdateWorker',
array(
'operationPHID' => $this->getPHID(),
),
array(
'objectPHID' => $this->getPHID(),
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
}
public function applyOperation(DrydockInterface $interface) {
return $this->getImplementation()->applyOperation(
$this,
$interface);
}
public function getOperationDescription(PhabricatorUser $viewer) {
return $this->getImplementation()->getOperationDescription(
$this,
$viewer);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getRepository()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getRepository()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'A repository operation inherits the policies of the repository it '.
'affects.');
}
}

View file

@ -0,0 +1,65 @@
<?php
final class DrydockAuthorizationListView extends AphrontView {
private $authorizations;
private $noDataString;
public function setAuthorizations(array $authorizations) {
assert_instances_of($authorizations, 'DrydockAuthorization');
$this->authorizations = $authorizations;
return $this;
}
public function setNoDataString($string) {
$this->noDataString = $string;
return $this;
}
public function getNoDataString() {
return $this->noDataString;
}
public function render() {
$viewer = $this->getUser();
$authorizations = $this->authorizations;
$view = new PHUIObjectItemListView();
$nodata = $this->getNoDataString();
if ($nodata) {
$view->setNoDataString($nodata);
}
$handles = $viewer->loadHandles(mpull($authorizations, 'getObjectPHID'));
foreach ($authorizations as $authorization) {
$id = $authorization->getID();
$object_phid = $authorization->getObjectPHID();
$handle = $handles[$object_phid];
$item = id(new PHUIObjectItemView())
->setHref("/drydock/authorization/{$id}/")
->setObjectName(pht('Authorization %d', $id))
->setHeader($handle->getFullName());
$item->addAttribute($handle->getTypeName());
$object_state = $authorization->getObjectAuthorizationState();
$item->addAttribute(
DrydockAuthorization::getObjectStateName($object_state));
$state = $authorization->getBlueprintAuthorizationState();
$icon = DrydockAuthorization::getBlueprintStateIcon($state);
$name = DrydockAuthorization::getBlueprintStateName($state);
$item->setStatusIcon($icon, $name);
$view->addItem($item);
}
return $view;
}
}

View file

@ -0,0 +1,79 @@
<?php
final class DrydockObjectAuthorizationView extends AphrontView {
private $objectPHID;
private $blueprintPHIDs;
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setBlueprintPHIDs(array $blueprint_phids) {
$this->blueprintPHIDs = $blueprint_phids;
return $this;
}
public function getBlueprintPHIDs() {
return $this->blueprintPHIDs;
}
public function render() {
$viewer = $this->getUser();
$blueprint_phids = $this->getBlueprintPHIDs();
$object_phid = $this->getObjectPHID();
// NOTE: We're intentionally letting you see the authorization state on
// blueprints you can't see because this has a tremendous potential to
// be extremely confusing otherwise. You still can't see the blueprints
// themselves, but you can know if the object is authorized on something.
if ($blueprint_phids) {
$handles = $viewer->loadHandles($blueprint_phids);
$authorizations = id(new DrydockAuthorizationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withObjectPHIDs(array($object_phid))
->withBlueprintPHIDs($blueprint_phids)
->execute();
$authorizations = mpull($authorizations, null, 'getBlueprintPHID');
} else {
$handles = array();
$authorizations = array();
}
$items = array();
foreach ($blueprint_phids as $phid) {
$authorization = idx($authorizations, $phid);
if (!$authorization) {
continue;
}
$handle = $handles[$phid];
$item = id(new PHUIStatusItemView())
->setTarget($handle->renderLink());
$state = $authorization->getBlueprintAuthorizationState();
$item->setIcon(
DrydockAuthorization::getBlueprintStateIcon($state),
null,
DrydockAuthorization::getBlueprintStateName($state));
$items[] = $item;
}
$status = new PHUIStatusListView();
foreach ($items as $item) {
$status->addItem($item);
}
return $status;
}
}

View file

@ -211,10 +211,19 @@ final class DrydockLeaseUpdateWorker extends DrydockWorker {
$exceptions);
}
$resources = $this->removeUnacquirableResources($resources, $lease);
if (!$resources) {
// If we make it here, we just built a resource but aren't allowed
// to acquire it. We expect this during routine operation if the
// resource prevents acquisition until it activates. Yield and wait
// for activation.
throw new PhabricatorWorkerYieldException(15);
}
// NOTE: We have not acquired the lease yet, so it is possible that the
// resource we just built will be snatched up by some other lease before
// we can. This is not problematic: we'll retry a little later and should
// suceed eventually.
// we can acquire it. This is not problematic: we'll retry a little later
// and should suceed eventually.
}
$resources = $this->rankResources($resources, $lease);
@ -300,11 +309,46 @@ final class DrydockLeaseUpdateWorker extends DrydockWorker {
return array();
}
$blueprints = id(new DrydockBlueprintQuery())
$query = id(new DrydockBlueprintQuery())
->setViewer($viewer)
->withBlueprintClasses(array_keys($impls))
->withDisabled(false)
->execute();
->withDisabled(false);
$blueprint_phids = $lease->getAllowedBlueprintPHIDs();
if (!$blueprint_phids) {
$lease->logEvent(DrydockLeaseNoBlueprintsLogType::LOGCONST);
return array();
}
// The Drydock application itself is allowed to authorize anything. This
// is primarily used for leases generated by CLI administrative tools.
$drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
$authorizing_phid = $lease->getAuthorizingPHID();
if ($authorizing_phid != $drydock_phid) {
$blueprints = id(clone $query)
->withAuthorizedPHIDs(array($authorizing_phid))
->execute();
if (!$blueprints) {
// If we didn't hit any blueprints, check if this is an authorization
// problem: re-execute the query without the authorization constraint.
// If the second query hits blueprints, the overall configuration is
// fine but this is an authorization problem. If the second query also
// comes up blank, this is some other kind of configuration issue so
// we fall through to the default pathway.
$all_blueprints = $query->execute();
if ($all_blueprints) {
$lease->logEvent(
DrydockLeaseNoAuthorizationsLogType::LOGCONST,
array(
'authorizingPHID' => $authorizing_phid,
));
return array();
}
}
} else {
$blueprints = $query->execute();
}
$keep = array();
foreach ($blueprints as $key => $blueprint) {
@ -347,6 +391,22 @@ final class DrydockLeaseUpdateWorker extends DrydockWorker {
))
->execute();
return $this->removeUnacquirableResources($resources, $lease);
}
/**
* Remove resources which can not be acquired by a given lease from a list.
*
* @param list<DrydockResource> Candidate resources.
* @param DrydockLease Acquiring lease.
* @return list<DrydockResource> Resources which the lease may be able to
* acquire.
* @task allocator
*/
private function removeUnacquirableResources(
array $resources,
DrydockLease $lease) {
$keep = array();
foreach ($resources as $key => $resource) {
$blueprint = $resource->getBlueprint();

View file

@ -0,0 +1,177 @@
<?php
final class DrydockRepositoryOperationUpdateWorker
extends DrydockWorker {
protected function doWork() {
$operation_phid = $this->getTaskDataValue('operationPHID');
$hash = PhabricatorHash::digestForIndex($operation_phid);
$lock_key = 'drydock.operation:'.$hash;
$lock = PhabricatorGlobalLock::newLock($lock_key)
->lock(1);
try {
$operation = $this->loadOperation($operation_phid);
$this->handleUpdate($operation);
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
$lock->unlock();
}
private function handleUpdate(DrydockRepositoryOperation $operation) {
$viewer = $this->getViewer();
$operation_state = $operation->getOperationState();
switch ($operation_state) {
case DrydockRepositoryOperation::STATE_WAIT:
$operation
->setOperationState(DrydockRepositoryOperation::STATE_WORK)
->save();
break;
case DrydockRepositoryOperation::STATE_WORK:
break;
case DrydockRepositoryOperation::STATE_DONE:
case DrydockRepositoryOperation::STATE_FAIL:
// No more processing for these requests.
return;
}
// TODO: We should probably check for other running operations with lower
// IDs and the same repository target and yield to them here? That is,
// enforce sequential evaluation of operations against the same target so
// that if you land "A" and then land "B", we always finish "A" first.
// For now, just let stuff happen in any order. We can't lease until
// we know we're good to move forward because we might deadlock if we do:
// we're waiting for another operation to complete, and that operation is
// waiting for a lease we're holding.
try {
$lease = $this->loadWorkingCopyLease($operation);
$interface = $lease->getInterface(
DrydockCommandInterface::INTERFACE_TYPE);
// No matter what happens here, destroy the lease away once we're done.
$lease->releaseOnDestruction(true);
$operation->getImplementation()
->setViewer($viewer);
$operation->applyOperation($interface);
} catch (PhabricatorWorkerYieldException $ex) {
throw $ex;
} catch (Exception $ex) {
$operation
->setOperationState(DrydockRepositoryOperation::STATE_FAIL)
->save();
throw $ex;
}
$operation
->setOperationState(DrydockRepositoryOperation::STATE_DONE)
->save();
// TODO: Once we have sequencing, we could awaken the next operation
// against this target after finishing or failing.
}
private function loadWorkingCopyLease(
DrydockRepositoryOperation $operation) {
$viewer = $this->getViewer();
// TODO: This is very similar to leasing in Harbormaster, maybe we can
// share some of the logic?
$lease_phid = $operation->getProperty('exec.leasePHID');
if ($lease_phid) {
$lease = id(new DrydockLeaseQuery())
->setViewer($viewer)
->withPHIDs(array($lease_phid))
->executeOne();
if (!$lease) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Lease "%s" could not be loaded.',
$lease_phid));
}
} else {
$working_copy_type = id(new DrydockWorkingCopyBlueprintImplementation())
->getType();
$repository = $operation->getRepository();
$allowed_phids = $repository->getAutomationBlueprintPHIDs();
$authorizing_phid = $repository->getPHID();
$lease = DrydockLease::initializeNewLease()
->setResourceType($working_copy_type)
->setOwnerPHID($operation->getPHID())
->setAuthorizingPHID($authorizing_phid)
->setAllowedBlueprintPHIDs($allowed_phids);
$map = $this->buildRepositoryMap($operation);
$lease->setAttribute('repositories.map', $map);
$task_id = $this->getCurrentWorkerTaskID();
if ($task_id) {
$lease->setAwakenTaskIDs(array($task_id));
}
$operation
->setProperty('exec.leasePHID', $lease->getPHID())
->save();
$lease->queueForActivation();
}
if ($lease->isActivating()) {
throw new PhabricatorWorkerYieldException(15);
}
if (!$lease->isActive()) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Lease "%s" never activated.',
$lease->getPHID()));
}
return $lease;
}
private function buildRepositoryMap(DrydockRepositoryOperation $operation) {
$repository = $operation->getRepository();
$target = $operation->getRepositoryTarget();
list($type, $name) = explode(':', $target, 2);
switch ($type) {
case 'branch':
$spec = array(
'branch' => $name,
);
break;
default:
throw new Exception(
pht(
'Unknown repository operation target type "%s" (in target "%s").',
$type,
$target));
}
$map = array();
$map[$repository->getCloneName()] = array(
'phid' => $repository->getPHID(),
'default' => true,
) + $spec;
return $map;
}
}

View file

@ -36,6 +36,21 @@ abstract class DrydockWorker extends PhabricatorWorker {
return $resource;
}
protected function loadOperation($operation_phid) {
$viewer = $this->getViewer();
$operation = id(new DrydockRepositoryOperationQuery())
->setViewer($viewer)
->withPHIDs(array($operation_phid))
->executeOne();
if (!$operation) {
throw new PhabricatorWorkerPermanentFailureException(
pht('No such operation "%s"!', $operation_phid));
}
return $operation;
}
protected function loadCommands($target_phid) {
$viewer = $this->getViewer();

View file

@ -3,24 +3,15 @@
final class HarbormasterBuildActionController
extends HarbormasterController {
private $id;
private $action;
private $via;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->action = $data['action'];
$this->via = idx($data, 'via');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$command = $this->action;
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$action = $request->getURIData('action');
$via = $request->getURIData('via');
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
@ -31,7 +22,7 @@ final class HarbormasterBuildActionController
return new Aphront404Response();
}
switch ($command) {
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$can_issue = $build->canRestartBuild();
break;
@ -48,7 +39,7 @@ final class HarbormasterBuildActionController
return new Aphront400Response();
}
switch ($this->via) {
switch ($via) {
case 'buildable':
$return_uri = '/'.$build->getBuildable()->getMonogram();
break;
@ -66,14 +57,14 @@ final class HarbormasterBuildActionController
$xaction = id(new HarbormasterBuildTransaction())
->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
->setNewValue($command);
->setNewValue($action);
$editor->applyTransactions($build, array($xaction));
return id(new AphrontRedirectResponse())->setURI($return_uri);
}
switch ($command) {
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
if ($can_issue) {
$title = pht('Really restart build?');

View file

@ -3,22 +3,14 @@
final class HarbormasterBuildableActionController
extends HarbormasterController {
private $id;
private $action;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$command = $this->action;
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$action = $request->getURIData('action');
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->withIDs(array($id))
->needBuilds(true)
->requireCapabilities(
array(
@ -33,7 +25,7 @@ final class HarbormasterBuildableActionController
$issuable = array();
foreach ($buildable->getBuilds() as $build) {
switch ($command) {
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
if ($build->canRestartBuild()) {
$issuable[] = $build;
@ -69,7 +61,7 @@ final class HarbormasterBuildableActionController
$xaction = id(new HarbormasterBuildableTransaction())
->setTransactionType(HarbormasterBuildableTransaction::TYPE_COMMAND)
->setNewValue($command);
->setNewValue($action);
$editor->applyTransactions($buildable, array($xaction));
@ -82,14 +74,14 @@ final class HarbormasterBuildableActionController
foreach ($issuable as $build) {
$xaction = id(new HarbormasterBuildTransaction())
->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
->setNewValue($command);
->setNewValue($action);
$build_editor->applyTransactions($build, array($xaction));
}
return id(new AphrontRedirectResponse())->setURI($return_uri);
}
switch ($command) {
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
if ($issuable) {
$title = pht('Really restart all builds?');

View file

@ -2,19 +2,13 @@
final class HarbormasterBuildableListController extends HarbormasterController {
private $queryKey;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->queryKey = idx($data, 'queryKey');
}
public function processRequest() {
public function handleRequest(AphrontRequest $request) {
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($this->queryKey)
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine(new HarbormasterBuildableSearchEngine())
->setNavigation($this->buildSideNavView());

View file

@ -4,7 +4,6 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getviewer();
$id = $request->getURIData('id');
$plan = id(new HarbormasterBuildPlanQuery())

View file

@ -4,11 +4,11 @@ final class HarbormasterStepEditController extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$this->requireApplicationCapability(
HarbormasterManagePlansCapability::CAPABILITY);
$id = $request->getURIData('id');
if ($id) {
$step = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)

View file

@ -67,11 +67,6 @@ final class HarbormasterBuildStepCoreCustomField
$object->setDetail($key, $value);
}
public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) {
return;
}
public function getBuildTargetFieldValue() {
return $this->getProxy()->getFieldValue();
}

View file

@ -28,9 +28,13 @@ final class HarbormasterBuildStepPHIDType extends PhabricatorPHIDType {
foreach ($handles as $phid => $handle) {
$build_step = $objects[$phid];
$id = $build_step->getID();
$name = $build_step->getName();
$handle->setName($name);
$handle
->setName($name)
->setFullName(pht('Build Step %d: %s', $id, $name))
->setURI("/harbormaster/step/{$id}/edit/");
}
}

View file

@ -41,9 +41,14 @@ final class HarbormasterLeaseWorkingCopyBuildStepImplementation
$working_copy_type = id(new DrydockWorkingCopyBlueprintImplementation())
->getType();
$allowed_phids = $build_target->getFieldValue('repositoryPHIDs');
$authorizing_phid = $build_target->getBuildStep()->getPHID();
$lease = DrydockLease::initializeNewLease()
->setResourceType($working_copy_type)
->setOwnerPHID($build_target->getPHID());
->setOwnerPHID($build_target->getPHID())
->setAuthorizingPHID($authorizing_phid)
->setAllowedBlueprintPHIDs($allowed_phids);
$map = $this->buildRepositoryMap($build_target);
@ -104,6 +109,11 @@ final class HarbormasterLeaseWorkingCopyBuildStepImplementation
'type' => 'text',
'required' => true,
),
'blueprintPHIDs' => array(
'name' => pht('Use Blueprints'),
'type' => 'blueprints',
'required' => true,
),
'repositoryPHIDs' => array(
'name' => pht('Also Clone'),
'type' => 'datasource',

View file

@ -3,8 +3,8 @@
final class PhabricatorHomeQuickCreateController
extends PhabricatorHomeController {
public function processRequest() {
$viewer = $this->getRequest()->getUser();
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$items = $this->getCurrentApplication()->loadAllQuickCreateItems($viewer);

View file

@ -34,6 +34,7 @@ final class ManiphestAssignEmailCommand
array $argv) {
$xactions = array();
$assign_phid = null;
$assign_to = head($argv);
if ($assign_to) {

View file

@ -18,7 +18,7 @@ final class PhabricatorMetaMTAMailgunReceiveController
return phutil_hashes_are_identical($sig, $hash);
}
public function processRequest() {
public function handleRequest(AphrontRequest $request) {
// No CSRF for Mailgun.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
@ -28,7 +28,6 @@ final class PhabricatorMetaMTAMailgunReceiveController
pht('Mail signature is not valid. Check your Mailgun API key.'));
}
$request = $this->getRequest();
$user = $request->getUser();
$raw_headers = $request->getStr('headers');

View file

@ -7,12 +7,10 @@ final class PhabricatorMetaMTASendGridReceiveController
return false;
}
public function processRequest() {
public function handleRequest(AphrontRequest $request) {
// No CSRF for SendGrid.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$request = $this->getRequest();
$user = $request->getUser();
$raw_headers = $request->getStr('headers');

View file

@ -265,6 +265,7 @@ final class MultimeterControl extends Phobject {
'init' => true,
'diff' => true,
'cat' => true,
'files' => true,
),
'svnadmin' => array(
'create' => true,

View file

@ -249,6 +249,7 @@ final class PassphraseCredentialEditController extends PassphraseController {
id(new AphrontFormPolicyControl())
->setName('viewPolicy')
->setPolicyObject($credential)
->setSpacePHID($v_space)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicies($policies))
->appendControl(

View file

@ -201,11 +201,7 @@ final class PassphraseCredentialViewController extends PassphraseController {
pht('Description'),
PHUIPropertyListView::ICON_SUMMARY);
$properties->addTextContent(
PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())
->setContent($description),
'default',
$viewer));
new PHUIRemarkupView($viewer, $description));
}
return $properties;

View file

@ -178,4 +178,40 @@ final class PhabricatorObjectQuery
return null;
}
/**
* Select invalid or restricted PHIDs from a list.
*
* PHIDs are invalid if their objects do not exist or can not be seen by the
* viewer. This method is generally used to validate that PHIDs affected by
* a transaction are valid.
*
* @param PhabricatorUser Viewer.
* @param list<phid> List of ostensibly valid PHIDs.
* @return list<phid> List of invalid or restricted PHIDs.
*/
public static function loadInvalidPHIDsForViewer(
PhabricatorUser $viewer,
array $phids) {
if (!$phids) {
return array();
}
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
$invalid = array();
foreach ($phids as $phid) {
if (empty($objects[$phid])) {
$invalid[] = $phid;
}
}
return $invalid;
}
}

View file

@ -32,7 +32,7 @@ final class PonderAnswerSaveController extends PonderController {
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$answer = PonderAnswer::initializeNewAnswer($viewer);
$answer = PonderAnswer::initializeNewAnswer($viewer, $question);
// Question Editor

View file

@ -26,15 +26,18 @@ final class PonderAnswer extends PonderDAO
private $userVotes = array();
public static function initializeNewAnswer(PhabricatorUser $actor) {
public static function initializeNewAnswer(
PhabricatorUser $actor,
PonderQuestion $question) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPonderApplication'))
->executeOne();
return id(new PonderAnswer())
->setQuestionID(0)
->setQuestionID($question->getID())
->setContent('')
->attachQuestion($question)
->setAuthorPHID($actor->getPHID())
->setVoteCount(0)
->setStatus(PonderAnswerStatus::ANSWER_STATUS_VISIBLE);

View file

@ -112,7 +112,7 @@ final class ProjectQueryConduitAPIMethod extends ProjectConduitAPIMethod {
$slug_map = array();
if ($slugs) {
foreach ($slugs as $slug) {
$normal = rtrim(PhabricatorSlug::normalize($slug), '/');
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
foreach ($projects as $project) {
if (in_array($normal, $project['slugs'])) {
$slug_map[$slug] = $project['phid'];

View file

@ -26,10 +26,17 @@ final class PhabricatorProjectViewController
}
$project = $query->executeOne();
if (!$project) {
// If this request corresponds to a project but just doesn't have the
// slug quite right, redirect to the proper URI.
$uri = $this->getNormalizedURI($slug);
if ($uri !== null) {
return id(new AphrontRedirectResponse())->setURI($uri);
}
return new Aphront404Response();
}
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()))
@ -53,4 +60,31 @@ final class PhabricatorProjectViewController
return $this->delegateToController($controller_object);
}
private function getNormalizedURI($slug) {
if (!strlen($slug)) {
return null;
}
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
if ($normal === $slug) {
return null;
}
$viewer = $this->getViewer();
// Do execute() instead of executeOne() here so we canonicalize before
// raising a policy exception. This is a little more polished than letting
// the user hit the error on any variant of the slug.
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withSlugs(array($normal))
->execute();
if (!$projects) {
return null;
}
return "/tag/{$normal}/";
}
}

View file

@ -81,9 +81,9 @@ final class PhabricatorProjectTransactionEditor
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
$object->setName($xaction->getNewValue());
// TODO - this is really "setPrimarySlug"
$object->setPhrictionSlug($xaction->getNewValue());
$name = $xaction->getNewValue();
$object->setName($name);
$object->setPrimarySlug(PhabricatorSlug::normalizeProjectSlug($name));
return;
case PhabricatorProjectTransaction::TYPE_SLUGS:
return;
@ -265,9 +265,8 @@ final class PhabricatorProjectTransactionEditor
$errors[] = $error;
}
$slug_builder = clone $object;
$slug_builder->setPhrictionSlug($name);
$slug = $slug_builder->getPrimarySlug();
$slug = PhabricatorSlug::normalizeProjectSlug($name);
$slug_used_already = id(new PhabricatorProjectSlug())
->loadOneWhere('slug = %s', $slug);
if ($slug_used_already &&
@ -498,9 +497,7 @@ final class PhabricatorProjectTransactionEditor
PhabricatorLiskDAO $object,
$name) {
$object = (clone $object);
$object->setPhrictionSlug($name);
$slug = $object->getPrimarySlug();
$slug = PhabricatorSlug::normalizeProjectSlug($name);
$slug_object = id(new PhabricatorProjectSlug())->loadOneWhere(
'slug = %s',

View file

@ -7,7 +7,6 @@ final class PhabricatorProjectQuery
private $phids;
private $memberPHIDs;
private $slugs;
private $phrictionSlugs;
private $names;
private $nameTokens;
private $icons;
@ -50,11 +49,6 @@ final class PhabricatorProjectQuery
return $this;
}
public function withPhrictionSlugs(array $slugs) {
$this->phrictionSlugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
@ -308,13 +302,6 @@ final class PhabricatorProjectQuery
$this->slugs);
}
if ($this->phrictionSlugs !== null) {
$where[] = qsprintf(
$conn,
'phrictionSlug IN (%Ls)',
$this->phrictionSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,

View file

@ -189,24 +189,11 @@ final class PhabricatorProject extends PhabricatorProjectDAO
return $this->assertAttached($this->memberPHIDs);
}
public function setPhrictionSlug($slug) {
// NOTE: We're doing a little magic here and stripping out '/' so that
// project pages always appear at top level under projects/ even if the
// display name is "Hack / Slash" or similar (it will become
// 'hack_slash' instead of 'hack/slash').
$slug = str_replace('/', ' ', $slug);
$slug = PhabricatorSlug::normalize($slug);
$this->phrictionSlug = $slug;
public function setPrimarySlug($slug) {
$this->phrictionSlug = $slug.'/';
return $this;
}
public function getFullPhrictionSlug() {
$slug = $this->getPhrictionSlug();
return 'projects/'.$slug;
}
// TODO - once we sever project => phriction automagicalness,
// migrate getPhrictionSlug to have no trailing slash and be called
// getPrimarySlug

View file

@ -19,4 +19,22 @@ final class PhabricatorRepositoryVersion extends Phobject {
return null;
}
/**
* The `locate` command is deprecated as of Mercurial 3.2, to be
* replaced with `files` command, which supports most of the same
* arguments. This determines whether the new `files` command should
* be used instead of the `locate` command.
*
* @param string $mercurial_version - The current version of mercurial
* which can be retrieved by calling:
* PhabricatorRepositoryVersion::getMercurialVersion()
*
* @return boolean True if the version of Mercurial is new enough to support
* the `files` command, or false if otherwise.
*/
public static function isMercurialFilesCommandAvailable($mercurial_version) {
$min_version_for_files = '3.2';
return version_compare($mercurial_version, $min_version_for_files, '>=');
}
}

View file

@ -44,6 +44,7 @@ final class PhabricatorRepositoryEditor
$types[] = PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE;
$types[] = PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES;
$types[] = PhabricatorRepositoryTransaction::TYPE_STAGING_URI;
$types[] = PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
@ -107,6 +108,8 @@ final class PhabricatorRepositoryEditor
return $object->getSymbolSources();
case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
return $object->getDetail('staging-uri');
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
return $object->getDetail('automation.blueprintPHIDs', array());
}
}
@ -143,6 +146,7 @@ final class PhabricatorRepositoryEditor
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
return $xaction->getNewValue();
case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
@ -226,6 +230,11 @@ final class PhabricatorRepositoryEditor
case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
$object->setDetail('staging-uri', $xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
$object->setDetail(
'automation.blueprintPHIDs',
$xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_ENCODING:
// 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
@ -276,33 +285,17 @@ final class PhabricatorRepositoryEditor
$editor->save();
break;
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
DrydockAuthorization::applyAuthorizationChanges(
$this->getActor(),
$object->getPHID(),
$xaction->getOldValue(),
$xaction->getNewValue());
break;
}
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
switch ($type) {}
return parent::mergeTransactions($u, $v);
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$type = $xaction->getTransactionType();
switch ($type) {}
return parent::transactionHasEffect($object, $xaction);
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
@ -338,6 +331,7 @@ final class PhabricatorRepositoryEditor
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
@ -431,6 +425,29 @@ final class PhabricatorRepositoryEditor
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
foreach ($xactions as $xaction) {
$old = nonempty($xaction->getOldValue(), array());
$new = nonempty($xaction->getNewValue(), array());
$add = array_diff($new, $old);
$invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer(
$this->getActor(),
$add);
if ($invalid) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Some of the selected automation blueprints are invalid '.
'or restricted: %s.',
implode(', ', $invalid)),
$xaction);
}
}
break;
}
return $errors;

View file

@ -1799,7 +1799,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
/* -( Staging )-------------------------------------------------------------*/
/* -( Staging )------------------------------------------------------------ */
public function supportsStaging() {
@ -1815,6 +1815,33 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
/* -( Automation )--------------------------------------------------------- */
public function supportsAutomation() {
return $this->isGit();
}
public function canPerformAutomation() {
if (!$this->supportsAutomation()) {
return false;
}
if (!$this->getAutomationBlueprintPHIDs()) {
return false;
}
return true;
}
public function getAutomationBlueprintPHIDs() {
if (!$this->supportsAutomation()) {
return array();
}
return $this->getDetail('automation.blueprintPHIDs', array());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */

View file

@ -28,6 +28,7 @@ final class PhabricatorRepositoryTransaction
const TYPE_SYMBOLS_SOURCES = 'repo:symbol-source';
const TYPE_SYMBOLS_LANGUAGE = 'repo:symbol-language';
const TYPE_STAGING_URI = 'repo:staging-uri';
const TYPE_AUTOMATION_BLUEPRINTS = 'repo:automation-blueprints';
// TODO: Clean up these legacy transaction types.
const TYPE_SSH_LOGIN = 'repo:ssh-login';
@ -65,6 +66,7 @@ final class PhabricatorRepositoryTransaction
}
break;
case self::TYPE_SYMBOLS_SOURCES:
case self::TYPE_AUTOMATION_BLUEPRINTS:
if ($old) {
$phids = array_merge($phids, $old);
}
@ -436,6 +438,34 @@ final class PhabricatorRepositoryTransaction
$old,
$new);
}
case self::TYPE_AUTOMATION_BLUEPRINTS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed %s automation blueprint(s), '.
'added %s: %s; removed %s: %s.',
$this->renderHandleLink($author_phid),
new PhutilNumber(count($add) + count($rem)),
new PhutilNumber(count($add)),
$this->renderHandleList($add),
new PhutilNumber(count($rem)),
$this->renderHandleList($rem));
} else if ($add) {
return pht(
'%s added %s automation blueprint(s): %s.',
$this->renderHandleLink($author_phid),
new PhutilNumber(count($add)),
$this->renderHandleList($add));
} else {
return pht(
'%s removed %s automation blueprint(s): %s.',
$this->renderHandleLink($author_phid),
new PhutilNumber(count($rem)),
$this->renderHandleList($rem));
}
}
return parent::getTitle();

View file

@ -58,3 +58,12 @@ a corresponding resource by either finding a suitable unused resource or
creating a new resource. When work completes, the resource is returned to the
resource pool or destroyed.
Next Steps
==========
Continue by:
- understanding Drydock security concerns with
@{article:Drydock User Guide: Security}; or
- allowing Phabricator to write to repositories with
@{article:Drydock User Guide: Repository Automation}.

View file

@ -0,0 +1,39 @@
@title Drydock User Guide: Repository Automation
@group userguide
Configuring repository automation so Phabricator can push commits.
Overview
========
IMPORTANT: This feature is very new and most of the capabilities described
in this document are not yet available. This feature as a whole is a prototype.
By configuring Drydock and Diffusion appropriately, you can enable **Repository
Automation** for a repository. Once automation is set up, Phabricator will be
able to make changes to the repository.
Security
========
Configuring repository automation amounts to telling Phabricator where it
should perform working copy operations (like merges, cherry-picks and pushes)
when doing writes.
Depending on how stringent you are about change control, you may want to
make sure these processes are isolated and can not be tampered with. If you
run tests and automation on the same hardware, tests may be able to interfere
with automation. You can read more about this in
@{article:Drydock User Guide: Security}.
Next Steps
==========
Continue by:
- understanding Drydock security concerns with
@{article:Drydock User Guide: Security}; or
- returning to the @{article:Drydock User Guide}.

View file

@ -0,0 +1,209 @@
@title Drydock User Guide: Security
@group userguide
Understanding security concerns in Drydock.
Overview
========
Different applications use Drydock for different things, and some of the things
they do with Drydock require different levels of trust and access. It is
important to configure Drydock properly so that less trusted code can't do
anything you aren't comfortable with.
For example, running unit tests on Drydock normally involves running relatively
untrusted code (it often has a single author and has not yet been reviewed)
that needs very few capabilities (generally, it only needs to be able to report
results back to Phabricator). In contrast, automating merge requests on Drydock
involves running trusted code that needs more access (it must be able to write
to repositories).
Drydock allows resources to be shared and reused, so it's possible to configure
Drydock in a way that gives untrusted code a lot of access by accident.
One way Drydock makes allocations faster is by sharing, reusing, and recycling
resources. When an application asks Drydock for a working copy, it will try to
satisfy the request by cleaning up a suitable existing working copy if it can,
instead of building a new one. This is faster, but it means that tasks have
some ability to interact or interfere with each other.
Similarly, Drydock may allocate multiple leases on the same host at the same
time, running as the same user. This is generally simpler to configure and less
wasteful than fully isolating leases, but means that they can interact.
Depending on your organization, environment and use cases, you might not want
this, and it may be important that different use cases are unable to interfere
with each other. For example, you might want to prevent unit tests from writing
to repositories.
**Drydock does not guarantee that resources are isolated by default**. When
resources are more isolated, they are usually also harder to configure and
slower to allocate. Because most installs will want to find a balance between
isolation and complexity/performance, Drydock does not make assumptions about
either isolation or performance having absolute priority.
You'll usually want to isolate things just enough that nothing bad can happen.
Fortunately, this is straightforward. This document describes how to make sure
you have enough isolation so that nothing you're uncomfortable with can occur.
Choosing an Isolation Policy
============================
This section provides some reasonable examples of ways you might approach
configuring Drydock.
| Isolation | Suitable For | Description
|-----------|-----|-------
| Zero | Development | Everything on one host.
| Low | Small Installs | Use a dedicated Drydock host.
| High | Most Installs | **Recommended**. Use low-trust and high-trust pools.
| Custom | Special Requirements | Use multiple pools.
| Absolute | Special Requirements | Completely isolate all resources.
**Zero Isolation**: Run Drydock operations on the same host that Phabricator
runs on. This is only suitable for developing or testing Phabricator. Any
Drydock operation can potentially compromise Phabricator. It is intentionally
difficult to configure Drydock to operate in this mode. Running Drydock
operations on the Phabricator host is strongly discouraged.
**Low Isolation**: Designate a separate Drydock host and run Drydock
operations on it. This is suitable for small installs and provides a reasonable
level of isolation. However, it will allow unit tests (which often run
lower-trust code) to interfere with repository automation operations.
**High Isolation**: Designate two Drydock host pools and run low-trust
operations (like builds) on one pool and high-trust operations (like repository
automation) on a separate pool. This provides a good balance between isolation
and performance, although tests can still potentially interfere with the
execution of unrelated tests.
**Custom Isolation**: You can continue adding pools to refine the resource
isolation model. For example, you may have higher-trust and lower-trust
repositories or do builds on a mid-trust tier which runs only reviewed code.
**Absolute Isolation**: Configure blueprints to completely initialize and
destroy hosts or containers on every request, and limit all resources to one
simultaneous lease. This will completely isolate every operation, but come at
a high performance and complexity cost.
NOTE: It is not currently possible to configure Drydock in an absolute
isolation mode.
It is usually reasonable to choose one of these approaches as a starting point
and then adjust it to fit your requirements. You can also evolve your use of
Drydock over time as your needs change.
Threat Scenarios
================
This section will help you understand the threats to a Drydock environment.
Not all threats will be concerning to all installs, and you can choose an
approach which defuses only the threats you care about.
Attackers have three primary targets:
- capturing hosts;
- compromising Phabricator; and
- compromising the integrity of other Drydock processes.
**Attacks against hosts** are the least sophisticated. In this scenario, an
attacker wants to run a program like a Bitcoin miner or botnet client on
hardware that they aren't paying for or which can't be traced to them. They
write a "unit test" or which launches this software, then send a revision
containing this "unit test" for review. If Phabricator is configured to
automatically run tests on new revisions, it may execute automatically and give
the attacker access to computing resources they did not previously control and
which can not easily be traced back to them.
This is usually only a meaningful threat for open source installs, because
there is a high probability of eventual detection and the value of these
resources is small, so employees will generally not have an incentive to
attempt this sort of attack. The easiest way to prevent this attack is to
prevent untrusted, anonymous contributors from running tests. For example,
create a "Trusted Contributors" project and only run tests if a revision author
is a member of the project.
**Attacks against Phabricator** are more sophisticated. In this scenario, an
attacker tries to compromise Phabricator itself (for example, to make themselves
an administrator or gain access to an administrator account).
This is made possible if Drydock is running on the same host as Phabricator or
runs on a privileged subnet with access to resources like Phabricator database
hosts. Most installs should be concerned about this attack.
The best way to defuse this attack is to run Drydock processes on a separate
host which is not on a privileged subnet. For example, use a
`build.mycompany.com` host or pool for Drydock processes, separate from your
`phabricator.mycompany.com` host or pool.
Even if the host is not privileged, many Drydock processes have some level of
privilege (enabling them to clone repositories, or report test results back to
Phabricator). Be aware that tests can hijack credentials they are run with,
and potentialy hijack credentials given to other processes on the same hosts.
You should use credentials with a minimum set of privileges and assume all
processes on a host have the highest level of access that any process on the
host has.
**Attacks against Drydock** are the most sophisticated. In this scenario, an
attacker uses one Drydock process to compromise a different process: for
example, a unit test which tampers with a merge or injects code into a build.
This might allow an attacker to make changes to a repository or binary without
going through review or triggering other rules which would normally detect the
change.
These attackers could also make failing tests appear to pass, or break tests or
builds, but these attacks are generally less interesting than tampering with
a repository or binary.
This is a complex attack which you may not have to worry about unless you have
a high degree of process and control in your change pipeline. If users can push
changes directly to repositories, this often represents a faster and easier way
to achieve the same tampering.
The best way to defuse this attack is to prevent high-trust (repository
automation) processes from running on the same hosts as low-trust (unit test)
processes. For example, use an `automation.mycompany.com` host or pool for
repository automation, and a `build.mycompany.com` host or pool for tests.
Applying an Isolation Policy
============================
Designing a security and isolation policy for Drydock can take some thought,
but applying it is straightforward. Applications which want to use Drydock must
explicitly list which blueprints they are allowed to use, and they must be
approved to use them in Drydock. By default, nothing can do anything, which is
very safe and secure.
To get builds or automation running on a host, specify the host blueprint as a
usable blueprint in the build step or repository configuration. This creates a
new authorization request in Drydock which must be approved before things can
move forward.
Until the authorization is approved, the process can not use the blueprint to
create any resources, nor can it use resources previously created by the
blueprint.
You can review and approve requests from the blueprint detail view in Drydock:
find the request and click {nav Approve Authorization}. You can also revoke
approval at any time from this screen which will prevent the object from
continuing to use the blueprint (but note that this does not release any
existing leases).
Once the authorization request is approved, the build or automation process
should be able to run if everything else is configured properly.
Note that authorizations are transitive: if a build step is authorized to use
blueprint A, and blueprint A is authorized to use blueprint B, the build step
may indirectly operate on resources created by blueprint B. This should
normally be consistent with expectations.
Next Steps
==========
Continue by:
- returning to the @{article:Drydock User Guide}.

View file

@ -540,9 +540,7 @@ abstract class PhabricatorCustomField extends Phobject {
* @task storage
*/
public function newStorageObject() {
if ($this->proxy) {
return $this->proxy->newStorageObject();
}
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}

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