1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-28 16:30:59 +01:00

Merge branch 'master' into redesign-2015

This commit is contained in:
epriestley 2015-06-23 13:36:47 -07:00
commit 075f041bec
51 changed files with 2624 additions and 425 deletions

View file

@ -0,0 +1,15 @@
CREATE TABLE {$NAMESPACE}_worker.worker_bulkjob (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
jobTypeKey VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
status VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
parameters LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
size INT UNSIGNED NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (phid),
KEY `key_type` (jobTypeKey),
KEY `key_author` (authorPHID),
KEY `key_status` (status)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,9 @@
CREATE TABLE {$NAMESPACE}_worker.worker_bulktask (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
bulkJobPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
status VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
data LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
KEY `key_job` (bulkJobPHID, status),
KEY `key_object` (objectPHID)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

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

View file

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

View file

@ -379,6 +379,7 @@ phutil_register_library_map(array(
'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php',
'DifferentialGitHubLandingStrategy' => 'applications/differential/landing/DifferentialGitHubLandingStrategy.php',
'DifferentialGitSVNIDField' => 'applications/differential/customfield/DifferentialGitSVNIDField.php',
'DifferentialHarbormasterField' => 'applications/differential/customfield/DifferentialHarbormasterField.php',
'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php',
'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php',
'DifferentialHostedGitLandingStrategy' => 'applications/differential/landing/DifferentialHostedGitLandingStrategy.php',
@ -893,6 +894,7 @@ phutil_register_library_map(array(
'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php',
'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php',
'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php',
'HarbormasterManagePlansCapability' => 'applications/harbormaster/capability/HarbormasterManagePlansCapability.php',
'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php',
@ -921,6 +923,7 @@ phutil_register_library_map(array(
'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php',
'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php',
'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
'HarbormasterUnitMessagesController' => 'applications/harbormaster/controller/HarbormasterUnitMessagesController.php',
'HarbormasterUnitPropertyView' => 'applications/harbormaster/view/HarbormasterUnitPropertyView.php',
'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php',
'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php',
@ -1082,6 +1085,7 @@ phutil_register_library_map(array(
'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php',
'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php',
'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php',
'ManiphestTaskEditBulkJobType' => 'applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php',
'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php',
'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php',
'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php',
@ -1708,6 +1712,9 @@ phutil_register_library_map(array(
'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php',
'PhabricatorCustomHeaderConfigType' => 'applications/config/custom/PhabricatorCustomHeaderConfigType.php',
'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php',
'PhabricatorDaemonBulkJobListController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobListController.php',
'PhabricatorDaemonBulkJobMonitorController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php',
'PhabricatorDaemonBulkJobViewController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php',
'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php',
'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php',
'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php',
@ -2111,10 +2118,14 @@ phutil_register_library_map(array(
'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php',
'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php',
'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php',
'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'applications/metamta/edge/PhabricatorMetaMTAMailHasRecipientEdgeType.php',
'PhabricatorMetaMTAMailListController' => 'applications/metamta/controller/PhabricatorMetaMTAMailListController.php',
'PhabricatorMetaMTAMailPHIDType' => 'applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php',
'PhabricatorMetaMTAMailQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailQuery.php',
'PhabricatorMetaMTAMailSearchEngine' => 'applications/metamta/query/PhabricatorMetaMTAMailSearchEngine.php',
'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php',
'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php',
'PhabricatorMetaMTAMailViewController' => 'applications/metamta/controller/PhabricatorMetaMTAMailViewController.php',
'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php',
'PhabricatorMetaMTAMailableFunctionDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php',
'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php',
@ -2815,6 +2826,19 @@ phutil_register_library_map(array(
'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php',
'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php',
'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php',
'PhabricatorWorkerBulkJob' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php',
'PhabricatorWorkerBulkJobCreateWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobCreateWorker.php',
'PhabricatorWorkerBulkJobEditor' => 'infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php',
'PhabricatorWorkerBulkJobPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerBulkJobPHIDType.php',
'PhabricatorWorkerBulkJobQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobQuery.php',
'PhabricatorWorkerBulkJobSearchEngine' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobSearchEngine.php',
'PhabricatorWorkerBulkJobTaskWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobTaskWorker.php',
'PhabricatorWorkerBulkJobTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerBulkJobTestCase.php',
'PhabricatorWorkerBulkJobTransaction' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJobTransaction.php',
'PhabricatorWorkerBulkJobTransactionQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobTransactionQuery.php',
'PhabricatorWorkerBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php',
'PhabricatorWorkerBulkJobWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php',
'PhabricatorWorkerBulkTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkTask.php',
'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php',
'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php',
'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php',
@ -2824,6 +2848,7 @@ phutil_register_library_map(array(
'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php',
'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php',
'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php',
'PhabricatorWorkerSchemaSpec' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php',
'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php',
'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php',
'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php',
@ -3744,6 +3769,7 @@ phutil_register_library_map(array(
'DifferentialGetWorkingCopy' => 'Phobject',
'DifferentialGitHubLandingStrategy' => 'DifferentialLandingStrategy',
'DifferentialGitSVNIDField' => 'DifferentialCustomField',
'DifferentialHarbormasterField' => 'DifferentialCustomField',
'DifferentialHiddenComment' => 'DifferentialDAO',
'DifferentialHostField' => 'DifferentialCustomField',
'DifferentialHostedGitLandingStrategy' => 'DifferentialLandingStrategy',
@ -3769,7 +3795,7 @@ phutil_register_library_map(array(
'DifferentialLandingStrategy' => 'Phobject',
'DifferentialLegacyHunk' => 'DifferentialHunk',
'DifferentialLineAdjustmentMap' => 'Phobject',
'DifferentialLintField' => 'DifferentialCustomField',
'DifferentialLintField' => 'DifferentialHarbormasterField',
'DifferentialLintStatus' => 'Phobject',
'DifferentialLocalCommitsView' => 'AphrontView',
'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField',
@ -3845,7 +3871,7 @@ phutil_register_library_map(array(
'DifferentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'DifferentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DifferentialTransactionView' => 'PhabricatorApplicationTransactionView',
'DifferentialUnitField' => 'DifferentialCustomField',
'DifferentialUnitField' => 'DifferentialHarbormasterField',
'DifferentialUnitStatus' => 'Phobject',
'DifferentialUnitTestResult' => 'Phobject',
'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
@ -4356,6 +4382,7 @@ phutil_register_library_map(array(
'HarbormasterDAO' => 'PhabricatorLiskDAO',
'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterLintMessagesController' => 'HarbormasterController',
'HarbormasterLintPropertyView' => 'AphrontView',
'HarbormasterManagePlansCapability' => 'PhabricatorPolicyCapability',
'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow',
@ -4384,6 +4411,7 @@ phutil_register_library_map(array(
'HarbormasterTargetWorker' => 'HarbormasterWorker',
'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation',
'HarbormasterUIEventListener' => 'PhabricatorEventListener',
'HarbormasterUnitMessagesController' => 'HarbormasterController',
'HarbormasterUnitPropertyView' => 'AphrontView',
'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
@ -4584,6 +4612,7 @@ phutil_register_library_map(array(
'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDetailController' => 'ManiphestController',
'ManiphestTaskEditBulkJobType' => 'PhabricatorWorkerBulkJobType',
'ManiphestTaskEditController' => 'ManiphestController',
'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType',
@ -5295,6 +5324,9 @@ phutil_register_library_map(array(
'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomHeaderConfigType' => 'PhabricatorConfigOptionType',
'PhabricatorDaemon' => 'PhutilDaemon',
'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonController',
'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonController',
'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonController',
'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController',
'PhabricatorDaemonController' => 'PhabricatorController',
'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO',
@ -5759,10 +5791,14 @@ phutil_register_library_map(array(
),
'PhabricatorMetaMTAMailBody' => 'Phobject',
'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMetaMTAMailListController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMetaMTAMailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAMailSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorMetaMTAMailSection' => 'Phobject',
'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAMailViewController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorMetaMTAMailableFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController',
@ -6593,6 +6629,25 @@ phutil_register_library_map(array(
'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorQuery',
'PhabricatorWorkerBulkJob' => array(
'PhabricatorWorkerDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorWorkerBulkJobCreateWorker' => 'PhabricatorWorkerBulkJobWorker',
'PhabricatorWorkerBulkJobEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorWorkerBulkJobPHIDType' => 'PhabricatorPHIDType',
'PhabricatorWorkerBulkJobQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorWorkerBulkJobSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorWorkerBulkJobTaskWorker' => 'PhabricatorWorkerBulkJobWorker',
'PhabricatorWorkerBulkJobTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerBulkJobTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorWorkerBulkJobTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorWorkerBulkJobType' => 'Phobject',
'PhabricatorWorkerBulkJobWorker' => 'PhabricatorWorker',
'PhabricatorWorkerBulkTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery',
'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow',
@ -6602,6 +6657,7 @@ phutil_register_library_map(array(
'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorWorkerPermanentFailureException' => 'Exception',
'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController',

View file

@ -46,6 +46,15 @@ final class PhabricatorDaemonsApplication extends PhabricatorApplication {
'(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogViewController',
),
'event/(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogEventViewController',
'bulk/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' =>
'PhabricatorDaemonBulkJobListController',
'monitor/(?P<id>\d+)/' =>
'PhabricatorDaemonBulkJobMonitorController',
'view/(?P<id>\d+)/' =>
'PhabricatorDaemonBulkJobViewController',
),
),
);
}

View file

@ -0,0 +1,31 @@
<?php
final class PhabricatorDaemonBulkJobListController
extends PhabricatorDaemonController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine(new PhabricatorWorkerBulkJobSearchEngine())
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
protected function buildSideNavView($for_app = false) {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new PhabricatorWorkerBulkJobSearchEngine())
->setViewer($user)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
}

View file

@ -0,0 +1,165 @@
<?php
final class PhabricatorDaemonBulkJobMonitorController
extends PhabricatorDaemonController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$job = id(new PhabricatorWorkerBulkJobQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$job) {
return new Aphront404Response();
}
// If the user clicks "Continue" on a completed job, take them back to
// whatever application sent them here.
if ($request->getStr('done')) {
if ($request->isFormPost()) {
$done_uri = $job->getDoneURI();
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
}
$title = pht('Bulk Job %d', $job->getID());
if ($job->getStatus() == PhabricatorWorkerBulkJob::STATUS_CONFIRM) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$job,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
if ($request->isFormPost()) {
$type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
$xactions = array();
$xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
->setTransactionType($type_status)
->setNewValue(PhabricatorWorkerBulkJob::STATUS_WAITING);
$editor = id(new PhabricatorWorkerBulkJobEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->applyTransactions($job, $xactions);
return id(new AphrontRedirectResponse())
->setURI($job->getMonitorURI());
} else {
return $this->newDialog()
->setTitle(pht('Confirm Bulk Job'))
->appendParagraph($job->getDescriptionForConfirm())
->appendParagraph(
pht('Start work on this bulk job?'))
->addCancelButton($job->getManageURI(), pht('Details'))
->addSubmitButton(pht('Start Work'));
}
} else {
return $this->newDialog()
->setTitle(pht('Waiting For Confirmation'))
->appendParagraph(
pht(
'This job is waiting for confirmation before work begins.'))
->addCancelButotn($job->getManageURI(), pht('Details'));
}
}
$dialog = $this->newDialog()
->setTitle(pht('%s: %s', $title, $job->getStatusName()))
->addCancelButton($job->getManageURI(), pht('Details'));
switch ($job->getStatus()) {
case PhabricatorWorkerBulkJob::STATUS_WAITING:
$dialog->appendParagraph(
pht('This job is waiting for tasks to be queued.'));
break;
case PhabricatorWorkerBulkJob::STATUS_RUNNING:
$dialog->appendParagraph(
pht('This job is running.'));
break;
case PhabricatorWorkerBulkJob::STATUS_COMPLETE:
$dialog->appendParagraph(
pht('This job is complete.'));
break;
}
$counts = $job->loadTaskStatusCounts();
if ($counts) {
$dialog->appendParagraph($this->renderProgress($counts));
}
switch ($job->getStatus()) {
case PhabricatorWorkerBulkJob::STATUS_COMPLETE:
$dialog->addHiddenInput('done', true);
$dialog->addSubmitButton(pht('Continue'));
break;
default:
Javelin::initBehavior('bulk-job-reload');
break;
}
return $dialog;
}
private function renderProgress(array $counts) {
$this->requireResource('bulk-job-css');
$states = array(
PhabricatorWorkerBulkTask::STATUS_DONE => array(
'class' => 'bulk-job-progress-slice-green',
),
PhabricatorWorkerBulkTask::STATUS_RUNNING => array(
'class' => 'bulk-job-progress-slice-blue',
),
PhabricatorWorkerBulkTask::STATUS_WAITING => array(
'class' => 'bulk-job-progress-slice-empty',
),
PhabricatorWorkerBulkTask::STATUS_FAIL => array(
'class' => 'bulk-job-progress-slice-red',
),
);
$total = array_sum($counts);
$offset = 0;
$bars = array();
foreach ($states as $state => $spec) {
$size = idx($counts, $state, 0);
if (!$size) {
continue;
}
$classes = array();
$classes[] = 'bulk-job-progress-slice';
$classes[] = $spec['class'];
$width = ($size / $total);
$bars[] = phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
'style' =>
'left: '.sprintf('%.2f%%', 100 * $offset).'; '.
'width: '.sprintf('%.2f%%', 100 * $width).';',
),
'');
$offset += $width;
}
return phutil_tag(
'div',
array(
'class' => 'bulk-job-progress-bar',
),
$bars);
}
}

View file

@ -0,0 +1,83 @@
<?php
final class PhabricatorDaemonBulkJobViewController
extends PhabricatorDaemonController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$job = id(new PhabricatorWorkerBulkJobQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$job) {
return new Aphront404Response();
}
$title = pht('Bulk Job %d', $job->getID());
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Bulk Jobs'), '/daemon/bulk/');
$crumbs->addTextCrumb($title);
$properties = $this->renderProperties($job);
$actions = $this->renderActions($job);
$properties->setActionList($actions);
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->addPropertyList($properties);
$timeline = $this->buildTransactionTimeline(
$job,
new PhabricatorWorkerBulkJobTransactionQuery());
$timeline->setShouldTerminate(true);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$timeline,
),
array(
'title' => $title,
));
}
private function renderProperties(PhabricatorWorkerBulkJob $job) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($job);
$view->addProperty(
pht('Author'),
$viewer->renderHandle($job->getAuthorPHID()));
$view->addProperty(pht('Status'), $job->getStatusName());
return $view;
}
private function renderActions(PhabricatorWorkerBulkJob $job) {
$viewer = $this->getViewer();
$actions = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($job);
$actions->addAction(
id(new PhabricatorActionView())
->setHref($job->getDoneURI())
->setIcon('fa-arrow-circle-o-right')
->setName(pht('Continue')));
return $actions;
}
}

View file

@ -10,6 +10,9 @@ abstract class PhabricatorDaemonController extends PhabricatorController {
$nav->addFilter('/', pht('Console'));
$nav->addFilter('log', pht('All Daemons'));
$nav->addLabel(pht('Bulk Jobs'));
$nav->addFilter('bulk', pht('Manage Bulk Jobs'));
return $nav;
}

View file

@ -0,0 +1,107 @@
<?php
abstract class DifferentialHarbormasterField
extends DifferentialCustomField {
abstract protected function getDiffPropertyKeys();
abstract protected function loadHarbormasterTargetMessages(
array $target_phids);
abstract protected function getLegacyProperty();
abstract protected function newModernMessage(array $message);
abstract protected function renderHarbormasterStatus(
DifferentialDiff $diff,
array $messages);
abstract protected function newHarbormasterMessageView(array $messages);
public function renderDiffPropertyViewValue(DifferentialDiff $diff) {
// TODO: This load is slightly inefficient, but most of this is moving
// to Harbormaster and this simplifies the transition. Eat 1-2 extra
// queries for now.
$keys = $this->getDiffPropertyKeys();
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d AND name IN (%Ls)',
$diff->getID(),
$keys);
$properties = mpull($properties, 'getData', 'getName');
foreach ($keys as $key) {
$diff->attachProperty($key, idx($properties, $key));
}
$messages = array();
$buildable = $diff->getBuildable();
if ($buildable) {
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
if ($target_phids) {
$messages = $this->loadHarbormasterTargetMessages($target_phids);
}
}
if (!$messages) {
// No Harbormaster messages, so look for legacy messages and make them
// look like modern messages.
$legacy_messages = $diff->getProperty($this->getLegacyProperty());
if ($legacy_messages) {
// Show the top 100 legacy lint messages. Previously, we showed some
// by default and let the user toggle the rest. With modern messages,
// we can send the user to the Harbormaster detail page. Just show
// "a lot" of messages in legacy cases to try to strike a balance
// between implementation simplicitly and compatibility.
$legacy_messages = array_slice($legacy_messages, 0, 100);
foreach ($legacy_messages as $message) {
try {
$modern = $this->newModernMessage($message);
$messages[] = $modern;
} catch (Exception $ex) {
// Ignore any poorly formatted messages.
}
}
}
}
$status = $this->renderHarbormasterStatus($diff, $messages);
if ($messages) {
$path_map = mpull($diff->loadChangesets(), 'getID', 'getFilename');
foreach ($path_map as $path => $id) {
$href = '#C'.$id.'NL';
// TODO: When the diff is not the right-hand-size diff, we should
// ideally adjust this URI to be absolute.
$path_map[$path] = $href;
}
$view = $this->newHarbormasterMessageView($messages);
if ($view) {
$view->setPathURIMap($path_map);
}
} else {
$view = null;
}
if ($view) {
$view = phutil_tag(
'div',
array(
'class' => 'differential-harbormaster-table-view',
),
$view);
}
return array(
$status,
$view,
);
}
}

View file

@ -1,7 +1,7 @@
<?php
final class DifferentialLintField
extends DifferentialCustomField {
extends DifferentialHarbormasterField {
public function getFieldKey() {
return 'differential:lint';
@ -31,91 +31,33 @@ final class DifferentialLintField
return $this->getFieldName();
}
public function renderDiffPropertyViewValue(DifferentialDiff $diff) {
// TODO: This load is slightly inefficient, but most of this is moving
// to Harbormaster and this simplifies the transition. Eat 1-2 extra
// queries for now.
$keys = array(
protected function getLegacyProperty() {
return 'arc:lint';
}
protected function getDiffPropertyKeys() {
return array(
'arc:lint',
'arc:lint-excuse',
);
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d AND name IN (%Ls)',
$diff->getID(),
$keys);
$properties = mpull($properties, 'getData', 'getName');
protected function loadHarbormasterTargetMessages(array $target_phids) {
return id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls) LIMIT 25',
$target_phids);
}
foreach ($keys as $key) {
$diff->attachProperty($key, idx($properties, $key));
}
protected function newHarbormasterMessageView(array $messages) {
return id(new HarbormasterLintPropertyView())
->setLimit(25)
->setLintMessages($messages);
}
$status = $this->renderLintStatus($diff);
$lint = array();
$buildable = $diff->getBuildable();
if ($buildable) {
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
$lint = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls) LIMIT 25',
$target_phids);
}
if (!$lint) {
// No Harbormaster messages, so look for legacy messages and make them
// look like modern messages.
$legacy_lint = $diff->getProperty('arc:lint');
if ($legacy_lint) {
// Show the top 100 legacy lint messages. Previously, we showed some
// by default and let the user toggle the rest. With modern messages,
// we can send the user to the Harbormaster detail page. Just show
// "a lot" of messages in legacy cases to try to strike a balance
// between implementation simplicitly and compatibility.
$legacy_lint = array_slice($legacy_lint, 0, 100);
$target = new HarbormasterBuildTarget();
foreach ($legacy_lint as $message) {
try {
$modern = HarbormasterBuildLintMessage::newFromDictionary(
$target,
$this->getModernLintMessageDictionary($message));
$lint[] = $modern;
} catch (Exception $ex) {
// Ignore any poorly formatted messages.
}
}
}
}
if ($lint) {
$path_map = mpull($diff->loadChangesets(), 'getID', 'getFilename');
foreach ($path_map as $path => $id) {
$href = '#C'.$id.'NL';
// TODO: When the diff is not the right-hand-size diff, we should
// ideally adjust this URI to be absolute.
$path_map[$path] = $href;
}
$view = id(new HarbormasterLintPropertyView())
->setPathURIMap($path_map)
->setLintMessages($lint);
} else {
$view = null;
}
return array(
$status,
$view,
);
protected function newModernMessage(array $message) {
return HarbormasterBuildLintMessage::newFromDictionary(
new HarbormasterBuildTarget(),
$this->getModernLintMessageDictionary($message));
}
public function getWarningsForDetailView() {
@ -141,7 +83,10 @@ final class DifferentialLintField
return $warnings;
}
private function renderLintStatus(DifferentialDiff $diff) {
protected function renderHarbormasterStatus(
DifferentialDiff $diff,
array $messages) {
$colors = array(
DifferentialLintStatus::LINT_NONE => 'grey',
DifferentialLintStatus::LINT_OKAY => 'green',

View file

@ -1,7 +1,7 @@
<?php
final class DifferentialUnitField
extends DifferentialCustomField {
extends DifferentialHarbormasterField {
public function getFieldKey() {
return 'differential:unit';
@ -31,84 +31,44 @@ final class DifferentialUnitField
return $this->getFieldName();
}
public function renderDiffPropertyViewValue(DifferentialDiff $diff) {
// TODO: See DifferentialLintField.
$keys = array(
protected function getLegacyProperty() {
return 'arc:unit';
}
protected function getDiffPropertyKeys() {
return array(
'arc:unit',
'arc:unit-excuse',
);
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d AND name IN (%Ls)',
$diff->getID(),
$keys);
$properties = mpull($properties, 'getData', 'getName');
protected function loadHarbormasterTargetMessages(array $target_phids) {
return id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
}
foreach ($keys as $key) {
$diff->attachProperty($key, idx($properties, $key));
}
protected function newModernMessage(array $message) {
return HarbormasterBuildUnitMessage::newFromDictionary(
new HarbormasterBuildTarget(),
$this->getModernUnitMessageDictionary($message));
}
$status = $this->renderUnitStatus($diff);
$unit = array();
$buildable = $diff->getBuildable();
if ($buildable) {
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
$unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls) LIMIT 25',
$target_phids);
}
if (!$unit) {
$legacy_unit = $diff->getProperty('arc:unit');
if ($legacy_unit) {
// Show the top 100 legacy unit messages.
$legacy_unit = array_slice($legacy_unit, 0, 100);
$target = new HarbormasterBuildTarget();
foreach ($legacy_unit as $message) {
try {
$modern = HarbormasterBuildUnitMessage::newFromDictionary(
$target,
$this->getModernUnitMessageDictionary($message));
$unit[] = $modern;
} catch (Exception $ex) {
// Just ignore it if legacy messages aren't formatted like
// we expect.
}
}
protected function newHarbormasterMessageView(array $messages) {
foreach ($messages as $key => $message) {
if ($message->getResult() == ArcanistUnitTestResult::RESULT_PASS) {
unset($messages[$key]);
}
}
if ($unit) {
$path_map = mpull($diff->loadChangesets(), 'getID', 'getFilename');
foreach ($path_map as $path => $id) {
$href = '#C'.$id.'NL';
// TODO: When the diff is not the right-hand-size diff, we should
// ideally adjust this URI to be absolute.
$path_map[$path] = $href;
}
$view = id(new HarbormasterUnitPropertyView())
->setPathURIMap($path_map)
->setUnitMessages($unit);
} else {
$view = null;
if (!$messages) {
return null;
}
return array(
$status,
$view,
);
return id(new HarbormasterUnitPropertyView())
->setLimit(10)
->setHidePassingTests(true)
->setUnitMessages($messages);
}
public function getWarningsForDetailView() {
@ -132,8 +92,10 @@ final class DifferentialUnitField
return $warnings;
}
protected function renderHarbormasterStatus(
DifferentialDiff $diff,
array $messages) {
private function renderUnitStatus(DifferentialDiff $diff) {
$colors = array(
DifferentialUnitStatus::UNIT_NONE => 'grey',
DifferentialUnitStatus::UNIT_OKAY => 'green',
@ -147,6 +109,55 @@ final class DifferentialUnitField
$message = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff);
$note = array();
$groups = mgroup($messages, 'getResult');
$groups = array_select_keys(
$groups,
array(
ArcanistUnitTestResult::RESULT_FAIL,
ArcanistUnitTestResult::RESULT_BROKEN,
ArcanistUnitTestResult::RESULT_UNSOUND,
ArcanistUnitTestResult::RESULT_SKIP,
ArcanistUnitTestResult::RESULT_PASS,
)) + $groups;
foreach ($groups as $result => $group) {
$count = new PhutilNumber(count($group));
switch ($result) {
case ArcanistUnitTestResult::RESULT_PASS:
$note[] = pht('%s Passed Test(s)', $count);
break;
case ArcanistUnitTestResult::RESULT_FAIL:
$note[] = pht('%s Failed Test(s)', $count);
break;
case ArcanistUnitTestResult::RESULT_SKIP:
$note[] = pht('%s Skipped Test(s)', $count);
break;
case ArcanistUnitTestResult::RESULT_BROKEN:
$note[] = pht('%s Broken Test(s)', $count);
break;
case ArcanistUnitTestResult::RESULT_UNSOUND:
$note[] = pht('%s Unsound Test(s)', $count);
break;
default:
$note[] = pht('%s Other Test(s)', $count);
break;
}
}
$buildable = $diff->getBuildable();
if ($buildable) {
$full_results = '/harbormaster/unit/'.$buildable->getID().'/';
$note[] = phutil_tag(
'a',
array(
'href' => $full_results,
),
pht('View Full Results'));
}
$excuse = $diff->getProperty('arc:unit-excuse');
if (strlen($excuse)) {
$excuse = array(
@ -154,14 +165,17 @@ final class DifferentialUnitField
' ',
phutil_escape_html_newlines($excuse),
);
$note[] = $excuse;
}
$note = phutil_implode_html(" \xC2\xB7 ", $note);
$status = id(new PHUIStatusListView())
->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_STAR, $icon_color)
->setTarget($message)
->setNote($excuse));
->setNote($note));
return $status;
}

View file

@ -77,6 +77,12 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication {
'run/(?P<id>\d+)/' => 'HarbormasterPlanRunController',
'(?P<id>\d+)/' => 'HarbormasterPlanViewController',
),
'unit/' => array(
'(?P<id>\d+)/' => 'HarbormasterUnitMessagesController',
),
'lint/' => array(
'(?P<id>\d+)/' => 'HarbormasterLintMessagesController',
),
),
);
}

View file

@ -25,7 +25,7 @@ final class HarbormasterBuildableViewController
->needBuildTargets(true)
->execute();
list($lint, $unit) = $this->renderLintAndUnit($builds);
list($lint, $unit) = $this->renderLintAndUnit($buildable, $builds);
$buildable->attachBuilds($builds);
$object = $buildable->getBuildableObject();
@ -257,7 +257,10 @@ final class HarbormasterBuildableViewController
return $box;
}
private function renderLintAndUnit(array $builds) {
private function renderLintAndUnit(
HarbormasterBuildable $buildable,
array $builds) {
$viewer = $this->getViewer();
$targets = array();
@ -274,20 +277,32 @@ final class HarbormasterBuildableViewController
$target_phids = mpull($targets, 'getPHID');
$lint_data = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls) LIMIT 25',
'buildTargetPHID IN (%Ls)',
$target_phids);
$unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls) LIMIT 25',
'buildTargetPHID IN (%Ls)',
$target_phids);
if ($lint_data) {
$lint_table = id(new HarbormasterLintPropertyView())
->setUser($viewer)
->setLimit(10)
->setLintMessages($lint_data);
$lint_href = $this->getApplicationURI('lint/'.$buildable->getID().'/');
$lint_header = id(new PHUIHeaderView())
->setHeader(pht('Lint Messages'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($lint_href)
->setIconFont('fa-list-ul')
->setText('View All'));
$lint = id(new PHUIObjectBoxView())
->setHeaderText(pht('Lint Messages'))
->setHeader($lint_header)
->appendChild($lint_table);
} else {
$lint = null;
@ -296,10 +311,22 @@ final class HarbormasterBuildableViewController
if ($unit_data) {
$unit_table = id(new HarbormasterUnitPropertyView())
->setUser($viewer)
->setLimit(25)
->setUnitMessages($unit_data);
$unit_href = $this->getApplicationURI('unit/'.$buildable->getID().'/');
$unit_header = id(new PHUIHeaderView())
->setHeader(pht('Unit Tests'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($unit_href)
->setIconFont('fa-list-ul')
->setText('View All'));
$unit = id(new PHUIObjectBoxView())
->setHeaderText(pht('Unit Tests'))
->setHeader($unit_header)
->appendChild($unit_table);
} else {
$unit = null;

View file

@ -0,0 +1,64 @@
<?php
final class HarbormasterLintMessagesController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needBuilds(true)
->needTargets(true)
->executeOne();
if (!$buildable) {
return new Aphront404Response();
}
$id = $buildable->getID();
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
$lint_data = array();
if ($target_phids) {
$lint_data = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
} else {
$lint_data = array();
}
$lint_table = id(new HarbormasterLintPropertyView())
->setUser($viewer)
->setLintMessages($lint_data);
$lint = id(new PHUIObjectBoxView())
->setHeaderText(pht('Lint Messages'))
->appendChild($lint_table);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $buildable);
$crumbs->addTextCrumb(pht('Lint'));
$title = array(
$buildable->getMonogram(),
pht('Lint'),
);
return $this->buildApplicationPage(
array(
$crumbs,
$lint,
),
array(
'title' => $title,
));
}
}

View file

@ -0,0 +1,64 @@
<?php
final class HarbormasterUnitMessagesController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needBuilds(true)
->needTargets(true)
->executeOne();
if (!$buildable) {
return new Aphront404Response();
}
$id = $buildable->getID();
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
$unit_data = array();
if ($target_phids) {
$unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
} else {
$unit_data = array();
}
$unit_table = id(new HarbormasterUnitPropertyView())
->setUser($viewer)
->setUnitMessages($unit_data);
$unit = id(new PHUIObjectBoxView())
->setHeaderText(pht('Unit Tests'))
->appendChild($unit_table);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $buildable);
$crumbs->addTextCrumb(pht('Unit Tests'));
$title = array(
$buildable->getMonogram(),
pht('Unit Tests'),
);
return $this->buildApplicationPage(
array(
$crumbs,
$unit,
),
array(
'title' => $title,
));
}
}

View file

@ -51,22 +51,17 @@ final class HarbormasterUIEventListener
return;
}
$buildables = id(new HarbormasterBuildableQuery())
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($user)
->withManualBuildables(false)
->withBuildablePHIDs(array($buildable_phid))
->execute();
if (!$buildables) {
->needBuilds(true)
->executeOne();
if (!$buildable) {
return;
}
$builds = id(new HarbormasterBuildQuery())
->setViewer($user)
->withBuildablePHIDs(mpull($buildables, 'getPHID'))
->execute();
if (!$builds) {
return;
}
$builds = $buildable->getBuilds();
$build_handles = id(new PhabricatorHandleQuery())
->setViewer($user)
@ -75,6 +70,29 @@ final class HarbormasterUIEventListener
$status_view = new PHUIStatusListView();
$buildable_status = $buildable->getBuildableStatus();
$buildable_icon = HarbormasterBuildable::getBuildableStatusIcon(
$buildable_status);
$buildable_color = HarbormasterBuildable::getBuildableStatusColor(
$buildable_status);
$buildable_name = HarbormasterBuildable::getBuildableStatusName(
$buildable_status);
$target = phutil_tag(
'a',
array(
'href' => '/'.$buildable->getMonogram(),
),
pht('Buildable %d', $buildable->getID()));
$target = phutil_tag('strong', array(), $target);
$status_view
->addItem(
id(new PHUIStatusItemView())
->setIcon($buildable_icon, $buildable_color, $buildable_name)
->setTarget($target));
foreach ($builds as $build) {
$item = new PHUIStatusItemView();
$item->setTarget($build_handles[$build->getPHID()]->renderLink());

View file

@ -95,4 +95,26 @@ final class HarbormasterBuildLintMessage
return $this;
}
public function getSortKey() {
// TODO: Maybe use more numeric values after T6861.
$map = array(
ArcanistLintSeverity::SEVERITY_ERROR => 'A',
ArcanistLintSeverity::SEVERITY_WARNING => 'B',
ArcanistLintSeverity::SEVERITY_AUTOFIX => 'C',
ArcanistLintSeverity::SEVERITY_ADVICE => 'Y',
ArcanistLintSeverity::SEVERITY_DISABLED => 'Z',
);
$severity = idx($map, $this->getSeverity(), 'N');
$parts = array(
$severity,
$this->getPath(),
sprintf('%08d', $this->getLine()),
$this->getCode(),
);
return implode("\0", $parts);
}
}

View file

@ -97,4 +97,26 @@ final class HarbormasterBuildUnitMessage
return $this;
}
public function getSortKey() {
// TODO: Maybe use more numeric values after T6861.
$map = array(
ArcanistUnitTestResult::RESULT_FAIL => 'A',
ArcanistUnitTestResult::RESULT_BROKEN => 'B',
ArcanistUnitTestResult::RESULT_UNSOUND => 'C',
ArcanistUnitTestResult::RESULT_PASS => 'Z',
);
$result = idx($map, $this->getResult(), 'N');
$parts = array(
$result,
$this->getEngine(),
$this->getNamespace(),
$this->getName(),
$this->getID(),
);
return implode("\0", $parts);
}
}

View file

@ -4,6 +4,7 @@ final class HarbormasterLintPropertyView extends AphrontView {
private $pathURIMap = array();
private $lintMessages = array();
private $limit;
public function setPathURIMap(array $map) {
$this->pathURIMap = $map;
@ -16,9 +17,21 @@ final class HarbormasterLintPropertyView extends AphrontView {
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function render() {
$messages = $this->lintMessages;
$messages = msort($messages, 'getSortKey');
if ($this->limit) {
$messages = array_slice($messages, 0, $this->limit);
}
$rows = array();
foreach ($this->lintMessages as $message) {
foreach ($messages as $message) {
$path = $message->getPath();
$line = $message->getLine();
@ -40,8 +53,8 @@ final class HarbormasterLintPropertyView extends AphrontView {
}
$rows[] = array(
$location,
$severity,
$location,
$message->getCode(),
$message->getName(),
);
@ -50,15 +63,15 @@ final class HarbormasterLintPropertyView extends AphrontView {
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Location'),
pht('Severity'),
pht('Location'),
pht('Code'),
pht('Message'),
))
->setColumnClasses(
array(
'pri',
null,
'pri',
null,
'wide',
));

View file

@ -4,6 +4,7 @@ final class HarbormasterUnitPropertyView extends AphrontView {
private $pathURIMap = array();
private $unitMessages = array();
private $limit;
public function setPathURIMap(array $map) {
$this->pathURIMap = $map;
@ -16,11 +17,22 @@ final class HarbormasterUnitPropertyView extends AphrontView {
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function render() {
$messages = $this->unitMessages;
$messages = msort($messages, 'getSortKey');
if ($this->limit) {
$messages = array_slice($messages, 0, $this->limit);
}
$rows = array();
$any_duration = false;
foreach ($this->unitMessages as $message) {
foreach ($messages as $message) {
$result = $this->renderResult($message->getResult());
$duration = $message->getDuration();
@ -48,7 +60,6 @@ final class HarbormasterUnitPropertyView extends AphrontView {
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(

View file

@ -0,0 +1,296 @@
<?php
final class ManiphestTaskEditBulkJobType
extends PhabricatorWorkerBulkJobType {
public function getBulkJobTypeKey() {
return 'maniphest.task.edit';
}
public function getJobName(PhabricatorWorkerBulkJob $job) {
return pht('Maniphest Bulk Edit');
}
public function getDescriptionForConfirm(PhabricatorWorkerBulkJob $job) {
return pht(
'You are about to apply a bulk edit to Maniphest which will affect '.
'%s task(s).',
new PhutilNumber($job->getSize()));
}
public function getJobSize(PhabricatorWorkerBulkJob $job) {
return count($job->getParameter('taskPHIDs', array()));
}
public function getDoneURI(PhabricatorWorkerBulkJob $job) {
return $job->getParameter('doneURI');
}
public function createTasks(PhabricatorWorkerBulkJob $job) {
$tasks = array();
foreach ($job->getParameter('taskPHIDs', array()) as $phid) {
$tasks[] = PhabricatorWorkerBulkTask::initializeNewTask($job, $phid);
}
return $tasks;
}
public function runTask(
PhabricatorUser $actor,
PhabricatorWorkerBulkJob $job,
PhabricatorWorkerBulkTask $task) {
$object = id(new ManiphestTaskQuery())
->setViewer($actor)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withPHIDs(array($task->getObjectPHID()))
->executeOne();
if (!$object) {
return;
}
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($object);
$actions = $job->getParameter('actions');
$xactions = $this->buildTransactions($actions, $object);
$editor = id(new ManiphestTransactionEditor())
->setActor($actor)
->setContentSource($job->newContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($object, $xactions);
}
private function buildTransactions($actions, ManiphestTask $task) {
$value_map = array();
$type_map = array(
'add_comment' => PhabricatorTransactions::TYPE_COMMENT,
'assign' => ManiphestTransaction::TYPE_OWNER,
'status' => ManiphestTransaction::TYPE_STATUS,
'priority' => ManiphestTransaction::TYPE_PRIORITY,
'add_project' => PhabricatorTransactions::TYPE_EDGE,
'remove_project' => PhabricatorTransactions::TYPE_EDGE,
'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
'space' => PhabricatorTransactions::TYPE_SPACE,
);
$edge_edit_types = array(
'add_project' => true,
'remove_project' => true,
'add_ccs' => true,
'remove_ccs' => true,
);
$xactions = array();
foreach ($actions as $action) {
if (empty($type_map[$action['action']])) {
throw new Exception(pht("Unknown batch edit action '%s'!", $action));
}
$type = $type_map[$action['action']];
// Figure out the current value, possibly after modifications by other
// batch actions of the same type. For example, if the user chooses to
// "Add Comment" twice, we should add both comments. More notably, if the
// user chooses "Remove Project..." and also "Add Project...", we should
// avoid restoring the removed project in the second transaction.
if (array_key_exists($type, $value_map)) {
$current = $value_map[$type];
} else {
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$current = null;
break;
case ManiphestTransaction::TYPE_OWNER:
$current = $task->getOwnerPHID();
break;
case ManiphestTransaction::TYPE_STATUS:
$current = $task->getStatus();
break;
case ManiphestTransaction::TYPE_PRIORITY:
$current = $task->getPriority();
break;
case PhabricatorTransactions::TYPE_EDGE:
$current = $task->getProjectPHIDs();
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$current = $task->getSubscriberPHIDs();
break;
case PhabricatorTransactions::TYPE_SPACE:
$current = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
$task);
break;
}
}
// Check if the value is meaningful / provided, and normalize it if
// necessary. This discards, e.g., empty comments and empty owner
// changes.
$value = $action['value'];
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (!strlen($value)) {
continue 2;
}
break;
case PhabricatorTransactions::TYPE_SPACE:
if (empty($value)) {
continue 2;
}
$value = head($value);
break;
case ManiphestTransaction::TYPE_OWNER:
if (empty($value)) {
continue 2;
}
$value = head($value);
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
if ($value === $no_owner) {
$value = null;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
if (empty($value)) {
continue 2;
}
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
if (empty($value)) {
continue 2;
}
break;
}
// If the edit doesn't change anything, go to the next action. This
// check is only valid for changes like "owner", "status", etc, not
// for edge edits, because we should still apply an edit like
// "Remove Projects: A, B" to a task with projects "A, B".
if (empty($edge_edit_types[$action['action']])) {
if ($value == $current) {
continue;
}
}
// Apply the value change; for most edits this is just replacement, but
// some need to merge the current and edited values (add/remove project).
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (strlen($current)) {
$value = $current."\n\n".$value;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$is_remove = $action['action'] == 'remove_project';
$current = array_fill_keys($current, true);
$value = array_fill_keys($value, true);
$new = $current;
$did_something = false;
if ($is_remove) {
foreach ($value as $phid => $ignored) {
if (isset($new[$phid])) {
unset($new[$phid]);
$did_something = true;
}
}
} else {
foreach ($value as $phid => $ignored) {
if (empty($new[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
}
if (!$did_something) {
continue 2;
}
$value = array_keys($new);
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$is_remove = $action['action'] == 'remove_ccs';
$current = array_fill_keys($current, true);
$new = array();
$did_something = false;
if ($is_remove) {
foreach ($value as $phid) {
if (isset($current[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
if ($new) {
$value = array('-' => array_keys($new));
}
} else {
$new = array();
foreach ($value as $phid) {
$new[$phid] = true;
$did_something = true;
}
if ($new) {
$value = array('+' => array_keys($new));
}
}
if (!$did_something) {
continue 2;
}
break;
}
$value_map[$type] = $value;
}
$template = new ManiphestTransaction();
foreach ($value_map as $type => $value) {
$xaction = clone $template;
$xaction->setTransactionType($type);
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($value));
break;
case PhabricatorTransactions::TYPE_EDGE:
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xaction
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'=' => array_fuse($value),
));
break;
default:
$xaction->setNewValue($value);
break;
}
$xactions[] = $xaction;
}
return $xactions;
}
}

View file

@ -45,8 +45,7 @@ final class ManiphestBatchEditController extends ManiphestController {
if (!$tasks) {
throw new Exception(
pht(
"You don't have permission to edit any of the selected tasks."));
pht("You don't have permission to edit any of the selected tasks."));
}
if ($project) {
@ -62,27 +61,32 @@ final class ManiphestBatchEditController extends ManiphestController {
$actions = phutil_json_decode($actions);
}
if ($request->isFormPost() && is_array($actions)) {
foreach ($tasks as $task) {
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($task);
if ($request->isFormPost() && $actions) {
$job = PhabricatorWorkerBulkJob::initializeNewJob(
$viewer,
new ManiphestTaskEditBulkJobType(),
array(
'taskPHIDs' => mpull($tasks, 'getPHID'),
'actions' => $actions,
'cancelURI' => $cancel_uri,
'doneURI' => $redirect_uri,
));
$xactions = $this->buildTransactions($actions, $task);
if ($xactions) {
// TODO: Set content source to "batch edit".
$type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($task, $xactions);
}
}
$xactions = array();
$xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
->setTransactionType($type_status)
->setNewValue(PhabricatorWorkerBulkJob::STATUS_CONFIRM);
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
$editor = id(new PhabricatorWorkerBulkJobEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->applyTransactions($job, $xactions);
return id(new AphrontRedirectResponse())
->setURI($job->getMonitorURI());
}
$handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
@ -210,228 +214,4 @@ final class ManiphestBatchEditController extends ManiphestController {
));
}
private function buildTransactions($actions, ManiphestTask $task) {
$value_map = array();
$type_map = array(
'add_comment' => PhabricatorTransactions::TYPE_COMMENT,
'assign' => ManiphestTransaction::TYPE_OWNER,
'status' => ManiphestTransaction::TYPE_STATUS,
'priority' => ManiphestTransaction::TYPE_PRIORITY,
'add_project' => PhabricatorTransactions::TYPE_EDGE,
'remove_project' => PhabricatorTransactions::TYPE_EDGE,
'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
'space' => PhabricatorTransactions::TYPE_SPACE,
);
$edge_edit_types = array(
'add_project' => true,
'remove_project' => true,
'add_ccs' => true,
'remove_ccs' => true,
);
$xactions = array();
foreach ($actions as $action) {
if (empty($type_map[$action['action']])) {
throw new Exception(pht("Unknown batch edit action '%s'!", $action));
}
$type = $type_map[$action['action']];
// Figure out the current value, possibly after modifications by other
// batch actions of the same type. For example, if the user chooses to
// "Add Comment" twice, we should add both comments. More notably, if the
// user chooses "Remove Project..." and also "Add Project...", we should
// avoid restoring the removed project in the second transaction.
if (array_key_exists($type, $value_map)) {
$current = $value_map[$type];
} else {
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$current = null;
break;
case ManiphestTransaction::TYPE_OWNER:
$current = $task->getOwnerPHID();
break;
case ManiphestTransaction::TYPE_STATUS:
$current = $task->getStatus();
break;
case ManiphestTransaction::TYPE_PRIORITY:
$current = $task->getPriority();
break;
case PhabricatorTransactions::TYPE_EDGE:
$current = $task->getProjectPHIDs();
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$current = $task->getSubscriberPHIDs();
break;
case PhabricatorTransactions::TYPE_SPACE:
$current = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
$task);
break;
}
}
// Check if the value is meaningful / provided, and normalize it if
// necessary. This discards, e.g., empty comments and empty owner
// changes.
$value = $action['value'];
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (!strlen($value)) {
continue 2;
}
break;
case PhabricatorTransactions::TYPE_SPACE:
if (empty($value)) {
continue 2;
}
$value = head($value);
break;
case ManiphestTransaction::TYPE_OWNER:
if (empty($value)) {
continue 2;
}
$value = head($value);
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
if ($value === $no_owner) {
$value = null;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
if (empty($value)) {
continue 2;
}
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
if (empty($value)) {
continue 2;
}
break;
}
// If the edit doesn't change anything, go to the next action. This
// check is only valid for changes like "owner", "status", etc, not
// for edge edits, because we should still apply an edit like
// "Remove Projects: A, B" to a task with projects "A, B".
if (empty($edge_edit_types[$action['action']])) {
if ($value == $current) {
continue;
}
}
// Apply the value change; for most edits this is just replacement, but
// some need to merge the current and edited values (add/remove project).
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (strlen($current)) {
$value = $current."\n\n".$value;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$is_remove = $action['action'] == 'remove_project';
$current = array_fill_keys($current, true);
$value = array_fill_keys($value, true);
$new = $current;
$did_something = false;
if ($is_remove) {
foreach ($value as $phid => $ignored) {
if (isset($new[$phid])) {
unset($new[$phid]);
$did_something = true;
}
}
} else {
foreach ($value as $phid => $ignored) {
if (empty($new[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
}
if (!$did_something) {
continue 2;
}
$value = array_keys($new);
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$is_remove = $action['action'] == 'remove_ccs';
$current = array_fill_keys($current, true);
$new = array();
$did_something = false;
if ($is_remove) {
foreach ($value as $phid) {
if (isset($current[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
if ($new) {
$value = array('-' => array_keys($new));
}
} else {
$new = array();
foreach ($value as $phid) {
$new[$phid] = true;
$did_something = true;
}
if ($new) {
$value = array('+' => array_keys($new));
}
}
if (!$did_something) {
continue 2;
}
break;
}
$value_map[$type] = $value;
}
$template = new ManiphestTransaction();
foreach ($value_map as $type => $value) {
$xaction = clone $template;
$xaction->setTransactionType($type);
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($value));
break;
case PhabricatorTransactions::TYPE_EDGE:
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xaction
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'=' => array_fuse($value),
));
break;
default:
$xaction->setNewValue($value);
break;
}
$xactions[] = $xaction;
}
return $xactions;
}
}

View file

@ -6,6 +6,10 @@ final class PhabricatorMetaMTAApplication extends PhabricatorApplication {
return pht('MetaMTA');
}
public function getBaseURI() {
return '/mail/';
}
public function getFontIcon() {
return 'fa-send';
}
@ -37,6 +41,9 @@ final class PhabricatorMetaMTAApplication extends PhabricatorApplication {
public function getRoutes() {
return array(
'/mail/' => array(
'(query/(?P<queryKey>[^/]+)/)?' =>
'PhabricatorMetaMTAMailListController',
'detail/(?P<id>[1-9]\d*)/' => 'PhabricatorMetaMTAMailViewController',
'sendgrid/' => 'PhabricatorMetaMTASendGridReceiveController',
'mailgun/' => 'PhabricatorMetaMTAMailgunReceiveController',
),

View file

@ -15,6 +15,7 @@ final class PhabricatorContentSource extends Phobject {
const SOURCE_DAEMON = 'daemon';
const SOURCE_LIPSUM = 'lipsum';
const SOURCE_PHORTUNE = 'phortune';
const SOURCE_BULK = 'bulk';
private $source;
private $params = array();
@ -79,6 +80,7 @@ final class PhabricatorContentSource extends Phobject {
self::SOURCE_LIPSUM => pht('Lipsum'),
self::SOURCE_UNKNOWN => pht('Old World'),
self::SOURCE_PHORTUNE => pht('Phortune'),
self::SOURCE_BULK => pht('Bulk Edit'),
);
}

View file

@ -0,0 +1,30 @@
<?php
final class PhabricatorMetaMTAMailListController
extends PhabricatorMetaMTAController {
public function handleRequest(AphrontRequest $request) {
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine(new PhabricatorMetaMTAMailSearchEngine())
->setNavigation($this->buildSideNav());
return $this->delegateToController($controller);
}
public function buildSideNav() {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new PhabricatorMetaMTAMailSearchEngine())
->setViewer($user)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
}

View file

@ -0,0 +1,91 @@
<?php
final class PhabricatorMetaMTAMailViewController
extends PhabricatorMetaMTAController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$mail = id(new PhabricatorMetaMTAMailQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$mail) {
return new Aphront404Response();
}
if ($mail->hasSensitiveContent()) {
$title = pht('Content Redacted');
} else {
$title = $mail->getSubject();
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($this->getRequest()->getUser())
->setPolicyObject($mail);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(
'Mail '.$mail->getID());
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($this->buildPropertyView($mail));
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
),
array(
'title' => $title,
'pageObjects' => array($mail->getPHID()),
));
}
private function buildPropertyView(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($mail);
if ($mail->getActorPHID()) {
$actor_str = $viewer->renderHandle($mail->getActorPHID());
} else {
$actor_str = pht('Generated by Phabricator');
}
$properties->addProperty(
pht('Actor'),
$actor_str);
if ($mail->getFrom()) {
$from_str = $viewer->renderHandle($mail->getFrom());
} else {
$from_str = pht('Sent by Phabricator');
}
$properties->addProperty(
pht('From'),
$from_str);
if ($mail->getToPHIDs()) {
$to_list = $viewer->renderHandleList($mail->getToPHIDs());
} else {
$to_list = pht('None');
}
$properties->addProperty(
pht('To'),
$to_list);
if ($mail->getCcPHIDs()) {
$cc_list = $viewer->renderHandleList($mail->getCcPHIDs());
} else {
$cc_list = pht('None');
}
$properties->addProperty(
pht('Cc'),
$cc_list);
return $properties;
}
}

View file

@ -0,0 +1,8 @@
<?php
final class PhabricatorMetaMTAMailHasRecipientEdgeType
extends PhabricatorEdgeType {
const EDGECONST = 57;
}

View file

@ -5,6 +5,8 @@ final class PhabricatorMetaMTAMailQuery
private $ids;
private $phids;
private $actorPHIDs;
private $recipientPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -16,6 +18,16 @@ final class PhabricatorMetaMTAMailQuery
return $this;
}
public function withActorPHIDs(array $phids) {
$this->actorPHIDs = $phids;
return $this;
}
public function withRecipientPHIDs(array $phids) {
$this->recipientPHIDs = $phids;
return $this;
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
@ -37,11 +49,57 @@ final class PhabricatorMetaMTAMailQuery
$this->phids);
}
if ($this->actorPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'mail.actorPHID IN (%Ls)',
$this->actorPHIDs);
}
if ($this->recipientPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'recipient.dst IN (%Ls)',
$this->recipientPHIDs);
}
if ($this->actorPHIDs === null && $this->recipientPHIDs === null) {
$viewer = $this->getViewer();
$where[] = qsprintf(
$conn_r,
'edge.dst = %s OR actorPHID = %s',
$viewer->getPHID(),
$viewer->getPHID());
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$joins = array();
if ($this->actorPHIDs === null && $this->recipientPHIDs === null) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T edge ON mail.phid = edge.src AND edge.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST);
}
if ($this->recipientPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T recipient '.
'ON mail.phid = recipient.src AND recipient.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST);
}
return implode(' ', $joins);
}
protected function getPrimaryTableAlias() {
return 'mail';
}

View file

@ -0,0 +1,123 @@
<?php
final class PhabricatorMetaMTAMailSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('MetaMTA Mails');
}
public function getApplicationClassName() {
return 'PhabricatorMetaMTAApplication';
}
public function newQuery() {
return new PhabricatorMetaMTAMailQuery();
}
protected function shouldShowOrderField() {
return false;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchUsersField())
->setLabel(pht('Actors'))
->setKey('actorPHIDs')
->setAliases(array('actor', 'actors')),
id(new PhabricatorSearchUsersField())
->setLabel(pht('Recipients'))
->setKey('recipientPHIDs')
->setAliases(array('recipient', 'recipients')),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['actorPHIDs']) {
$query->withActorPHIDs($map['actorPHIDs']);
}
if ($map['recipientPHIDs']) {
$query->withRecipientPHIDs($map['recipientPHIDs']);
}
return $query;
}
protected function getURI($path) {
return '/mail/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'inbox' => pht('Inbox'),
'outbox' => pht('Outbox'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$viewer = $this->requireViewer();
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'inbox':
return $query->setParameter(
'recipientPHIDs',
array($viewer->getPHID()));
case 'outbox':
return $query->setParameter(
'actorPHIDs',
array($viewer->getPHID()));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $objects,
PhabricatorSavedQuery $query) {
$phids = array();
foreach ($objects as $mail) {
$phids[] = $mail->getExpandedRecipientPHIDs();
}
return array_mergev($phids);
}
protected function renderResultList(
array $mails,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($mails, 'PhabricatorMetaMTAMail');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
foreach ($mails as $mail) {
if ($mail->hasSensitiveContent()) {
$header = pht(
'Mail %d: < content redacted >',
$mail->getID());
} else {
$header = pht(
'Mail %d: %s',
$mail->getID(),
$mail->getSubject());
}
$item = id(new PHUIObjectItemView())
->setObject($mail)
->setHeader($header)
->setHref($this->getURI('detail/'.$mail->getID()));
$list->addItem($item);
}
return $list;
}
}

View file

@ -25,7 +25,7 @@ final class PhabricatorMetaMTAMail
public function __construct() {
$this->status = self::STATUS_QUEUE;
$this->parameters = array();
$this->parameters = array('sensitive' => true);
parent::__construct();
}
@ -262,6 +262,15 @@ final class PhabricatorMetaMTAMail
return $this;
}
public function setSensitiveContent($bool) {
$this->setParam('sensitive', $bool);
return $this;
}
public function hasSensitiveContent() {
return $this->getParam('sensitive', true);
}
public function setHTMLBody($html) {
$this->setParam('html-body', $html);
return $this;
@ -366,9 +375,24 @@ final class PhabricatorMetaMTAMail
// method.
$this->openTransaction();
// Save to generate a task ID.
// Save to generate a mail ID and PHID.
$result = parent::save();
// Write the recipient edges.
$editor = new PhabricatorEdgeEditor();
$edge_type = PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST;
$recipient_phids = array_merge(
$this->getToPHIDs(),
$this->getCcPHIDs());
$expanded_phids = $this->expandRecipients($recipient_phids);
$all_phids = array_unique(array_merge(
$recipient_phids,
$expanded_phids));
foreach ($all_phids as $curr_phid) {
$editor->addEdge($this->getPHID(), $edge_type, $curr_phid);
}
$editor->save();
// Queue a task to send this mail.
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
@ -813,11 +837,15 @@ final class PhabricatorMetaMTAMail
}
public function loadAllActors() {
$actor_phids = $this->getAllActorPHIDs();
$actor_phids = $this->expandRecipients($actor_phids);
$actor_phids = $this->getExpandedRecipientPHIDs();
return $this->loadActors($actor_phids);
}
public function getExpandedRecipientPHIDs() {
$actor_phids = $this->getAllActorPHIDs();
return $this->expandRecipients($actor_phids);
}
private function getAllActorPHIDs() {
return array_merge(
array($this->getParam('from')),
@ -1025,8 +1053,7 @@ final class PhabricatorMetaMTAMail
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$actor_phids = $this->getAllActorPHIDs();
$actor_phids = $this->expandRecipients($actor_phids);
$actor_phids = $this->getExpandedRecipientPHIDs();
return in_array($viewer->getPHID(), $actor_phids);
}

View file

@ -2334,6 +2334,7 @@ abstract class PhabricatorApplicationTransactionEditor
}
$mail
->setSensitiveContent(false)
->setFrom($this->getActingAsPHID())
->setSubjectPrefix($this->getMailSubjectPrefix())
->setVarySubjectPrefix('['.$action.']')

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorWorkerBulkJobTestCase extends PhabricatorTestCase {
public function testGetAllBulkJobTypes() {
PhabricatorWorkerBulkJobType::getAllJobTypes();
$this->assertTrue(true);
}
}

View file

@ -0,0 +1,51 @@
<?php
final class PhabricatorWorkerBulkJobCreateWorker
extends PhabricatorWorkerBulkJobWorker {
protected function doWork() {
$lock = $this->acquireJobLock();
$job = $this->loadJob();
$actor = $this->loadActor($job);
$status = $job->getStatus();
switch ($status) {
case PhabricatorWorkerBulkJob::STATUS_WAITING:
// This is what we expect. Other statuses indicate some kind of race
// is afoot.
break;
default:
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Found unexpected job status ("%s").',
$status));
}
$tasks = $job->createTasks();
foreach ($tasks as $task) {
$task->save();
}
$this->updateJobStatus(
$job,
PhabricatorWorkerBulkJob::STATUS_RUNNING);
$lock->unlock();
foreach ($tasks as $task) {
PhabricatorWorker::scheduleTask(
'PhabricatorWorkerBulkJobTaskWorker',
array(
'jobID' => $job->getID(),
'taskID' => $task->getID(),
),
array(
'priority' => PhabricatorWorker::PRIORITY_BULK,
));
}
$this->updateJob($job);
}
}

View file

@ -0,0 +1,46 @@
<?php
final class PhabricatorWorkerBulkJobTaskWorker
extends PhabricatorWorkerBulkJobWorker {
protected function doWork() {
$lock = $this->acquireTaskLock();
$task = $this->loadTask();
$status = $task->getStatus();
switch ($task->getStatus()) {
case PhabricatorWorkerBulkTask::STATUS_WAITING:
// This is what we expect.
break;
default:
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Found unexpected task status ("%s").',
$status));
}
$task
->setStatus(PhabricatorWorkerBulkTask::STATUS_RUNNING)
->save();
$lock->unlock();
$job = $this->loadJob();
$actor = $this->loadActor($job);
try {
$job->runTask($actor, $task);
$status = PhabricatorWorkerBulkTask::STATUS_DONE;
} catch (Exception $ex) {
phlog($ex);
$status = PhabricatorWorkerBulkTask::STATUS_FAIL;
}
$task
->setStatus($status)
->save();
$this->updateJob($job);
}
}

View file

@ -0,0 +1,28 @@
<?php
abstract class PhabricatorWorkerBulkJobType extends Phobject {
abstract public function getJobName(PhabricatorWorkerBulkJob $job);
abstract public function getBulkJobTypeKey();
abstract public function getJobSize(PhabricatorWorkerBulkJob $job);
abstract public function getDescriptionForConfirm(
PhabricatorWorkerBulkJob $job);
abstract public function createTasks(PhabricatorWorkerBulkJob $job);
abstract public function runTask(
PhabricatorUser $actor,
PhabricatorWorkerBulkJob $job,
PhabricatorWorkerBulkTask $task);
public function getDoneURI(PhabricatorWorkerBulkJob $job) {
return $job->getManageURI();
}
final public static function getAllJobTypes() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getBulkJobTypeKey')
->execute();
}
}

View file

@ -0,0 +1,138 @@
<?php
abstract class PhabricatorWorkerBulkJobWorker
extends PhabricatorWorker {
final protected function acquireJobLock() {
return PhabricatorGlobalLock::newLock('bulkjob.'.$this->getJobID())
->lock(15);
}
final protected function acquireTaskLock() {
return PhabricatorGlobalLock::newLock('bulktask.'.$this->getTaskID())
->lock(15);
}
final protected function getJobID() {
$data = $this->getTaskData();
$id = idx($data, 'jobID');
if (!$id) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has no job ID.'));
}
return $id;
}
final protected function getTaskID() {
$data = $this->getTaskData();
$id = idx($data, 'taskID');
if (!$id) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has no task ID.'));
}
return $id;
}
final protected function loadJob() {
$id = $this->getJobID();
$job = id(new PhabricatorWorkerBulkJobQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($id))
->executeOne();
if (!$job) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has invalid job ID ("%s").', $id));
}
return $job;
}
final protected function loadTask() {
$id = $this->getTaskID();
$task = id(new PhabricatorWorkerBulkTask())->load($id);
if (!$task) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has invalid task ID ("%s").', $id));
}
return $task;
}
final protected function loadActor(PhabricatorWorkerBulkJob $job) {
$actor_phid = $job->getAuthorPHID();
$actor = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($actor_phid))
->executeOne();
if (!$actor) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Worker has invalid actor PHID ("%s").', $actor_phid));
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$actor,
$job,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_edit) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Job actor does not have permission to edit job.'));
}
return $actor;
}
final protected function updateJob(PhabricatorWorkerBulkJob $job) {
$has_work = $this->hasRemainingWork($job);
if ($has_work) {
return;
}
$lock = $this->acquireJobLock();
$job = $this->loadJob();
if ($job->getStatus() == PhabricatorWorkerBulkJob::STATUS_RUNNING) {
if (!$this->hasRemainingWork($job)) {
$this->updateJobStatus(
$job,
PhabricatorWorkerBulkJob::STATUS_COMPLETE);
}
}
$lock->unlock();
}
private function hasRemainingWork(PhabricatorWorkerBulkJob $job) {
return (bool)queryfx_one(
$job->establishConnection('r'),
'SELECT * FROM %T WHERE bulkJobPHID = %s
AND status NOT IN (%Ls) LIMIT 1',
id(new PhabricatorWorkerBulkTask())->getTableName(),
$job->getPHID(),
array(
PhabricatorWorkerBulkTask::STATUS_DONE,
PhabricatorWorkerBulkTask::STATUS_FAIL,
));
}
protected function updateJobStatus(PhabricatorWorkerBulkJob $job, $status) {
$type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
$xactions = array();
$xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
->setTransactionType($type_status)
->setNewValue($status);
$daemon_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_DAEMON,
array());
$app_phid = id(new PhabricatorDaemonsApplication())->getPHID();
$editor = id(new PhabricatorWorkerBulkJobEditor())
->setActor(PhabricatorUser::getOmnipotentUser())
->setActingAsPHID($app_phid)
->setContentSource($daemon_source)
->setContinueOnMissingFields(true)
->applyTransactions($job, $xactions);
}
}

View file

@ -0,0 +1,87 @@
<?php
final class PhabricatorWorkerBulkJobEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorDaemonsApplication';
}
public function getEditorObjectsDescription() {
return pht('Bulk Jobs');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS:
return $object->getStatus();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS:
return $xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$new = $xaction->getNewValue();
switch ($type) {
case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$new = $xaction->getNewValue();
switch ($type) {
case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS:
switch ($new) {
case PhabricatorWorkerBulkJob::STATUS_WAITING:
PhabricatorWorker::scheduleTask(
'PhabricatorWorkerBulkJobCreateWorker',
array(
'jobID' => $object->getID(),
),
array(
'priority' => PhabricatorWorker::PRIORITY_BULK,
));
break;
}
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
}

View file

@ -0,0 +1,37 @@
<?php
final class PhabricatorWorkerBulkJobPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'BULK';
public function getTypeName() {
return pht('Bulk Job');
}
public function newObject() {
return new PhabricatorWorkerBulkJob();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorWorkerBulkJobQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$job = $objects[$phid];
$id = $job->getID();
$handle->setName(pht('Bulk Job %d', $id));
}
}
}

View file

@ -0,0 +1,106 @@
<?php
final class PhabricatorWorkerBulkJobQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $bulkJobTypes;
private $statuses;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withBulkJobTypes(array $job_types) {
$this->bulkJobTypes = $job_types;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function newResultObject() {
return new PhabricatorWorkerBulkJob();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $page) {
$map = PhabricatorWorkerBulkJobType::getAllJobTypes();
foreach ($page as $key => $job) {
$implementation = idx($map, $job->getJobTypeKey());
if (!$implementation) {
$this->didRejectResult($job);
unset($page[$key]);
continue;
}
$job->attachJobImplementation($implementation);
}
return $page;
}
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->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->bulkJobTypes !== null) {
$where[] = qsprintf(
$conn,
'bulkJobType IN (%Ls)',
$this->bulkJobTypes);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorDaemonsApplication';
}
}

View file

@ -0,0 +1,98 @@
<?php
final class PhabricatorWorkerBulkJobSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Bulk Jobs');
}
public function getApplicationClassName() {
return 'PhabricatorDaemonsApplication';
}
public function newQuery() {
return id(new PhabricatorWorkerBulkJobQuery());
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['authorPHIDs']) {
$query->withAuthorPHIDs($map['authorPHIDs']);
}
return $query;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchUsersField())
->setLabel(pht('Authors'))
->setKey('authorPHIDs')
->setAliases(array('author', 'authors')),
);
}
protected function getURI($path) {
return '/daemon/bulk/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['authored'] = pht('Authored Jobs');
}
$names['all'] = pht('All Jobs');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
case 'authored':
return $query->setParameter(
'authorPHIDs',
array($this->requireViewer()->getPHID()));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $jobs,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($jobs, 'PhabricatorWorkerBulkJob');
$viewer = $this->requireViewer();
$list = id(new PHUIObjectItemListView())
->setUser($viewer);
foreach ($jobs as $job) {
$size = pht('%s Bulk Task(s)', new PhutilNumber($job->getSize()));
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Bulk Job %d', $job->getID()))
->setHeader($job->getJobName())
->addAttribute(phabricator_datetime($job->getDateCreated(), $viewer))
->setHref($job->getManageURI())
->addIcon($job->getStatusIcon(), $job->getStatusName())
->addIcon('none', $size);
$list->addItem($item);
}
// TODO: Needs new wrapper when merging to redesign.
return $list;
}
}

View file

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

View file

@ -0,0 +1,272 @@
<?php
/**
* @task implementation Job Implementation
*/
final class PhabricatorWorkerBulkJob
extends PhabricatorWorkerDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
const STATUS_CONFIRM = 'confirm';
const STATUS_WAITING = 'waiting';
const STATUS_RUNNING = 'running';
const STATUS_COMPLETE = 'complete';
protected $authorPHID;
protected $jobTypeKey;
protected $status;
protected $parameters = array();
protected $size;
private $jobImplementation = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'jobTypeKey' => 'text32',
'status' => 'text32',
'size' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_type' => array(
'columns' => array('jobTypeKey'),
),
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public static function initializeNewJob(
PhabricatorUser $actor,
PhabricatorWorkerBulkJobType $type,
array $parameters) {
$job = id(new PhabricatorWorkerBulkJob())
->setAuthorPHID($actor->getPHID())
->setJobTypeKey($type->getBulkJobTypeKey())
->setParameters($parameters)
->attachJobImplementation($type);
$job->setSize($job->computeSize());
return $job;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorWorkerBulkJobPHIDType::TYPECONST);
}
public function getMonitorURI() {
return '/daemon/bulk/monitor/'.$this->getID().'/';
}
public function getManageURI() {
return '/daemon/bulk/view/'.$this->getID().'/';
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function loadTaskStatusCounts() {
$table = new PhabricatorWorkerBulkTask();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT status, COUNT(*) N FROM %T WHERE bulkJobPHID = %s
GROUP BY status',
$table->getTableName(),
$this->getPHID());
return ipull($rows, 'N', 'status');
}
public function newContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_BULK,
array(
'jobID' => $this->getID(),
));
}
public function getStatusIcon() {
$map = array(
self::STATUS_CONFIRM => 'fa-question',
self::STATUS_WAITING => 'fa-clock-o',
self::STATUS_RUNNING => 'fa-clock-o',
self::STATUS_COMPLETE => 'fa-check grey',
);
return idx($map, $this->getStatus(), 'none');
}
public function getStatusName() {
$map = array(
self::STATUS_CONFIRM => pht('Confirming'),
self::STATUS_WAITING => pht('Waiting'),
self::STATUS_RUNNING => pht('Running'),
self::STATUS_COMPLETE => pht('Complete'),
);
return idx($map, $this->getStatus(), $this->getStatus());
}
/* -( Job Implementation )------------------------------------------------- */
protected function getJobImplementation() {
return $this->assertAttached($this->jobImplementation);
}
public function attachJobImplementation(PhabricatorWorkerBulkJobType $type) {
$this->jobImplementation = $type;
return $this;
}
private function computeSize() {
return $this->getJobImplementation()->getJobSize($this);
}
public function getCancelURI() {
return $this->getJobImplementation()->getCancelURI($this);
}
public function getDoneURI() {
return $this->getJobImplementation()->getDoneURI($this);
}
public function getDescriptionForConfirm() {
return $this->getJobImplementation()->getDescriptionForConfirm($this);
}
public function createTasks() {
return $this->getJobImplementation()->createTasks($this);
}
public function runTask(
PhabricatorUser $actor,
PhabricatorWorkerBulkTask $task) {
return $this->getJobImplementation()->runTask($actor, $this, $task);
}
public function getJobName() {
return $this->getJobImplementation()->getJobName($this);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getAuthorPHID();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only the owner of a bulk job can edit it.');
default:
return null;
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorWorkerBulkJobEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorWorkerBulkJobTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
// We're only removing the actual task objects. This may leave stranded
// workers in the queue itself, but they'll just flush out automatically
// when they can't load bulk job data.
$task_table = new PhabricatorWorkerBulkTask();
$conn_w = $task_table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE bulkJobPHID = %s',
$task_table->getPHID(),
$this->getPHID());
$this->delete();
$this->saveTransaction();
}
}

View file

@ -0,0 +1,51 @@
<?php
final class PhabricatorWorkerBulkJobTransaction
extends PhabricatorApplicationTransaction {
const TYPE_STATUS = 'bulkjob.status';
public function getApplicationName() {
return 'worker';
}
public function getApplicationTransactionType() {
return PhabricatorWorkerBulkJobPHIDType::TYPECONST;
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$type = $this->getTransactionType();
switch ($type) {
case self::TYPE_STATUS:
if ($old === null) {
return pht(
'%s created this bulk job.',
$this->renderHandleLink($author_phid));
} else {
switch ($new) {
case PhabricatorWorkerBulkJob::STATUS_WAITING:
return pht(
'%s confirmed this job.',
$this->renderHandleLink($author_phid));
case PhabricatorWorkerBulkJob::STATUS_RUNNING:
return pht(
'%s marked this job as running.',
$this->renderHandleLink($author_phid));
case PhabricatorWorkerBulkJob::STATUS_COMPLETE:
return pht(
'%s marked this job complete.',
$this->renderHandleLink($author_phid));
}
}
break;
}
return parent::getTitle();
}
}

View file

@ -0,0 +1,46 @@
<?php
final class PhabricatorWorkerBulkTask
extends PhabricatorWorkerDAO {
const STATUS_WAITING = 'waiting';
const STATUS_RUNNING = 'running';
const STATUS_DONE = 'done';
const STATUS_FAIL = 'fail';
protected $bulkJobPHID;
protected $objectPHID;
protected $status;
protected $data = array();
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'data' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_job' => array(
'columns' => array('bulkJobPHID', 'status'),
),
'key_object' => array(
'columns' => array('objectPHID'),
),
),
) + parent::getConfiguration();
}
public static function initializeNewTask(
PhabricatorWorkerBulkJob $job,
$object_phid) {
return id(new PhabricatorWorkerBulkTask())
->setBulkJobPHID($job->getPHID())
->setStatus(self::STATUS_WAITING)
->setObjectPHID($object_phid);
}
}

View file

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

View file

@ -1170,6 +1170,18 @@ final class PhabricatorUSEnglishTranslation
'This call takes %s parameters, but only %s are documented.',
),
),
'%s Passed Test(s)' => '%s Passed',
'%s Failed Test(s)' => '%s Failed',
'%s Skipped Test(s)' => '%s Skipped',
'%s Broken Test(s)' => '%s Broken',
'%s Unsound Test(s)' => '%s Unsound',
'%s Other Test(s)' => '%s Other',
'%s Bulk Task(s)' => array(
'%s Task',
'%s Tasks',
),
);
}

View file

@ -0,0 +1,32 @@
/**
* @provides bulk-job-css
*/
.bulk-job-progress-bar {
position: relative;
width: 100%;
border: 1px solid {$lightgreyborder};
height: 32px;
}
.bulk-job-progress-slice {
position: absolute;
top: 0;
bottom: 0;
}
.bulk-job-progress-slice-green {
background-color: {$green};
}
.bulk-job-progress-slice-blue {
background-color: {$blue};
}
.bulk-job-progress-slice-red {
background-color: {$red};
}
.bulk-job-progress-slice-empty {
background-color: {$lightbluebackground};
}

View file

@ -83,3 +83,8 @@ table.aphront-table-view td.differential-toc-ftype {
border-top: 1px solid {$thinblueborder};
padding: 8px;
}
.differential-harbormaster-table-view {
margin: 4px 0;
border: 1px solid {$thinblueborder};
}

View file

@ -0,0 +1,18 @@
/**
* @provides javelin-behavior-bulk-job-reload
* @requires javelin-behavior
* javelin-uri
*/
JX.behavior('bulk-job-reload', function() {
// TODO: It would be nice to have a pretty Ajax progress bar here, but just
// reload the page for now.
function reload() {
JX.$U().go();
}
setTimeout(reload, 1000);
});