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.