Changeset View
Changeset View
Standalone View
Standalone View
src/applications/maniphest/editor/ManiphestTransactionEditor.php
| Show First 20 Lines • Show All 291 Lines • ▼ Show 20 Lines | foreach ($xactions as $xaction) { | ||||
| default: | default: | ||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| return $copy; | return $copy; | ||||
| } | } | ||||
| /** | |||||
| * Get priorities for moving a task to a new priority. | |||||
| */ | |||||
| public static function getEdgeSubpriority( | |||||
| $priority, | |||||
| $is_end) { | |||||
| $query = id(new ManiphestTaskQuery()) | |||||
| ->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
| ->withPriorities(array($priority)) | |||||
| ->setLimit(1); | |||||
| if ($is_end) { | |||||
| $query->setOrderVector(array('-priority', '-subpriority', '-id')); | |||||
| } else { | |||||
| $query->setOrderVector(array('priority', 'subpriority', 'id')); | |||||
| } | |||||
| $result = $query->executeOne(); | |||||
| $step = (double)(2 << 32); | |||||
| if ($result) { | |||||
| $base = $result->getSubpriority(); | |||||
| if ($is_end) { | |||||
| $sub = ($base - $step); | |||||
| } else { | |||||
| $sub = ($base + $step); | |||||
| } | |||||
| } else { | |||||
| $sub = 0; | |||||
| } | |||||
| return array($priority, $sub); | |||||
| } | |||||
| /** | |||||
| * Get priorities for moving a task before or after another task. | |||||
| */ | |||||
| public static function getAdjacentSubpriority( | |||||
| ManiphestTask $dst, | |||||
| $is_after) { | |||||
| $query = id(new ManiphestTaskQuery()) | |||||
| ->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
| ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) | |||||
| ->withPriorities(array($dst->getPriority())) | |||||
| ->setLimit(1); | |||||
| if ($is_after) { | |||||
| $query->setAfterID($dst->getID()); | |||||
| } else { | |||||
| $query->setBeforeID($dst->getID()); | |||||
| } | |||||
| $adjacent = $query->executeOne(); | |||||
| $base = $dst->getSubpriority(); | |||||
| $step = (double)(2 << 32); | |||||
| // If we find an adjacent task, we average the two subpriorities and | |||||
| // return the result. | |||||
| if ($adjacent) { | |||||
| $epsilon = 1.0; | |||||
| // If the adjacent task has a subpriority that is identical or very | |||||
| // close to the task we're looking at, we're going to spread out all | |||||
| // the nearby tasks. | |||||
| $adjacent_sub = $adjacent->getSubpriority(); | |||||
| if ((abs($adjacent_sub - $base) < $epsilon)) { | |||||
| $base = self::disperseBlock( | |||||
| $dst, | |||||
| $epsilon * 2); | |||||
| if ($is_after) { | |||||
| $sub = $base - $epsilon; | |||||
| } else { | |||||
| $sub = $base + $epsilon; | |||||
| } | |||||
| } else { | |||||
| $sub = ($adjacent_sub + $base) / 2; | |||||
| } | |||||
| } else { | |||||
| // Otherwise, we take a step away from the target's subpriority and | |||||
| // use that. | |||||
| if ($is_after) { | |||||
| $sub = ($base - $step); | |||||
| } else { | |||||
| $sub = ($base + $step); | |||||
| } | |||||
| } | |||||
| return array($dst->getPriority(), $sub); | |||||
| } | |||||
| /** | |||||
| * Distribute a cluster of tasks with similar subpriorities. | |||||
| */ | |||||
| private static function disperseBlock( | |||||
| ManiphestTask $task, | |||||
| $spacing) { | |||||
| $conn = $task->establishConnection('w'); | |||||
| // Find a block of subpriority space which is, on average, sparse enough | |||||
| // to hold all the tasks that are inside it with a reasonable level of | |||||
| // separation between them. | |||||
amckinley: This is nuts; I had no idea this code was so complicated. | |||||
| // We'll start by looking near the target task for a range of numbers | |||||
| // which has more space available than tasks. For example, if the target | |||||
| // task has subpriority 33 and we want to separate each task by at least 1, | |||||
| // we might start by looking in the range [23, 43]. | |||||
| // If we find fewer than 20 tasks there, we have room to reassign them | |||||
| // with the desired level of separation. We space them out, then we're | |||||
| // done. | |||||
| // However: if we find more than 20 tasks, we don't have enough room to | |||||
| // distribute them. We'll widen our search and look in a bigger range, | |||||
| // maybe [13, 53]. This range has more space, so if we find fewer than | |||||
| // 40 tasks in this range we can spread them out. If we still find too | |||||
| // many tasks, we keep widening the search. | |||||
Done Inline ActionsParticularly, this is the clever bit. epriestley: Particularly, this is the clever bit. | |||||
| $base = $task->getSubpriority(); | |||||
| $scale = 4.0; | |||||
| while (true) { | |||||
| $range = ($spacing * $scale) / 2.0; | |||||
| $min = ($base - $range); | |||||
| $max = ($base + $range); | |||||
| $result = queryfx_one( | |||||
| $conn, | |||||
| 'SELECT COUNT(*) N FROM %T WHERE priority = %d AND | |||||
| subpriority BETWEEN %f AND %f', | |||||
| $task->getTableName(), | |||||
| $task->getPriority(), | |||||
| $min, | |||||
| $max); | |||||
| $count = $result['N']; | |||||
| if ($count < $scale) { | |||||
| // We have found a block which we can make sparse enough, so bail and | |||||
| // continue below with our selection. | |||||
| break; | |||||
| } | |||||
| // This block had too many tasks for its size, so try again with a | |||||
| // bigger block. | |||||
| $scale *= 2.0; | |||||
| } | |||||
| $rows = queryfx_all( | |||||
| $conn, | |||||
| 'SELECT id FROM %T WHERE priority = %d AND | |||||
| subpriority BETWEEN %f AND %f | |||||
| ORDER BY priority, subpriority, id', | |||||
| $task->getTableName(), | |||||
| $task->getPriority(), | |||||
| $min, | |||||
| $max); | |||||
| $task_id = $task->getID(); | |||||
| $result = null; | |||||
| // NOTE: In strict mode (which we encourage enabling) we can't structure | |||||
| // this bulk update as an "INSERT ... ON DUPLICATE KEY UPDATE" unless we | |||||
| // provide default values for ALL of the columns that don't have defaults. | |||||
| // This is gross, but we may be moving enough rows that individual | |||||
| // queries are unreasonably slow. An alternate construction which might | |||||
| // be worth evaluating is to use "CASE". Another approach is to disable | |||||
| // strict mode for this query. | |||||
| $default_str = qsprintf($conn, '%s', ''); | |||||
| $default_int = qsprintf($conn, '%d', 0); | |||||
| $extra_columns = array( | |||||
| 'phid' => $default_str, | |||||
| 'authorPHID' => $default_str, | |||||
| 'status' => $default_str, | |||||
| 'priority' => $default_int, | |||||
| 'title' => $default_str, | |||||
| 'description' => $default_str, | |||||
| 'dateCreated' => $default_int, | |||||
| 'dateModified' => $default_int, | |||||
| 'mailKey' => $default_str, | |||||
| 'viewPolicy' => $default_str, | |||||
| 'editPolicy' => $default_str, | |||||
| 'ownerOrdering' => $default_str, | |||||
| 'spacePHID' => $default_str, | |||||
| 'bridgedObjectPHID' => $default_str, | |||||
| 'properties' => $default_str, | |||||
| 'points' => $default_int, | |||||
| 'subtype' => $default_str, | |||||
| ); | |||||
| $sql = array(); | |||||
| $offset = 0; | |||||
| // Often, we'll have more room than we need in the range. Distribute the | |||||
| // tasks evenly over the whole range so that we're less likely to end up | |||||
| // with tasks spaced exactly the minimum distance apart, which may | |||||
| // get shifted again later. We have one fewer space to distribute than we | |||||
| // have tasks. | |||||
| $divisor = (double)(count($rows) - 1.0); | |||||
| if ($divisor > 0) { | |||||
| $available_distance = (($max - $min) / $divisor); | |||||
| } else { | |||||
| $available_distance = 0.0; | |||||
| } | |||||
| foreach ($rows as $row) { | |||||
| $subpriority = $min + ($offset * $available_distance); | |||||
| // If this is the task that we're spreading out relative to, keep track | |||||
| // of where it is ending up so we can return the new subpriority. | |||||
| $id = $row['id']; | |||||
| if ($id == $task_id) { | |||||
| $result = $subpriority; | |||||
| } | |||||
| $sql[] = qsprintf( | |||||
| $conn, | |||||
| '(%d, %LQ, %f)', | |||||
| $id, | |||||
| $extra_columns, | |||||
| $subpriority); | |||||
| $offset++; | |||||
| } | |||||
| foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { | |||||
| queryfx( | |||||
| $conn, | |||||
| 'INSERT INTO %T (id, %LC, subpriority) VALUES %LQ | |||||
| ON DUPLICATE KEY UPDATE subpriority = VALUES(subpriority)', | |||||
| $task->getTableName(), | |||||
| array_keys($extra_columns), | |||||
| $chunk); | |||||
| } | |||||
| return $result; | |||||
| } | |||||
| protected function validateAllTransactions( | protected function validateAllTransactions( | ||||
| PhabricatorLiskDAO $object, | PhabricatorLiskDAO $object, | ||||
| array $xactions) { | array $xactions) { | ||||
| $errors = parent::validateAllTransactions($object, $xactions); | $errors = parent::validateAllTransactions($object, $xactions); | ||||
| if ($this->moreValidationErrors) { | if ($this->moreValidationErrors) { | ||||
| $errors = array_merge($errors, $this->moreValidationErrors); | $errors = array_merge($errors, $this->moreValidationErrors); | ||||
| ▲ Show 20 Lines • Show All 548 Lines • Show Last 20 Lines | |||||
This is nuts; I had no idea this code was so complicated.