private function performMerge(ManiphestTask $task, PhabricatorObjectHandle $handle, array $phids)
 {
     $user = $this->getRequest()->getUser();
     $response = id(new AphrontReloadResponse())->setURI($handle->getURI());
     $phids = array_fill_keys($phids, true);
     unset($phids[$task->getPHID()]);
     // Prevent merging a task into itself.
     if (!$phids) {
         return $response;
     }
     $targets = id(new ManiphestTaskQuery())->setViewer($user)->requireCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT))->withPHIDs(array_keys($phids))->needSubscriberPHIDs(true)->needProjectPHIDs(true)->execute();
     if (empty($targets)) {
         return $response;
     }
     $editor = id(new ManiphestTransactionEditor())->setActor($user)->setContentSourceFromRequest($this->getRequest())->setContinueOnNoEffect(true)->setContinueOnMissingFields(true);
     $cc_vector = array();
     // since we loaded this via a generic object query, go ahead and get the
     // attach the subscriber and project phids now
     $task->attachSubscriberPHIDs(PhabricatorSubscribersQuery::loadSubscribersForPHID($task->getPHID()));
     $task->attachProjectPHIDs(PhabricatorEdgeQuery::loadDestinationPHIDs($task->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST));
     $cc_vector[] = $task->getSubscriberPHIDs();
     foreach ($targets as $target) {
         $cc_vector[] = $target->getSubscriberPHIDs();
         $cc_vector[] = array($target->getAuthorPHID(), $target->getOwnerPHID());
         $merged_into_txn = id(new ManiphestTransaction())->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO)->setNewValue($task->getPHID());
         $editor->applyTransactions($target, array($merged_into_txn));
     }
     $all_ccs = array_mergev($cc_vector);
     $all_ccs = array_filter($all_ccs);
     $all_ccs = array_unique($all_ccs);
     $add_ccs = id(new ManiphestTransaction())->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)->setNewValue(array('=' => $all_ccs));
     $merged_from_txn = id(new ManiphestTransaction())->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM)->setNewValue(mpull($targets, 'getPHID'));
     $editor->applyTransactions($task, array($add_ccs, $merged_from_txn));
     return $response;
 }
 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);
     $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("Unknown batch edit action '{$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;
             }
         }
         // 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 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;
 }