@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Execute Maniphest batch edits in the background with a web UI progress bar

Summary:
Ref T8637. This does nothing interesting, just has empty scaffolding for a bulk job queue.

Basic idea is that when you do something like a batch edit in Maniphest, we:

- Create a BulkJob with all the details.
- Queue a worker to start the job.
- Send you to a progress bar page for the job.

In the background:

- The "start job" worker creates a ton of Task objects, then queues worker tasks to do the work.

In the foreground:

- Fancy ajax animates the progress bar and it goes wooosh.

In general:

- Big jobs actually work.
- Jobs get logged.
- You can monitor jobs.
- Terrible junk like T8637 should be much harder to write and much easier to catch and diagnose.

Test Plan:
No interesting code/beahavior yet. Clean `storage adjust`.

{F526411}

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T8637

Differential Revision: https://secure.phabricator.com/D13392

+1767 -244
+8
resources/celerity/map.php
··· 55 55 'rsrc/css/application/conpherence/widget-pane.css' => '2af42ebe', 56 56 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 57 57 'rsrc/css/application/countdown/timer.css' => '86b7b0a0', 58 + 'rsrc/css/application/daemon/bulk-job.css' => 'df9c1d4a', 58 59 'rsrc/css/application/dashboard/dashboard.css' => '17937d22', 59 60 'rsrc/css/application/diff/inline-comment-summary.css' => '51efda3a', 60 61 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40', ··· 343 344 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3', 344 345 'rsrc/js/application/conpherence/behavior-widget-pane.js' => '93568464', 345 346 'rsrc/js/application/countdown/timer.js' => 'e4cc26b3', 347 + 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => 'edf8a145', 346 348 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e', 347 349 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '82439934', 348 350 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', ··· 495 497 'aphront-two-column-view-css' => '16ab3ad2', 496 498 'aphront-typeahead-control-css' => '0e403212', 497 499 'auth-css' => '44975d4b', 500 + 'bulk-job-css' => 'df9c1d4a', 498 501 'calendar-icon-css' => '98ce946d', 499 502 'changeset-view-manager' => '58562350', 500 503 'conduit-api-css' => '7bc725c4', ··· 541 544 'javelin-behavior-aphront-more' => 'a80d0378', 542 545 'javelin-behavior-audio-source' => '59b251eb', 543 546 'javelin-behavior-audit-preview' => 'd835b03a', 547 + 'javelin-behavior-bulk-job-reload' => 'edf8a145', 544 548 'javelin-behavior-choose-control' => '6153c708', 545 549 'javelin-behavior-config-reorder-fields' => 'b6993408', 546 550 'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a', ··· 1924 1928 'javelin-dom', 1925 1929 'javelin-uri', 1926 1930 'phabricator-notification', 1931 + ), 1932 + 'edf8a145' => array( 1933 + 'javelin-behavior', 1934 + 'javelin-uri', 1927 1935 ), 1928 1936 'eeaa9e5a' => array( 1929 1937 'javelin-behavior',
+15
resources/sql/autopatches/20150622.bulk.1.job.sql
··· 1 + CREATE TABLE {$NAMESPACE}_worker.worker_bulkjob ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + authorPHID VARBINARY(64) NOT NULL, 5 + jobTypeKey VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, 6 + status VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, 7 + parameters LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 8 + size INT UNSIGNED NOT NULL, 9 + dateCreated INT UNSIGNED NOT NULL, 10 + dateModified INT UNSIGNED NOT NULL, 11 + UNIQUE KEY `key_phid` (phid), 12 + KEY `key_type` (jobTypeKey), 13 + KEY `key_author` (authorPHID), 14 + KEY `key_status` (status) 15 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+9
resources/sql/autopatches/20150622.bulk.2.task.sql
··· 1 + CREATE TABLE {$NAMESPACE}_worker.worker_bulktask ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + bulkJobPHID VARBINARY(64) NOT NULL, 4 + objectPHID VARBINARY(64) NOT NULL, 5 + status VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, 6 + data LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 7 + KEY `key_job` (bulkJobPHID, status), 8 + KEY `key_object` (objectPHID) 9 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+19
resources/sql/autopatches/20150622.bulk.3.xaction.sql
··· 1 + CREATE TABLE {$NAMESPACE}_worker.worker_bulkjobtransaction ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + authorPHID VARBINARY(64) NOT NULL, 5 + objectPHID VARBINARY(64) NOT NULL, 6 + viewPolicy VARBINARY(64) NOT NULL, 7 + editPolicy VARBINARY(64) NOT NULL, 8 + commentPHID VARBINARY(64) DEFAULT NULL, 9 + commentVersion INT UNSIGNED NOT NULL, 10 + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, 11 + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 12 + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 13 + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 14 + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 15 + dateCreated INT UNSIGNED NOT NULL, 16 + dateModified INT UNSIGNED NOT NULL, 17 + UNIQUE KEY `key_phid` (`phid`), 18 + KEY `key_object` (`objectPHID`) 19 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+16
resources/sql/autopatches/20150622.bulk.4.edge.sql
··· 1 + CREATE TABLE {$NAMESPACE}_worker.edge ( 2 + src VARBINARY(64) NOT NULL, 3 + type INT UNSIGNED NOT NULL, 4 + dst VARBINARY(64) NOT NULL, 5 + dateCreated INT UNSIGNED NOT NULL, 6 + seq INT UNSIGNED NOT NULL, 7 + dataID INT UNSIGNED, 8 + PRIMARY KEY (src, type, dst), 9 + KEY `src` (src, type, dateCreated, seq), 10 + UNIQUE KEY `key_dst` (dst, type, src) 11 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 12 + 13 + CREATE TABLE {$NAMESPACE}_worker.edgedata ( 14 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 15 + data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT} 16 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+42
src/__phutil_library_map__.php
··· 1082 1082 'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php', 1083 1083 'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php', 1084 1084 'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php', 1085 + 'ManiphestTaskEditBulkJobType' => 'applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php', 1085 1086 'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php', 1086 1087 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', 1087 1088 'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php', ··· 1708 1709 'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php', 1709 1710 'PhabricatorCustomHeaderConfigType' => 'applications/config/custom/PhabricatorCustomHeaderConfigType.php', 1710 1711 'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php', 1712 + 'PhabricatorDaemonBulkJobListController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobListController.php', 1713 + 'PhabricatorDaemonBulkJobMonitorController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php', 1714 + 'PhabricatorDaemonBulkJobViewController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php', 1711 1715 'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php', 1712 1716 'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php', 1713 1717 'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php', ··· 2820 2824 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php', 2821 2825 'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php', 2822 2826 'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php', 2827 + 'PhabricatorWorkerBulkJob' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php', 2828 + 'PhabricatorWorkerBulkJobCreateWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobCreateWorker.php', 2829 + 'PhabricatorWorkerBulkJobEditor' => 'infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php', 2830 + 'PhabricatorWorkerBulkJobPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerBulkJobPHIDType.php', 2831 + 'PhabricatorWorkerBulkJobQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobQuery.php', 2832 + 'PhabricatorWorkerBulkJobSearchEngine' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobSearchEngine.php', 2833 + 'PhabricatorWorkerBulkJobTaskWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobTaskWorker.php', 2834 + 'PhabricatorWorkerBulkJobTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerBulkJobTestCase.php', 2835 + 'PhabricatorWorkerBulkJobTransaction' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJobTransaction.php', 2836 + 'PhabricatorWorkerBulkJobTransactionQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobTransactionQuery.php', 2837 + 'PhabricatorWorkerBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php', 2838 + 'PhabricatorWorkerBulkJobWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php', 2839 + 'PhabricatorWorkerBulkTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkTask.php', 2823 2840 'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php', 2824 2841 'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php', 2825 2842 'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php', ··· 2829 2846 'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php', 2830 2847 'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php', 2831 2848 'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php', 2849 + 'PhabricatorWorkerSchemaSpec' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php', 2832 2850 'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php', 2833 2851 'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php', 2834 2852 'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php', ··· 4589 4607 'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType', 4590 4608 'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType', 4591 4609 'ManiphestTaskDetailController' => 'ManiphestController', 4610 + 'ManiphestTaskEditBulkJobType' => 'PhabricatorWorkerBulkJobType', 4592 4611 'ManiphestTaskEditController' => 'ManiphestController', 4593 4612 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', 4594 4613 'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType', ··· 5300 5319 'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 5301 5320 'PhabricatorCustomHeaderConfigType' => 'PhabricatorConfigOptionType', 5302 5321 'PhabricatorDaemon' => 'PhutilDaemon', 5322 + 'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonController', 5323 + 'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonController', 5324 + 'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonController', 5303 5325 'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController', 5304 5326 'PhabricatorDaemonController' => 'PhabricatorController', 5305 5327 'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO', ··· 6603 6625 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask', 6604 6626 'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask', 6605 6627 'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorQuery', 6628 + 'PhabricatorWorkerBulkJob' => array( 6629 + 'PhabricatorWorkerDAO', 6630 + 'PhabricatorPolicyInterface', 6631 + 'PhabricatorSubscribableInterface', 6632 + 'PhabricatorApplicationTransactionInterface', 6633 + 'PhabricatorDestructibleInterface', 6634 + ), 6635 + 'PhabricatorWorkerBulkJobCreateWorker' => 'PhabricatorWorkerBulkJobWorker', 6636 + 'PhabricatorWorkerBulkJobEditor' => 'PhabricatorApplicationTransactionEditor', 6637 + 'PhabricatorWorkerBulkJobPHIDType' => 'PhabricatorPHIDType', 6638 + 'PhabricatorWorkerBulkJobQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 6639 + 'PhabricatorWorkerBulkJobSearchEngine' => 'PhabricatorApplicationSearchEngine', 6640 + 'PhabricatorWorkerBulkJobTaskWorker' => 'PhabricatorWorkerBulkJobWorker', 6641 + 'PhabricatorWorkerBulkJobTestCase' => 'PhabricatorTestCase', 6642 + 'PhabricatorWorkerBulkJobTransaction' => 'PhabricatorApplicationTransaction', 6643 + 'PhabricatorWorkerBulkJobTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 6644 + 'PhabricatorWorkerBulkJobType' => 'Phobject', 6645 + 'PhabricatorWorkerBulkJobWorker' => 'PhabricatorWorker', 6646 + 'PhabricatorWorkerBulkTask' => 'PhabricatorWorkerDAO', 6606 6647 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', 6607 6648 'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery', 6608 6649 'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow', ··· 6612 6653 'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow', 6613 6654 'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow', 6614 6655 'PhabricatorWorkerPermanentFailureException' => 'Exception', 6656 + 'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec', 6615 6657 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', 6616 6658 'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO', 6617 6659 'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController',
+9
src/applications/daemon/application/PhabricatorDaemonsApplication.php
··· 46 46 '(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogViewController', 47 47 ), 48 48 'event/(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogEventViewController', 49 + 'bulk/' => array( 50 + '(?:query/(?P<queryKey>[^/]+)/)?' => 51 + 'PhabricatorDaemonBulkJobListController', 52 + 'monitor/(?P<id>\d+)/' => 53 + 'PhabricatorDaemonBulkJobMonitorController', 54 + 'view/(?P<id>\d+)/' => 55 + 'PhabricatorDaemonBulkJobViewController', 56 + 57 + ), 49 58 ), 50 59 ); 51 60 }
+31
src/applications/daemon/controller/PhabricatorDaemonBulkJobListController.php
··· 1 + <?php 2 + 3 + final class PhabricatorDaemonBulkJobListController 4 + extends PhabricatorDaemonController { 5 + 6 + public function shouldAllowPublic() { 7 + return true; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $controller = id(new PhabricatorApplicationSearchController()) 12 + ->setQueryKey($request->getURIData('queryKey')) 13 + ->setSearchEngine(new PhabricatorWorkerBulkJobSearchEngine()) 14 + ->setNavigation($this->buildSideNavView()); 15 + return $this->delegateToController($controller); 16 + } 17 + 18 + protected function buildSideNavView($for_app = false) { 19 + $user = $this->getRequest()->getUser(); 20 + 21 + $nav = new AphrontSideNavFilterView(); 22 + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); 23 + 24 + id(new PhabricatorWorkerBulkJobSearchEngine()) 25 + ->setViewer($user) 26 + ->addNavigationItems($nav->getMenu()); 27 + $nav->selectFilter(null); 28 + 29 + return $nav; 30 + } 31 + }
+165
src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php
··· 1 + <?php 2 + 3 + final class PhabricatorDaemonBulkJobMonitorController 4 + extends PhabricatorDaemonController { 5 + 6 + public function shouldAllowPublic() { 7 + return true; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $viewer = $this->getViewer(); 12 + 13 + $job = id(new PhabricatorWorkerBulkJobQuery()) 14 + ->setViewer($viewer) 15 + ->withIDs(array($request->getURIData('id'))) 16 + ->executeOne(); 17 + if (!$job) { 18 + return new Aphront404Response(); 19 + } 20 + 21 + // If the user clicks "Continue" on a completed job, take them back to 22 + // whatever application sent them here. 23 + if ($request->getStr('done')) { 24 + if ($request->isFormPost()) { 25 + $done_uri = $job->getDoneURI(); 26 + return id(new AphrontRedirectResponse())->setURI($done_uri); 27 + } 28 + } 29 + 30 + $title = pht('Bulk Job %d', $job->getID()); 31 + 32 + if ($job->getStatus() == PhabricatorWorkerBulkJob::STATUS_CONFIRM) { 33 + $can_edit = PhabricatorPolicyFilter::hasCapability( 34 + $viewer, 35 + $job, 36 + PhabricatorPolicyCapability::CAN_EDIT); 37 + 38 + if ($can_edit) { 39 + if ($request->isFormPost()) { 40 + $type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS; 41 + 42 + $xactions = array(); 43 + $xactions[] = id(new PhabricatorWorkerBulkJobTransaction()) 44 + ->setTransactionType($type_status) 45 + ->setNewValue(PhabricatorWorkerBulkJob::STATUS_WAITING); 46 + 47 + $editor = id(new PhabricatorWorkerBulkJobEditor()) 48 + ->setActor($viewer) 49 + ->setContentSourceFromRequest($request) 50 + ->setContinueOnMissingFields(true) 51 + ->applyTransactions($job, $xactions); 52 + 53 + return id(new AphrontRedirectResponse()) 54 + ->setURI($job->getMonitorURI()); 55 + } else { 56 + return $this->newDialog() 57 + ->setTitle(pht('Confirm Bulk Job')) 58 + ->appendParagraph($job->getDescriptionForConfirm()) 59 + ->appendParagraph( 60 + pht('Start work on this bulk job?')) 61 + ->addCancelButton($job->getManageURI(), pht('Details')) 62 + ->addSubmitButton(pht('Start Work')); 63 + } 64 + } else { 65 + return $this->newDialog() 66 + ->setTitle(pht('Waiting For Confirmation')) 67 + ->appendParagraph( 68 + pht( 69 + 'This job is waiting for confirmation before work begins.')) 70 + ->addCancelButotn($job->getManageURI(), pht('Details')); 71 + } 72 + } 73 + 74 + 75 + $dialog = $this->newDialog() 76 + ->setTitle(pht('%s: %s', $title, $job->getStatusName())) 77 + ->addCancelButton($job->getManageURI(), pht('Details')); 78 + 79 + switch ($job->getStatus()) { 80 + case PhabricatorWorkerBulkJob::STATUS_WAITING: 81 + $dialog->appendParagraph( 82 + pht('This job is waiting for tasks to be queued.')); 83 + break; 84 + case PhabricatorWorkerBulkJob::STATUS_RUNNING: 85 + $dialog->appendParagraph( 86 + pht('This job is running.')); 87 + break; 88 + case PhabricatorWorkerBulkJob::STATUS_COMPLETE: 89 + $dialog->appendParagraph( 90 + pht('This job is complete.')); 91 + break; 92 + } 93 + 94 + $counts = $job->loadTaskStatusCounts(); 95 + if ($counts) { 96 + $dialog->appendParagraph($this->renderProgress($counts)); 97 + } 98 + 99 + switch ($job->getStatus()) { 100 + case PhabricatorWorkerBulkJob::STATUS_COMPLETE: 101 + $dialog->addHiddenInput('done', true); 102 + $dialog->addSubmitButton(pht('Continue')); 103 + break; 104 + default: 105 + Javelin::initBehavior('bulk-job-reload'); 106 + break; 107 + } 108 + 109 + return $dialog; 110 + } 111 + 112 + private function renderProgress(array $counts) { 113 + $this->requireResource('bulk-job-css'); 114 + 115 + $states = array( 116 + PhabricatorWorkerBulkTask::STATUS_DONE => array( 117 + 'class' => 'bulk-job-progress-slice-green', 118 + ), 119 + PhabricatorWorkerBulkTask::STATUS_RUNNING => array( 120 + 'class' => 'bulk-job-progress-slice-blue', 121 + ), 122 + PhabricatorWorkerBulkTask::STATUS_WAITING => array( 123 + 'class' => 'bulk-job-progress-slice-empty', 124 + ), 125 + PhabricatorWorkerBulkTask::STATUS_FAIL => array( 126 + 'class' => 'bulk-job-progress-slice-red', 127 + ), 128 + ); 129 + 130 + $total = array_sum($counts); 131 + $offset = 0; 132 + $bars = array(); 133 + foreach ($states as $state => $spec) { 134 + $size = idx($counts, $state, 0); 135 + if (!$size) { 136 + continue; 137 + } 138 + 139 + $classes = array(); 140 + $classes[] = 'bulk-job-progress-slice'; 141 + $classes[] = $spec['class']; 142 + 143 + $width = ($size / $total); 144 + $bars[] = phutil_tag( 145 + 'div', 146 + array( 147 + 'class' => implode(' ', $classes), 148 + 'style' => 149 + 'left: '.sprintf('%.2f%%', 100 * $offset).'; '. 150 + 'width: '.sprintf('%.2f%%', 100 * $width).';', 151 + ), 152 + ''); 153 + 154 + $offset += $width; 155 + } 156 + 157 + return phutil_tag( 158 + 'div', 159 + array( 160 + 'class' => 'bulk-job-progress-bar', 161 + ), 162 + $bars); 163 + } 164 + 165 + }
+83
src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php
··· 1 + <?php 2 + 3 + final class PhabricatorDaemonBulkJobViewController 4 + extends PhabricatorDaemonController { 5 + 6 + public function shouldAllowPublic() { 7 + return true; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $viewer = $this->getViewer(); 12 + 13 + $job = id(new PhabricatorWorkerBulkJobQuery()) 14 + ->setViewer($viewer) 15 + ->withIDs(array($request->getURIData('id'))) 16 + ->executeOne(); 17 + if (!$job) { 18 + return new Aphront404Response(); 19 + } 20 + 21 + $title = pht('Bulk Job %d', $job->getID()); 22 + 23 + $crumbs = $this->buildApplicationCrumbs(); 24 + $crumbs->addTextCrumb(pht('Bulk Jobs'), '/daemon/bulk/'); 25 + $crumbs->addTextCrumb($title); 26 + 27 + $properties = $this->renderProperties($job); 28 + $actions = $this->renderActions($job); 29 + $properties->setActionList($actions); 30 + 31 + $box = id(new PHUIObjectBoxView()) 32 + ->setHeaderText($title) 33 + ->addPropertyList($properties); 34 + 35 + $timeline = $this->buildTransactionTimeline( 36 + $job, 37 + new PhabricatorWorkerBulkJobTransactionQuery()); 38 + $timeline->setShouldTerminate(true); 39 + 40 + return $this->buildApplicationPage( 41 + array( 42 + $crumbs, 43 + $box, 44 + $timeline, 45 + ), 46 + array( 47 + 'title' => $title, 48 + )); 49 + } 50 + 51 + private function renderProperties(PhabricatorWorkerBulkJob $job) { 52 + $viewer = $this->getViewer(); 53 + 54 + $view = id(new PHUIPropertyListView()) 55 + ->setUser($viewer) 56 + ->setObject($job); 57 + 58 + $view->addProperty( 59 + pht('Author'), 60 + $viewer->renderHandle($job->getAuthorPHID())); 61 + 62 + $view->addProperty(pht('Status'), $job->getStatusName()); 63 + 64 + return $view; 65 + } 66 + 67 + private function renderActions(PhabricatorWorkerBulkJob $job) { 68 + $viewer = $this->getViewer(); 69 + 70 + $actions = id(new PhabricatorActionListView()) 71 + ->setUser($viewer) 72 + ->setObject($job); 73 + 74 + $actions->addAction( 75 + id(new PhabricatorActionView()) 76 + ->setHref($job->getDoneURI()) 77 + ->setIcon('fa-arrow-circle-o-right') 78 + ->setName(pht('Continue'))); 79 + 80 + return $actions; 81 + } 82 + 83 + }
+3
src/applications/daemon/controller/PhabricatorDaemonController.php
··· 10 10 $nav->addFilter('/', pht('Console')); 11 11 $nav->addFilter('log', pht('All Daemons')); 12 12 13 + $nav->addLabel(pht('Bulk Jobs')); 14 + $nav->addFilter('bulk', pht('Manage Bulk Jobs')); 15 + 13 16 return $nav; 14 17 } 15 18
+296
src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php
··· 1 + <?php 2 + 3 + final class ManiphestTaskEditBulkJobType 4 + extends PhabricatorWorkerBulkJobType { 5 + 6 + public function getBulkJobTypeKey() { 7 + return 'maniphest.task.edit'; 8 + } 9 + 10 + public function getJobName(PhabricatorWorkerBulkJob $job) { 11 + return pht('Maniphest Bulk Edit'); 12 + } 13 + 14 + public function getDescriptionForConfirm(PhabricatorWorkerBulkJob $job) { 15 + return pht( 16 + 'You are about to apply a bulk edit to Maniphest which will affect '. 17 + '%s task(s).', 18 + new PhutilNumber($job->getSize())); 19 + } 20 + 21 + public function getJobSize(PhabricatorWorkerBulkJob $job) { 22 + return count($job->getParameter('taskPHIDs', array())); 23 + } 24 + 25 + public function getDoneURI(PhabricatorWorkerBulkJob $job) { 26 + return $job->getParameter('doneURI'); 27 + } 28 + 29 + public function createTasks(PhabricatorWorkerBulkJob $job) { 30 + $tasks = array(); 31 + 32 + foreach ($job->getParameter('taskPHIDs', array()) as $phid) { 33 + $tasks[] = PhabricatorWorkerBulkTask::initializeNewTask($job, $phid); 34 + } 35 + 36 + return $tasks; 37 + } 38 + 39 + public function runTask( 40 + PhabricatorUser $actor, 41 + PhabricatorWorkerBulkJob $job, 42 + PhabricatorWorkerBulkTask $task) { 43 + 44 + $object = id(new ManiphestTaskQuery()) 45 + ->setViewer($actor) 46 + ->requireCapabilities( 47 + array( 48 + PhabricatorPolicyCapability::CAN_VIEW, 49 + PhabricatorPolicyCapability::CAN_EDIT, 50 + )) 51 + ->withPHIDs(array($task->getObjectPHID())) 52 + ->executeOne(); 53 + if (!$object) { 54 + return; 55 + } 56 + 57 + $field_list = PhabricatorCustomField::getObjectFields( 58 + $object, 59 + PhabricatorCustomField::ROLE_EDIT); 60 + $field_list->readFieldsFromStorage($object); 61 + 62 + $actions = $job->getParameter('actions'); 63 + $xactions = $this->buildTransactions($actions, $object); 64 + 65 + $editor = id(new ManiphestTransactionEditor()) 66 + ->setActor($actor) 67 + ->setContentSource($job->newContentSource()) 68 + ->setContinueOnNoEffect(true) 69 + ->setContinueOnMissingFields(true) 70 + ->applyTransactions($object, $xactions); 71 + } 72 + 73 + private function buildTransactions($actions, ManiphestTask $task) { 74 + $value_map = array(); 75 + $type_map = array( 76 + 'add_comment' => PhabricatorTransactions::TYPE_COMMENT, 77 + 'assign' => ManiphestTransaction::TYPE_OWNER, 78 + 'status' => ManiphestTransaction::TYPE_STATUS, 79 + 'priority' => ManiphestTransaction::TYPE_PRIORITY, 80 + 'add_project' => PhabricatorTransactions::TYPE_EDGE, 81 + 'remove_project' => PhabricatorTransactions::TYPE_EDGE, 82 + 'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS, 83 + 'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS, 84 + 'space' => PhabricatorTransactions::TYPE_SPACE, 85 + ); 86 + 87 + $edge_edit_types = array( 88 + 'add_project' => true, 89 + 'remove_project' => true, 90 + 'add_ccs' => true, 91 + 'remove_ccs' => true, 92 + ); 93 + 94 + $xactions = array(); 95 + foreach ($actions as $action) { 96 + if (empty($type_map[$action['action']])) { 97 + throw new Exception(pht("Unknown batch edit action '%s'!", $action)); 98 + } 99 + 100 + $type = $type_map[$action['action']]; 101 + 102 + // Figure out the current value, possibly after modifications by other 103 + // batch actions of the same type. For example, if the user chooses to 104 + // "Add Comment" twice, we should add both comments. More notably, if the 105 + // user chooses "Remove Project..." and also "Add Project...", we should 106 + // avoid restoring the removed project in the second transaction. 107 + 108 + if (array_key_exists($type, $value_map)) { 109 + $current = $value_map[$type]; 110 + } else { 111 + switch ($type) { 112 + case PhabricatorTransactions::TYPE_COMMENT: 113 + $current = null; 114 + break; 115 + case ManiphestTransaction::TYPE_OWNER: 116 + $current = $task->getOwnerPHID(); 117 + break; 118 + case ManiphestTransaction::TYPE_STATUS: 119 + $current = $task->getStatus(); 120 + break; 121 + case ManiphestTransaction::TYPE_PRIORITY: 122 + $current = $task->getPriority(); 123 + break; 124 + case PhabricatorTransactions::TYPE_EDGE: 125 + $current = $task->getProjectPHIDs(); 126 + break; 127 + case PhabricatorTransactions::TYPE_SUBSCRIBERS: 128 + $current = $task->getSubscriberPHIDs(); 129 + break; 130 + case PhabricatorTransactions::TYPE_SPACE: 131 + $current = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( 132 + $task); 133 + break; 134 + } 135 + } 136 + 137 + // Check if the value is meaningful / provided, and normalize it if 138 + // necessary. This discards, e.g., empty comments and empty owner 139 + // changes. 140 + 141 + $value = $action['value']; 142 + switch ($type) { 143 + case PhabricatorTransactions::TYPE_COMMENT: 144 + if (!strlen($value)) { 145 + continue 2; 146 + } 147 + break; 148 + case PhabricatorTransactions::TYPE_SPACE: 149 + if (empty($value)) { 150 + continue 2; 151 + } 152 + $value = head($value); 153 + break; 154 + case ManiphestTransaction::TYPE_OWNER: 155 + if (empty($value)) { 156 + continue 2; 157 + } 158 + $value = head($value); 159 + $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; 160 + if ($value === $no_owner) { 161 + $value = null; 162 + } 163 + break; 164 + case PhabricatorTransactions::TYPE_EDGE: 165 + if (empty($value)) { 166 + continue 2; 167 + } 168 + break; 169 + case PhabricatorTransactions::TYPE_SUBSCRIBERS: 170 + if (empty($value)) { 171 + continue 2; 172 + } 173 + break; 174 + } 175 + 176 + // If the edit doesn't change anything, go to the next action. This 177 + // check is only valid for changes like "owner", "status", etc, not 178 + // for edge edits, because we should still apply an edit like 179 + // "Remove Projects: A, B" to a task with projects "A, B". 180 + 181 + if (empty($edge_edit_types[$action['action']])) { 182 + if ($value == $current) { 183 + continue; 184 + } 185 + } 186 + 187 + // Apply the value change; for most edits this is just replacement, but 188 + // some need to merge the current and edited values (add/remove project). 189 + 190 + switch ($type) { 191 + case PhabricatorTransactions::TYPE_COMMENT: 192 + if (strlen($current)) { 193 + $value = $current."\n\n".$value; 194 + } 195 + break; 196 + case PhabricatorTransactions::TYPE_EDGE: 197 + $is_remove = $action['action'] == 'remove_project'; 198 + 199 + $current = array_fill_keys($current, true); 200 + $value = array_fill_keys($value, true); 201 + 202 + $new = $current; 203 + $did_something = false; 204 + 205 + if ($is_remove) { 206 + foreach ($value as $phid => $ignored) { 207 + if (isset($new[$phid])) { 208 + unset($new[$phid]); 209 + $did_something = true; 210 + } 211 + } 212 + } else { 213 + foreach ($value as $phid => $ignored) { 214 + if (empty($new[$phid])) { 215 + $new[$phid] = true; 216 + $did_something = true; 217 + } 218 + } 219 + } 220 + 221 + if (!$did_something) { 222 + continue 2; 223 + } 224 + 225 + $value = array_keys($new); 226 + break; 227 + case PhabricatorTransactions::TYPE_SUBSCRIBERS: 228 + $is_remove = $action['action'] == 'remove_ccs'; 229 + 230 + $current = array_fill_keys($current, true); 231 + 232 + $new = array(); 233 + $did_something = false; 234 + 235 + if ($is_remove) { 236 + foreach ($value as $phid) { 237 + if (isset($current[$phid])) { 238 + $new[$phid] = true; 239 + $did_something = true; 240 + } 241 + } 242 + if ($new) { 243 + $value = array('-' => array_keys($new)); 244 + } 245 + } else { 246 + $new = array(); 247 + foreach ($value as $phid) { 248 + $new[$phid] = true; 249 + $did_something = true; 250 + } 251 + if ($new) { 252 + $value = array('+' => array_keys($new)); 253 + } 254 + } 255 + if (!$did_something) { 256 + continue 2; 257 + } 258 + 259 + break; 260 + } 261 + 262 + $value_map[$type] = $value; 263 + } 264 + 265 + $template = new ManiphestTransaction(); 266 + 267 + foreach ($value_map as $type => $value) { 268 + $xaction = clone $template; 269 + $xaction->setTransactionType($type); 270 + 271 + switch ($type) { 272 + case PhabricatorTransactions::TYPE_COMMENT: 273 + $xaction->attachComment( 274 + id(new ManiphestTransactionComment()) 275 + ->setContent($value)); 276 + break; 277 + case PhabricatorTransactions::TYPE_EDGE: 278 + $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; 279 + $xaction 280 + ->setMetadataValue('edge:type', $project_type) 281 + ->setNewValue( 282 + array( 283 + '=' => array_fuse($value), 284 + )); 285 + break; 286 + default: 287 + $xaction->setNewValue($value); 288 + break; 289 + } 290 + 291 + $xactions[] = $xaction; 292 + } 293 + 294 + return $xactions; 295 + } 296 + }
+24 -244
src/applications/maniphest/controller/ManiphestBatchEditController.php
··· 45 45 46 46 if (!$tasks) { 47 47 throw new Exception( 48 - pht( 49 - "You don't have permission to edit any of the selected tasks.")); 48 + pht("You don't have permission to edit any of the selected tasks.")); 50 49 } 51 50 52 51 if ($project) { ··· 62 61 $actions = phutil_json_decode($actions); 63 62 } 64 63 65 - if ($request->isFormPost() && is_array($actions)) { 66 - foreach ($tasks as $task) { 67 - $field_list = PhabricatorCustomField::getObjectFields( 68 - $task, 69 - PhabricatorCustomField::ROLE_EDIT); 70 - $field_list->readFieldsFromStorage($task); 64 + if ($request->isFormPost() && $actions) { 65 + $job = PhabricatorWorkerBulkJob::initializeNewJob( 66 + $viewer, 67 + new ManiphestTaskEditBulkJobType(), 68 + array( 69 + 'taskPHIDs' => mpull($tasks, 'getPHID'), 70 + 'actions' => $actions, 71 + 'cancelURI' => $cancel_uri, 72 + 'doneURI' => $redirect_uri, 73 + )); 71 74 72 - $xactions = $this->buildTransactions($actions, $task); 73 - if ($xactions) { 74 - // TODO: Set content source to "batch edit". 75 + $type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS; 75 76 76 - $editor = id(new ManiphestTransactionEditor()) 77 - ->setActor($viewer) 78 - ->setContentSourceFromRequest($request) 79 - ->setContinueOnNoEffect(true) 80 - ->setContinueOnMissingFields(true) 81 - ->applyTransactions($task, $xactions); 82 - } 83 - } 77 + $xactions = array(); 78 + $xactions[] = id(new PhabricatorWorkerBulkJobTransaction()) 79 + ->setTransactionType($type_status) 80 + ->setNewValue(PhabricatorWorkerBulkJob::STATUS_CONFIRM); 84 81 85 - return id(new AphrontRedirectResponse())->setURI($redirect_uri); 82 + $editor = id(new PhabricatorWorkerBulkJobEditor()) 83 + ->setActor($viewer) 84 + ->setContentSourceFromRequest($request) 85 + ->setContinueOnMissingFields(true) 86 + ->applyTransactions($job, $xactions); 87 + 88 + return id(new AphrontRedirectResponse()) 89 + ->setURI($job->getMonitorURI()); 86 90 } 87 91 88 92 $handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); ··· 208 212 array( 209 213 'title' => $title, 210 214 )); 211 - } 212 - 213 - private function buildTransactions($actions, ManiphestTask $task) { 214 - $value_map = array(); 215 - $type_map = array( 216 - 'add_comment' => PhabricatorTransactions::TYPE_COMMENT, 217 - 'assign' => ManiphestTransaction::TYPE_OWNER, 218 - 'status' => ManiphestTransaction::TYPE_STATUS, 219 - 'priority' => ManiphestTransaction::TYPE_PRIORITY, 220 - 'add_project' => PhabricatorTransactions::TYPE_EDGE, 221 - 'remove_project' => PhabricatorTransactions::TYPE_EDGE, 222 - 'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS, 223 - 'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS, 224 - 'space' => PhabricatorTransactions::TYPE_SPACE, 225 - ); 226 - 227 - $edge_edit_types = array( 228 - 'add_project' => true, 229 - 'remove_project' => true, 230 - 'add_ccs' => true, 231 - 'remove_ccs' => true, 232 - ); 233 - 234 - $xactions = array(); 235 - foreach ($actions as $action) { 236 - if (empty($type_map[$action['action']])) { 237 - throw new Exception(pht("Unknown batch edit action '%s'!", $action)); 238 - } 239 - 240 - $type = $type_map[$action['action']]; 241 - 242 - // Figure out the current value, possibly after modifications by other 243 - // batch actions of the same type. For example, if the user chooses to 244 - // "Add Comment" twice, we should add both comments. More notably, if the 245 - // user chooses "Remove Project..." and also "Add Project...", we should 246 - // avoid restoring the removed project in the second transaction. 247 - 248 - if (array_key_exists($type, $value_map)) { 249 - $current = $value_map[$type]; 250 - } else { 251 - switch ($type) { 252 - case PhabricatorTransactions::TYPE_COMMENT: 253 - $current = null; 254 - break; 255 - case ManiphestTransaction::TYPE_OWNER: 256 - $current = $task->getOwnerPHID(); 257 - break; 258 - case ManiphestTransaction::TYPE_STATUS: 259 - $current = $task->getStatus(); 260 - break; 261 - case ManiphestTransaction::TYPE_PRIORITY: 262 - $current = $task->getPriority(); 263 - break; 264 - case PhabricatorTransactions::TYPE_EDGE: 265 - $current = $task->getProjectPHIDs(); 266 - break; 267 - case PhabricatorTransactions::TYPE_SUBSCRIBERS: 268 - $current = $task->getSubscriberPHIDs(); 269 - break; 270 - case PhabricatorTransactions::TYPE_SPACE: 271 - $current = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( 272 - $task); 273 - break; 274 - } 275 - } 276 - 277 - // Check if the value is meaningful / provided, and normalize it if 278 - // necessary. This discards, e.g., empty comments and empty owner 279 - // changes. 280 - 281 - $value = $action['value']; 282 - switch ($type) { 283 - case PhabricatorTransactions::TYPE_COMMENT: 284 - if (!strlen($value)) { 285 - continue 2; 286 - } 287 - break; 288 - case PhabricatorTransactions::TYPE_SPACE: 289 - if (empty($value)) { 290 - continue 2; 291 - } 292 - $value = head($value); 293 - break; 294 - case ManiphestTransaction::TYPE_OWNER: 295 - if (empty($value)) { 296 - continue 2; 297 - } 298 - $value = head($value); 299 - $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; 300 - if ($value === $no_owner) { 301 - $value = null; 302 - } 303 - break; 304 - case PhabricatorTransactions::TYPE_EDGE: 305 - if (empty($value)) { 306 - continue 2; 307 - } 308 - break; 309 - case PhabricatorTransactions::TYPE_SUBSCRIBERS: 310 - if (empty($value)) { 311 - continue 2; 312 - } 313 - break; 314 - } 315 - 316 - // If the edit doesn't change anything, go to the next action. This 317 - // check is only valid for changes like "owner", "status", etc, not 318 - // for edge edits, because we should still apply an edit like 319 - // "Remove Projects: A, B" to a task with projects "A, B". 320 - 321 - if (empty($edge_edit_types[$action['action']])) { 322 - if ($value == $current) { 323 - continue; 324 - } 325 - } 326 - 327 - // Apply the value change; for most edits this is just replacement, but 328 - // some need to merge the current and edited values (add/remove project). 329 - 330 - switch ($type) { 331 - case PhabricatorTransactions::TYPE_COMMENT: 332 - if (strlen($current)) { 333 - $value = $current."\n\n".$value; 334 - } 335 - break; 336 - case PhabricatorTransactions::TYPE_EDGE: 337 - $is_remove = $action['action'] == 'remove_project'; 338 - 339 - $current = array_fill_keys($current, true); 340 - $value = array_fill_keys($value, true); 341 - 342 - $new = $current; 343 - $did_something = false; 344 - 345 - if ($is_remove) { 346 - foreach ($value as $phid => $ignored) { 347 - if (isset($new[$phid])) { 348 - unset($new[$phid]); 349 - $did_something = true; 350 - } 351 - } 352 - } else { 353 - foreach ($value as $phid => $ignored) { 354 - if (empty($new[$phid])) { 355 - $new[$phid] = true; 356 - $did_something = true; 357 - } 358 - } 359 - } 360 - 361 - if (!$did_something) { 362 - continue 2; 363 - } 364 - 365 - $value = array_keys($new); 366 - break; 367 - case PhabricatorTransactions::TYPE_SUBSCRIBERS: 368 - $is_remove = $action['action'] == 'remove_ccs'; 369 - 370 - $current = array_fill_keys($current, true); 371 - 372 - $new = array(); 373 - $did_something = false; 374 - 375 - if ($is_remove) { 376 - foreach ($value as $phid) { 377 - if (isset($current[$phid])) { 378 - $new[$phid] = true; 379 - $did_something = true; 380 - } 381 - } 382 - if ($new) { 383 - $value = array('-' => array_keys($new)); 384 - } 385 - } else { 386 - $new = array(); 387 - foreach ($value as $phid) { 388 - $new[$phid] = true; 389 - $did_something = true; 390 - } 391 - if ($new) { 392 - $value = array('+' => array_keys($new)); 393 - } 394 - } 395 - if (!$did_something) { 396 - continue 2; 397 - } 398 - 399 - break; 400 - } 401 - 402 - $value_map[$type] = $value; 403 - } 404 - 405 - $template = new ManiphestTransaction(); 406 - 407 - foreach ($value_map as $type => $value) { 408 - $xaction = clone $template; 409 - $xaction->setTransactionType($type); 410 - 411 - switch ($type) { 412 - case PhabricatorTransactions::TYPE_COMMENT: 413 - $xaction->attachComment( 414 - id(new ManiphestTransactionComment()) 415 - ->setContent($value)); 416 - break; 417 - case PhabricatorTransactions::TYPE_EDGE: 418 - $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; 419 - $xaction 420 - ->setMetadataValue('edge:type', $project_type) 421 - ->setNewValue( 422 - array( 423 - '=' => array_fuse($value), 424 - )); 425 - break; 426 - default: 427 - $xaction->setNewValue($value); 428 - break; 429 - } 430 - 431 - $xactions[] = $xaction; 432 - } 433 - 434 - return $xactions; 435 215 } 436 216 437 217 }
+2
src/applications/metamta/contentsource/PhabricatorContentSource.php
··· 15 15 const SOURCE_DAEMON = 'daemon'; 16 16 const SOURCE_LIPSUM = 'lipsum'; 17 17 const SOURCE_PHORTUNE = 'phortune'; 18 + const SOURCE_BULK = 'bulk'; 18 19 19 20 private $source; 20 21 private $params = array(); ··· 79 80 self::SOURCE_LIPSUM => pht('Lipsum'), 80 81 self::SOURCE_UNKNOWN => pht('Old World'), 81 82 self::SOURCE_PHORTUNE => pht('Phortune'), 83 + self::SOURCE_BULK => pht('Bulk Edit'), 82 84 ); 83 85 } 84 86
+10
src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerBulkJobTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobTestCase extends PhabricatorTestCase { 4 + 5 + public function testGetAllBulkJobTypes() { 6 + PhabricatorWorkerBulkJobType::getAllJobTypes(); 7 + $this->assertTrue(true); 8 + } 9 + 10 + }
+51
src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobCreateWorker.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobCreateWorker 4 + extends PhabricatorWorkerBulkJobWorker { 5 + 6 + protected function doWork() { 7 + $lock = $this->acquireJobLock(); 8 + 9 + $job = $this->loadJob(); 10 + $actor = $this->loadActor($job); 11 + 12 + $status = $job->getStatus(); 13 + switch ($status) { 14 + case PhabricatorWorkerBulkJob::STATUS_WAITING: 15 + // This is what we expect. Other statuses indicate some kind of race 16 + // is afoot. 17 + break; 18 + default: 19 + throw new PhabricatorWorkerPermanentFailureException( 20 + pht( 21 + 'Found unexpected job status ("%s").', 22 + $status)); 23 + } 24 + 25 + $tasks = $job->createTasks(); 26 + foreach ($tasks as $task) { 27 + $task->save(); 28 + } 29 + 30 + $this->updateJobStatus( 31 + $job, 32 + PhabricatorWorkerBulkJob::STATUS_RUNNING); 33 + 34 + $lock->unlock(); 35 + 36 + foreach ($tasks as $task) { 37 + PhabricatorWorker::scheduleTask( 38 + 'PhabricatorWorkerBulkJobTaskWorker', 39 + array( 40 + 'jobID' => $job->getID(), 41 + 'taskID' => $task->getID(), 42 + ), 43 + array( 44 + 'priority' => PhabricatorWorker::PRIORITY_BULK, 45 + )); 46 + } 47 + 48 + $this->updateJob($job); 49 + } 50 + 51 + }
+46
src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobTaskWorker.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobTaskWorker 4 + extends PhabricatorWorkerBulkJobWorker { 5 + 6 + protected function doWork() { 7 + $lock = $this->acquireTaskLock(); 8 + 9 + $task = $this->loadTask(); 10 + $status = $task->getStatus(); 11 + switch ($task->getStatus()) { 12 + case PhabricatorWorkerBulkTask::STATUS_WAITING: 13 + // This is what we expect. 14 + break; 15 + default: 16 + throw new PhabricatorWorkerPermanentFailureException( 17 + pht( 18 + 'Found unexpected task status ("%s").', 19 + $status)); 20 + } 21 + 22 + $task 23 + ->setStatus(PhabricatorWorkerBulkTask::STATUS_RUNNING) 24 + ->save(); 25 + 26 + $lock->unlock(); 27 + 28 + $job = $this->loadJob(); 29 + $actor = $this->loadActor($job); 30 + 31 + try { 32 + $job->runTask($actor, $task); 33 + $status = PhabricatorWorkerBulkTask::STATUS_DONE; 34 + } catch (Exception $ex) { 35 + phlog($ex); 36 + $status = PhabricatorWorkerBulkTask::STATUS_FAIL; 37 + } 38 + 39 + $task 40 + ->setStatus($status) 41 + ->save(); 42 + 43 + $this->updateJob($job); 44 + } 45 + 46 + }
+28
src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorWorkerBulkJobType extends Phobject { 4 + 5 + abstract public function getJobName(PhabricatorWorkerBulkJob $job); 6 + abstract public function getBulkJobTypeKey(); 7 + abstract public function getJobSize(PhabricatorWorkerBulkJob $job); 8 + abstract public function getDescriptionForConfirm( 9 + PhabricatorWorkerBulkJob $job); 10 + 11 + abstract public function createTasks(PhabricatorWorkerBulkJob $job); 12 + abstract public function runTask( 13 + PhabricatorUser $actor, 14 + PhabricatorWorkerBulkJob $job, 15 + PhabricatorWorkerBulkTask $task); 16 + 17 + public function getDoneURI(PhabricatorWorkerBulkJob $job) { 18 + return $job->getManageURI(); 19 + } 20 + 21 + final public static function getAllJobTypes() { 22 + return id(new PhutilClassMapQuery()) 23 + ->setAncestorClass(__CLASS__) 24 + ->setUniqueMethod('getBulkJobTypeKey') 25 + ->execute(); 26 + } 27 + 28 + }
+138
src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorWorkerBulkJobWorker 4 + extends PhabricatorWorker { 5 + 6 + final protected function acquireJobLock() { 7 + return PhabricatorGlobalLock::newLock('bulkjob.'.$this->getJobID()) 8 + ->lock(15); 9 + } 10 + 11 + final protected function acquireTaskLock() { 12 + return PhabricatorGlobalLock::newLock('bulktask.'.$this->getTaskID()) 13 + ->lock(15); 14 + } 15 + 16 + final protected function getJobID() { 17 + $data = $this->getTaskData(); 18 + $id = idx($data, 'jobID'); 19 + if (!$id) { 20 + throw new PhabricatorWorkerPermanentFailureException( 21 + pht('Worker has no job ID.')); 22 + } 23 + return $id; 24 + } 25 + 26 + final protected function getTaskID() { 27 + $data = $this->getTaskData(); 28 + $id = idx($data, 'taskID'); 29 + if (!$id) { 30 + throw new PhabricatorWorkerPermanentFailureException( 31 + pht('Worker has no task ID.')); 32 + } 33 + return $id; 34 + } 35 + 36 + final protected function loadJob() { 37 + $id = $this->getJobID(); 38 + $job = id(new PhabricatorWorkerBulkJobQuery()) 39 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 40 + ->withIDs(array($id)) 41 + ->executeOne(); 42 + if (!$job) { 43 + throw new PhabricatorWorkerPermanentFailureException( 44 + pht('Worker has invalid job ID ("%s").', $id)); 45 + } 46 + return $job; 47 + } 48 + 49 + final protected function loadTask() { 50 + $id = $this->getTaskID(); 51 + $task = id(new PhabricatorWorkerBulkTask())->load($id); 52 + if (!$task) { 53 + throw new PhabricatorWorkerPermanentFailureException( 54 + pht('Worker has invalid task ID ("%s").', $id)); 55 + } 56 + return $task; 57 + } 58 + 59 + final protected function loadActor(PhabricatorWorkerBulkJob $job) { 60 + $actor_phid = $job->getAuthorPHID(); 61 + $actor = id(new PhabricatorPeopleQuery()) 62 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 63 + ->withPHIDs(array($actor_phid)) 64 + ->executeOne(); 65 + if (!$actor) { 66 + throw new PhabricatorWorkerPermanentFailureException( 67 + pht('Worker has invalid actor PHID ("%s").', $actor_phid)); 68 + } 69 + 70 + $can_edit = PhabricatorPolicyFilter::hasCapability( 71 + $actor, 72 + $job, 73 + PhabricatorPolicyCapability::CAN_EDIT); 74 + 75 + if (!$can_edit) { 76 + throw new PhabricatorWorkerPermanentFailureException( 77 + pht('Job actor does not have permission to edit job.')); 78 + } 79 + 80 + return $actor; 81 + } 82 + 83 + final protected function updateJob(PhabricatorWorkerBulkJob $job) { 84 + $has_work = $this->hasRemainingWork($job); 85 + if ($has_work) { 86 + return; 87 + } 88 + 89 + $lock = $this->acquireJobLock(); 90 + 91 + $job = $this->loadJob(); 92 + if ($job->getStatus() == PhabricatorWorkerBulkJob::STATUS_RUNNING) { 93 + if (!$this->hasRemainingWork($job)) { 94 + $this->updateJobStatus( 95 + $job, 96 + PhabricatorWorkerBulkJob::STATUS_COMPLETE); 97 + } 98 + } 99 + 100 + $lock->unlock(); 101 + } 102 + 103 + private function hasRemainingWork(PhabricatorWorkerBulkJob $job) { 104 + return (bool)queryfx_one( 105 + $job->establishConnection('r'), 106 + 'SELECT * FROM %T WHERE bulkJobPHID = %s 107 + AND status NOT IN (%Ls) LIMIT 1', 108 + id(new PhabricatorWorkerBulkTask())->getTableName(), 109 + $job->getPHID(), 110 + array( 111 + PhabricatorWorkerBulkTask::STATUS_DONE, 112 + PhabricatorWorkerBulkTask::STATUS_FAIL, 113 + )); 114 + } 115 + 116 + protected function updateJobStatus(PhabricatorWorkerBulkJob $job, $status) { 117 + $type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS; 118 + 119 + $xactions = array(); 120 + $xactions[] = id(new PhabricatorWorkerBulkJobTransaction()) 121 + ->setTransactionType($type_status) 122 + ->setNewValue($status); 123 + 124 + $daemon_source = PhabricatorContentSource::newForSource( 125 + PhabricatorContentSource::SOURCE_DAEMON, 126 + array()); 127 + 128 + $app_phid = id(new PhabricatorDaemonsApplication())->getPHID(); 129 + 130 + $editor = id(new PhabricatorWorkerBulkJobEditor()) 131 + ->setActor(PhabricatorUser::getOmnipotentUser()) 132 + ->setActingAsPHID($app_phid) 133 + ->setContentSource($daemon_source) 134 + ->setContinueOnMissingFields(true) 135 + ->applyTransactions($job, $xactions); 136 + } 137 + 138 + }
+87
src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobEditor 4 + extends PhabricatorApplicationTransactionEditor { 5 + 6 + public function getEditorApplicationClass() { 7 + return 'PhabricatorDaemonsApplication'; 8 + } 9 + 10 + public function getEditorObjectsDescription() { 11 + return pht('Bulk Jobs'); 12 + } 13 + 14 + public function getTransactionTypes() { 15 + $types = parent::getTransactionTypes(); 16 + 17 + $types[] = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS; 18 + 19 + return $types; 20 + } 21 + 22 + protected function getCustomTransactionOldValue( 23 + PhabricatorLiskDAO $object, 24 + PhabricatorApplicationTransaction $xaction) { 25 + 26 + switch ($xaction->getTransactionType()) { 27 + case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS: 28 + return $object->getStatus(); 29 + } 30 + } 31 + 32 + protected function getCustomTransactionNewValue( 33 + PhabricatorLiskDAO $object, 34 + PhabricatorApplicationTransaction $xaction) { 35 + 36 + switch ($xaction->getTransactionType()) { 37 + case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS: 38 + return $xaction->getNewValue(); 39 + } 40 + } 41 + 42 + protected function applyCustomInternalTransaction( 43 + PhabricatorLiskDAO $object, 44 + PhabricatorApplicationTransaction $xaction) { 45 + 46 + $type = $xaction->getTransactionType(); 47 + $new = $xaction->getNewValue(); 48 + 49 + switch ($type) { 50 + case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS: 51 + $object->setStatus($xaction->getNewValue()); 52 + return; 53 + } 54 + 55 + return parent::applyCustomInternalTransaction($object, $xaction); 56 + } 57 + 58 + protected function applyCustomExternalTransaction( 59 + PhabricatorLiskDAO $object, 60 + PhabricatorApplicationTransaction $xaction) { 61 + 62 + $type = $xaction->getTransactionType(); 63 + $new = $xaction->getNewValue(); 64 + 65 + switch ($type) { 66 + case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS: 67 + switch ($new) { 68 + case PhabricatorWorkerBulkJob::STATUS_WAITING: 69 + PhabricatorWorker::scheduleTask( 70 + 'PhabricatorWorkerBulkJobCreateWorker', 71 + array( 72 + 'jobID' => $object->getID(), 73 + ), 74 + array( 75 + 'priority' => PhabricatorWorker::PRIORITY_BULK, 76 + )); 77 + break; 78 + } 79 + return; 80 + } 81 + 82 + return parent::applyCustomExternalTransaction($object, $xaction); 83 + } 84 + 85 + 86 + 87 + }
+37
src/infrastructure/daemon/workers/phid/PhabricatorWorkerBulkJobPHIDType.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobPHIDType extends PhabricatorPHIDType { 4 + 5 + const TYPECONST = 'BULK'; 6 + 7 + public function getTypeName() { 8 + return pht('Bulk Job'); 9 + } 10 + 11 + public function newObject() { 12 + return new PhabricatorWorkerBulkJob(); 13 + } 14 + 15 + protected function buildQueryForObjects( 16 + PhabricatorObjectQuery $query, 17 + array $phids) { 18 + 19 + return id(new PhabricatorWorkerBulkJobQuery()) 20 + ->withPHIDs($phids); 21 + } 22 + 23 + public function loadHandles( 24 + PhabricatorHandleQuery $query, 25 + array $handles, 26 + array $objects) { 27 + 28 + foreach ($handles as $phid => $handle) { 29 + $job = $objects[$phid]; 30 + 31 + $id = $job->getID(); 32 + 33 + $handle->setName(pht('Bulk Job %d', $id)); 34 + } 35 + } 36 + 37 + }
+106
src/infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $phids; 8 + private $authorPHIDs; 9 + private $bulkJobTypes; 10 + private $statuses; 11 + 12 + public function withIDs(array $ids) { 13 + $this->ids = $ids; 14 + return $this; 15 + } 16 + 17 + public function withPHIDs(array $phids) { 18 + $this->phids = $phids; 19 + return $this; 20 + } 21 + 22 + public function withAuthorPHIDs(array $author_phids) { 23 + $this->authorPHIDs = $author_phids; 24 + return $this; 25 + } 26 + 27 + public function withBulkJobTypes(array $job_types) { 28 + $this->bulkJobTypes = $job_types; 29 + return $this; 30 + } 31 + 32 + public function withStatuses(array $statuses) { 33 + $this->statuses = $statuses; 34 + return $this; 35 + } 36 + 37 + public function newResultObject() { 38 + return new PhabricatorWorkerBulkJob(); 39 + } 40 + 41 + protected function loadPage() { 42 + return $this->loadStandardPage($this->newResultObject()); 43 + } 44 + 45 + protected function willFilterPage(array $page) { 46 + $map = PhabricatorWorkerBulkJobType::getAllJobTypes(); 47 + 48 + foreach ($page as $key => $job) { 49 + $implementation = idx($map, $job->getJobTypeKey()); 50 + if (!$implementation) { 51 + $this->didRejectResult($job); 52 + unset($page[$key]); 53 + continue; 54 + } 55 + $job->attachJobImplementation($implementation); 56 + } 57 + 58 + return $page; 59 + } 60 + 61 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 62 + $where = parent::buildWhereClauseParts($conn); 63 + 64 + if ($this->ids !== null) { 65 + $where[] = qsprintf( 66 + $conn, 67 + 'id IN (%Ld)', 68 + $this->ids); 69 + } 70 + 71 + if ($this->phids !== null) { 72 + $where[] = qsprintf( 73 + $conn, 74 + 'phid IN (%Ls)', 75 + $this->phids); 76 + } 77 + 78 + if ($this->authorPHIDs !== null) { 79 + $where[] = qsprintf( 80 + $conn, 81 + 'authorPHID IN (%Ls)', 82 + $this->authorPHIDs); 83 + } 84 + 85 + if ($this->bulkJobTypes !== null) { 86 + $where[] = qsprintf( 87 + $conn, 88 + 'bulkJobType IN (%Ls)', 89 + $this->bulkJobTypes); 90 + } 91 + 92 + if ($this->statuses !== null) { 93 + $where[] = qsprintf( 94 + $conn, 95 + 'status IN (%Ls)', 96 + $this->statuses); 97 + } 98 + 99 + return $where; 100 + } 101 + 102 + public function getQueryApplicationClass() { 103 + return 'PhabricatorDaemonsApplication'; 104 + } 105 + 106 + }
+98
src/infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobSearchEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobSearchEngine 4 + extends PhabricatorApplicationSearchEngine { 5 + 6 + public function getResultTypeDescription() { 7 + return pht('Bulk Jobs'); 8 + } 9 + 10 + public function getApplicationClassName() { 11 + return 'PhabricatorDaemonsApplication'; 12 + } 13 + 14 + public function newQuery() { 15 + return id(new PhabricatorWorkerBulkJobQuery()); 16 + } 17 + 18 + protected function buildQueryFromParameters(array $map) { 19 + $query = $this->newQuery(); 20 + 21 + if ($map['authorPHIDs']) { 22 + $query->withAuthorPHIDs($map['authorPHIDs']); 23 + } 24 + 25 + return $query; 26 + } 27 + 28 + protected function buildCustomSearchFields() { 29 + return array( 30 + id(new PhabricatorSearchUsersField()) 31 + ->setLabel(pht('Authors')) 32 + ->setKey('authorPHIDs') 33 + ->setAliases(array('author', 'authors')), 34 + ); 35 + } 36 + 37 + protected function getURI($path) { 38 + return '/daemon/bulk/'.$path; 39 + } 40 + 41 + protected function getBuiltinQueryNames() { 42 + $names = array(); 43 + 44 + if ($this->requireViewer()->isLoggedIn()) { 45 + $names['authored'] = pht('Authored Jobs'); 46 + } 47 + 48 + $names['all'] = pht('All Jobs'); 49 + 50 + return $names; 51 + } 52 + 53 + public function buildSavedQueryFromBuiltin($query_key) { 54 + 55 + $query = $this->newSavedQuery(); 56 + $query->setQueryKey($query_key); 57 + 58 + switch ($query_key) { 59 + case 'all': 60 + return $query; 61 + case 'authored': 62 + return $query->setParameter( 63 + 'authorPHIDs', 64 + array($this->requireViewer()->getPHID())); 65 + } 66 + 67 + return parent::buildSavedQueryFromBuiltin($query_key); 68 + } 69 + 70 + protected function renderResultList( 71 + array $jobs, 72 + PhabricatorSavedQuery $query, 73 + array $handles) { 74 + assert_instances_of($jobs, 'PhabricatorWorkerBulkJob'); 75 + 76 + $viewer = $this->requireViewer(); 77 + 78 + $list = id(new PHUIObjectItemListView()) 79 + ->setUser($viewer); 80 + foreach ($jobs as $job) { 81 + $size = pht('%s Bulk Task(s)', new PhutilNumber($job->getSize())); 82 + 83 + $item = id(new PHUIObjectItemView()) 84 + ->setObjectName(pht('Bulk Job %d', $job->getID())) 85 + ->setHeader($job->getJobName()) 86 + ->addAttribute(phabricator_datetime($job->getDateCreated(), $viewer)) 87 + ->setHref($job->getManageURI()) 88 + ->addIcon($job->getStatusIcon(), $job->getStatusName()) 89 + ->addIcon('none', $size); 90 + 91 + $list->addItem($item); 92 + } 93 + 94 + // TODO: Needs new wrapper when merging to redesign. 95 + 96 + return $list; 97 + } 98 + }
+10
src/infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobTransactionQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobTransactionQuery 4 + extends PhabricatorApplicationTransactionQuery { 5 + 6 + public function getTemplateApplicationTransaction() { 7 + return new PhabricatorWorkerBulkJobTransaction(); 8 + } 9 + 10 + }
+272
src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php
··· 1 + <?php 2 + 3 + /** 4 + * @task implementation Job Implementation 5 + */ 6 + final class PhabricatorWorkerBulkJob 7 + extends PhabricatorWorkerDAO 8 + implements 9 + PhabricatorPolicyInterface, 10 + PhabricatorSubscribableInterface, 11 + PhabricatorApplicationTransactionInterface, 12 + PhabricatorDestructibleInterface { 13 + 14 + const STATUS_CONFIRM = 'confirm'; 15 + const STATUS_WAITING = 'waiting'; 16 + const STATUS_RUNNING = 'running'; 17 + const STATUS_COMPLETE = 'complete'; 18 + 19 + protected $authorPHID; 20 + protected $jobTypeKey; 21 + protected $status; 22 + protected $parameters = array(); 23 + protected $size; 24 + 25 + private $jobImplementation = self::ATTACHABLE; 26 + 27 + protected function getConfiguration() { 28 + return array( 29 + self::CONFIG_AUX_PHID => true, 30 + self::CONFIG_SERIALIZATION => array( 31 + 'parameters' => self::SERIALIZATION_JSON, 32 + ), 33 + self::CONFIG_COLUMN_SCHEMA => array( 34 + 'jobTypeKey' => 'text32', 35 + 'status' => 'text32', 36 + 'size' => 'uint32', 37 + ), 38 + self::CONFIG_KEY_SCHEMA => array( 39 + 'key_type' => array( 40 + 'columns' => array('jobTypeKey'), 41 + ), 42 + 'key_author' => array( 43 + 'columns' => array('authorPHID'), 44 + ), 45 + 'key_status' => array( 46 + 'columns' => array('status'), 47 + ), 48 + ), 49 + ) + parent::getConfiguration(); 50 + } 51 + 52 + public static function initializeNewJob( 53 + PhabricatorUser $actor, 54 + PhabricatorWorkerBulkJobType $type, 55 + array $parameters) { 56 + 57 + $job = id(new PhabricatorWorkerBulkJob()) 58 + ->setAuthorPHID($actor->getPHID()) 59 + ->setJobTypeKey($type->getBulkJobTypeKey()) 60 + ->setParameters($parameters) 61 + ->attachJobImplementation($type); 62 + 63 + $job->setSize($job->computeSize()); 64 + 65 + return $job; 66 + } 67 + 68 + public function generatePHID() { 69 + return PhabricatorPHID::generateNewPHID( 70 + PhabricatorWorkerBulkJobPHIDType::TYPECONST); 71 + } 72 + 73 + public function getMonitorURI() { 74 + return '/daemon/bulk/monitor/'.$this->getID().'/'; 75 + } 76 + 77 + public function getManageURI() { 78 + return '/daemon/bulk/view/'.$this->getID().'/'; 79 + } 80 + 81 + public function getParameter($key, $default = null) { 82 + return idx($this->parameters, $key, $default); 83 + } 84 + 85 + public function setParameter($key, $value) { 86 + $this->parameters[$key] = $value; 87 + return $this; 88 + } 89 + 90 + public function loadTaskStatusCounts() { 91 + $table = new PhabricatorWorkerBulkTask(); 92 + $conn_r = $table->establishConnection('r'); 93 + $rows = queryfx_all( 94 + $conn_r, 95 + 'SELECT status, COUNT(*) N FROM %T WHERE bulkJobPHID = %s 96 + GROUP BY status', 97 + $table->getTableName(), 98 + $this->getPHID()); 99 + 100 + return ipull($rows, 'N', 'status'); 101 + } 102 + 103 + public function newContentSource() { 104 + return PhabricatorContentSource::newForSource( 105 + PhabricatorContentSource::SOURCE_BULK, 106 + array( 107 + 'jobID' => $this->getID(), 108 + )); 109 + } 110 + 111 + public function getStatusIcon() { 112 + $map = array( 113 + self::STATUS_CONFIRM => 'fa-question', 114 + self::STATUS_WAITING => 'fa-clock-o', 115 + self::STATUS_RUNNING => 'fa-clock-o', 116 + self::STATUS_COMPLETE => 'fa-check grey', 117 + ); 118 + 119 + return idx($map, $this->getStatus(), 'none'); 120 + } 121 + 122 + public function getStatusName() { 123 + $map = array( 124 + self::STATUS_CONFIRM => pht('Confirming'), 125 + self::STATUS_WAITING => pht('Waiting'), 126 + self::STATUS_RUNNING => pht('Running'), 127 + self::STATUS_COMPLETE => pht('Complete'), 128 + ); 129 + 130 + return idx($map, $this->getStatus(), $this->getStatus()); 131 + } 132 + 133 + 134 + /* -( Job Implementation )------------------------------------------------- */ 135 + 136 + 137 + protected function getJobImplementation() { 138 + return $this->assertAttached($this->jobImplementation); 139 + } 140 + 141 + public function attachJobImplementation(PhabricatorWorkerBulkJobType $type) { 142 + $this->jobImplementation = $type; 143 + return $this; 144 + } 145 + 146 + private function computeSize() { 147 + return $this->getJobImplementation()->getJobSize($this); 148 + } 149 + 150 + public function getCancelURI() { 151 + return $this->getJobImplementation()->getCancelURI($this); 152 + } 153 + 154 + public function getDoneURI() { 155 + return $this->getJobImplementation()->getDoneURI($this); 156 + } 157 + 158 + public function getDescriptionForConfirm() { 159 + return $this->getJobImplementation()->getDescriptionForConfirm($this); 160 + } 161 + 162 + public function createTasks() { 163 + return $this->getJobImplementation()->createTasks($this); 164 + } 165 + 166 + public function runTask( 167 + PhabricatorUser $actor, 168 + PhabricatorWorkerBulkTask $task) { 169 + return $this->getJobImplementation()->runTask($actor, $this, $task); 170 + } 171 + 172 + public function getJobName() { 173 + return $this->getJobImplementation()->getJobName($this); 174 + } 175 + 176 + 177 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 178 + 179 + 180 + public function getCapabilities() { 181 + return array( 182 + PhabricatorPolicyCapability::CAN_VIEW, 183 + PhabricatorPolicyCapability::CAN_EDIT, 184 + ); 185 + } 186 + 187 + public function getPolicy($capability) { 188 + switch ($capability) { 189 + case PhabricatorPolicyCapability::CAN_VIEW: 190 + return PhabricatorPolicies::getMostOpenPolicy(); 191 + case PhabricatorPolicyCapability::CAN_EDIT: 192 + return $this->getAuthorPHID(); 193 + } 194 + } 195 + 196 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 197 + return false; 198 + } 199 + 200 + public function describeAutomaticCapability($capability) { 201 + switch ($capability) { 202 + case PhabricatorPolicyCapability::CAN_EDIT: 203 + return pht('Only the owner of a bulk job can edit it.'); 204 + default: 205 + return null; 206 + } 207 + } 208 + 209 + 210 + /* -( PhabricatorSubscribableInterface )----------------------------------- */ 211 + 212 + 213 + public function isAutomaticallySubscribed($phid) { 214 + return false; 215 + } 216 + 217 + public function shouldShowSubscribersProperty() { 218 + return true; 219 + } 220 + 221 + public function shouldAllowSubscription($phid) { 222 + return true; 223 + } 224 + 225 + 226 + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ 227 + 228 + 229 + public function getApplicationTransactionEditor() { 230 + return new PhabricatorWorkerBulkJobEditor(); 231 + } 232 + 233 + public function getApplicationTransactionObject() { 234 + return $this; 235 + } 236 + 237 + public function getApplicationTransactionTemplate() { 238 + return new PhabricatorWorkerBulkJobTransaction(); 239 + } 240 + 241 + public function willRenderTimeline( 242 + PhabricatorApplicationTransactionView $timeline, 243 + AphrontRequest $request) { 244 + return $timeline; 245 + } 246 + 247 + /* -( PhabricatorDestructibleInterface )----------------------------------- */ 248 + 249 + 250 + public function destroyObjectPermanently( 251 + PhabricatorDestructionEngine $engine) { 252 + 253 + $this->openTransaction(); 254 + 255 + // We're only removing the actual task objects. This may leave stranded 256 + // workers in the queue itself, but they'll just flush out automatically 257 + // when they can't load bulk job data. 258 + 259 + $task_table = new PhabricatorWorkerBulkTask(); 260 + $conn_w = $task_table->establishConnection('w'); 261 + queryfx( 262 + $conn_w, 263 + 'DELETE FROM %T WHERE bulkJobPHID = %s', 264 + $task_table->getPHID(), 265 + $this->getPHID()); 266 + 267 + $this->delete(); 268 + $this->saveTransaction(); 269 + } 270 + 271 + 272 + }
+51
src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJobTransaction.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkJobTransaction 4 + extends PhabricatorApplicationTransaction { 5 + 6 + const TYPE_STATUS = 'bulkjob.status'; 7 + 8 + public function getApplicationName() { 9 + return 'worker'; 10 + } 11 + 12 + public function getApplicationTransactionType() { 13 + return PhabricatorWorkerBulkJobPHIDType::TYPECONST; 14 + } 15 + 16 + public function getTitle() { 17 + $author_phid = $this->getAuthorPHID(); 18 + 19 + $old = $this->getOldValue(); 20 + $new = $this->getNewValue(); 21 + 22 + $type = $this->getTransactionType(); 23 + switch ($type) { 24 + case self::TYPE_STATUS: 25 + if ($old === null) { 26 + return pht( 27 + '%s created this bulk job.', 28 + $this->renderHandleLink($author_phid)); 29 + } else { 30 + switch ($new) { 31 + case PhabricatorWorkerBulkJob::STATUS_WAITING: 32 + return pht( 33 + '%s confirmed this job.', 34 + $this->renderHandleLink($author_phid)); 35 + case PhabricatorWorkerBulkJob::STATUS_RUNNING: 36 + return pht( 37 + '%s marked this job as running.', 38 + $this->renderHandleLink($author_phid)); 39 + case PhabricatorWorkerBulkJob::STATUS_COMPLETE: 40 + return pht( 41 + '%s marked this job complete.', 42 + $this->renderHandleLink($author_phid)); 43 + } 44 + } 45 + break; 46 + } 47 + 48 + return parent::getTitle(); 49 + } 50 + 51 + }
+46
src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkTask.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerBulkTask 4 + extends PhabricatorWorkerDAO { 5 + 6 + const STATUS_WAITING = 'waiting'; 7 + const STATUS_RUNNING = 'running'; 8 + const STATUS_DONE = 'done'; 9 + const STATUS_FAIL = 'fail'; 10 + 11 + protected $bulkJobPHID; 12 + protected $objectPHID; 13 + protected $status; 14 + protected $data = array(); 15 + 16 + protected function getConfiguration() { 17 + return array( 18 + self::CONFIG_TIMESTAMPS => false, 19 + self::CONFIG_SERIALIZATION => array( 20 + 'data' => self::SERIALIZATION_JSON, 21 + ), 22 + self::CONFIG_COLUMN_SCHEMA => array( 23 + 'status' => 'text32', 24 + ), 25 + self::CONFIG_KEY_SCHEMA => array( 26 + 'key_job' => array( 27 + 'columns' => array('bulkJobPHID', 'status'), 28 + ), 29 + 'key_object' => array( 30 + 'columns' => array('objectPHID'), 31 + ), 32 + ), 33 + ) + parent::getConfiguration(); 34 + } 35 + 36 + public static function initializeNewTask( 37 + PhabricatorWorkerBulkJob $job, 38 + $object_phid) { 39 + 40 + return id(new PhabricatorWorkerBulkTask()) 41 + ->setBulkJobPHID($job->getPHID()) 42 + ->setStatus(self::STATUS_WAITING) 43 + ->setObjectPHID($object_phid); 44 + } 45 + 46 + }
+10
src/infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerSchemaSpec 4 + extends PhabricatorConfigSchemaSpec { 5 + 6 + public function buildSchemata() { 7 + $this->buildEdgeSchemata(new PhabricatorWorkerBulkJob()); 8 + } 9 + 10 + }
+5
src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
··· 1177 1177 '%s Broken Test(s)' => '%s Broken', 1178 1178 '%s Unsound Test(s)' => '%s Unsound', 1179 1179 '%s Other Test(s)' => '%s Other', 1180 + 1181 + '%s Bulk Task(s)' => array( 1182 + '%s Task', 1183 + '%s Tasks', 1184 + ), 1180 1185 ); 1181 1186 } 1182 1187
+32
webroot/rsrc/css/application/daemon/bulk-job.css
··· 1 + /** 2 + * @provides bulk-job-css 3 + */ 4 + 5 + .bulk-job-progress-bar { 6 + position: relative; 7 + width: 100%; 8 + border: 1px solid {$lightgreyborder}; 9 + height: 32px; 10 + } 11 + 12 + .bulk-job-progress-slice { 13 + position: absolute; 14 + top: 0; 15 + bottom: 0; 16 + } 17 + 18 + .bulk-job-progress-slice-green { 19 + background-color: {$green}; 20 + } 21 + 22 + .bulk-job-progress-slice-blue { 23 + background-color: {$blue}; 24 + } 25 + 26 + .bulk-job-progress-slice-red { 27 + background-color: {$red}; 28 + } 29 + 30 + .bulk-job-progress-slice-empty { 31 + background-color: {$lightbluebackground}; 32 + }
+18
webroot/rsrc/js/application/daemon/behavior-bulk-job-reload.js
··· 1 + /** 2 + * @provides javelin-behavior-bulk-job-reload 3 + * @requires javelin-behavior 4 + * javelin-uri 5 + */ 6 + 7 + JX.behavior('bulk-job-reload', function() { 8 + 9 + // TODO: It would be nice to have a pretty Ajax progress bar here, but just 10 + // reload the page for now. 11 + 12 + function reload() { 13 + JX.$U().go(); 14 + } 15 + 16 + setTimeout(reload, 1000); 17 + 18 + });