From 1eb8b6a1b8c56618ee59ee3ac4702d74d1821cc2 Mon Sep 17 00:00:00 2001
From: Bob Trahan <btrahan@phacility.com>
Date: Mon, 12 Jan 2015 13:42:37 -0800
Subject: [PATCH] Maniphest - allow for searching for tasks based on dependency
 relationships

Summary:
Fixes T5352. This is very useful for finding things that should be easy to do ("not blocked") as well as things that are important to do ("blocking"). I have wanted to check out the latter case in our installation, though no promises on what I would end up actually doing from that search result list. =D

I also think supporting something like T6638 is reasonable but the UI seems trickier to me; its some sort of task tokenizer, which I don't think we've done before?

Test Plan: toggled various search options and got reasonable results. When i clicked conflicting things like "blocking" and "not blocking" verified it was like I had not clicked anything at all.

Reviewers: chad, epriestley

Reviewed By: epriestley

Subscribers: Korvin, epriestley

Maniphest Tasks: T5352

Differential Revision: https://secure.phabricator.com/D11306
---
 .../maniphest/query/ManiphestTaskQuery.php    | 83 +++++++++++++++++++
 .../query/ManiphestTaskSearchEngine.php       | 32 ++++++-
 2 files changed, 114 insertions(+), 1 deletion(-)

diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index 51cb60ebbf..ba22f024c4 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -53,6 +53,9 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
   private $needSubscriberPHIDs;
   private $needProjectPHIDs;
 
+  private $blockingTasks;
+  private $blockedTasks;
+
   const DEFAULT_PAGE_SIZE   = 1000;
 
   public function withAuthors(array $authors) {
@@ -161,6 +164,34 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
     return $this;
   }
 
+  /**
+   * True returns tasks that are blocking other tasks only.
+   * False returns tasks that are not blocking other tasks only.
+   * Null returns tasks regardless of blocking status.
+   */
+  public function withBlockingTasks($mode) {
+    $this->blockingTasks = $mode;
+    return $this;
+  }
+
+  public function shouldJoinBlockingTasks() {
+    return $this->blockingTasks !== null;
+  }
+
+  /**
+   * True returns tasks that are blocked by other tasks only.
+   * False returns tasks that are not blocked by other tasks only.
+   * Null returns tasks regardless of blocked by status.
+   */
+  public function withBlockedTasks($mode) {
+    $this->blockedTasks = $mode;
+    return $this;
+  }
+
+  public function shouldJoinBlockedTasks() {
+    return $this->blockedTasks !== null;
+  }
+
   public function withDateCreatedBefore($date_created_before) {
     $this->dateCreatedBefore = $date_created_before;
     return $this;
@@ -207,6 +238,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
     $where[] = $this->buildStatusWhereClause($conn);
     $where[] = $this->buildStatusesWhereClause($conn);
     $where[] = $this->buildPrioritiesWhereClause($conn);
+    $where[] = $this->buildDependenciesWhereClause($conn);
     $where[] = $this->buildAuthorWhereClause($conn);
     $where[] = $this->buildOwnerWhereClause($conn);
     $where[] = $this->buildProjectWhereClause($conn);
@@ -520,6 +552,38 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
       $fulltext_results);
   }
 
+  private function buildDependenciesWhereClause(
+    AphrontDatabaseConnection $conn) {
+
+    if (!$this->shouldJoinBlockedTasks() &&
+        !$this->shouldJoinBlockingTasks()) {
+      return null;
+    }
+
+    $parts = array();
+    if ($this->blockingTasks === true) {
+      $parts[] = qsprintf(
+        $conn,
+        'blocking.dst IS NOT NULL');
+    } else if ($this->blockingTasks === false) {
+      $parts[] = qsprintf(
+        $conn,
+        'blocking.dst IS NULL');
+    }
+
+    if ($this->blockedTasks === true) {
+      $parts[] = qsprintf(
+        $conn,
+        'blocked.dst IS NOT NULL');
+    } else if ($this->blockedTasks === false) {
+      $parts[] = qsprintf(
+        $conn,
+        'blocked.dst IS NULL');
+    }
+
+    return '('.implode(') OR (', $parts).')';
+  }
+
   private function buildProjectWhereClause(AphrontDatabaseConnection $conn) {
     if (!$this->projectPHIDs && !$this->includeNoProject) {
       return null;
@@ -699,6 +763,23 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
     }
 
+    if ($this->shouldJoinBlockingTasks()) {
+      $joins[] = qsprintf(
+        $conn_r,
+        'LEFT JOIN %T blocking ON blocking.src = task.phid '.
+        'AND blocking.type = %d',
+        $edge_table,
+        ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
+    }
+    if ($this->shouldJoinBlockedTasks()) {
+      $joins[] = qsprintf(
+        $conn_r,
+        'LEFT JOIN %T blocked ON blocked.src = task.phid '.
+        'AND blocked.type = %d',
+        $edge_table,
+        ManiphestTaskDependsOnTaskEdgeType::EDGECONST);
+    }
+
     if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) {
       $joins[] = qsprintf(
         $conn_r,
@@ -766,6 +847,8 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
   private function buildGroupClause(AphrontDatabaseConnection $conn_r) {
     $joined_multiple_rows = (count($this->projectPHIDs) > 1) ||
                             (count($this->anyProjectPHIDs) > 1) ||
+                            $this->shouldJoinBlockingTasks() ||
+                            $this->shouldJoinBlockedTasks() ||
                             ($this->getApplicationSearchMayJoinMultipleRows());
 
     $joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
index b500b995e4..4d0e1381d7 100644
--- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
+++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
@@ -67,6 +67,13 @@ final class ManiphestTaskSearchEngine
       'priorities',
       $this->readListFromRequest($request, 'priorities'));
 
+    $saved->setParameter(
+      'blocking',
+      $this->readBoolFromRequest($request, 'blocking'));
+    $saved->setParameter(
+      'blocked',
+      $this->readBoolFromRequest($request, 'blocked'));
+
     $saved->setParameter('group', $request->getStr('group'));
     $saved->setParameter('order', $request->getStr('order'));
 
@@ -152,6 +159,10 @@ final class ManiphestTaskSearchEngine
       $query->withPriorities($priorities);
     }
 
+
+    $query->withBlockingTasks($saved->getParameter('blocking'));
+    $query->withBlockedTasks($saved->getParameter('blocked'));
+
     $this->applyOrderByToQuery(
       $query,
       $this->getOrderValues(),
@@ -302,6 +313,23 @@ final class ManiphestTaskSearchEngine
         isset($priorities[$pri]));
     }
 
+    $blocking_control = id(new AphrontFormSelectControl())
+      ->setLabel(pht('Blocking'))
+      ->setName('blocking')
+      ->setValue($this->getBoolFromQuery($saved, 'blocking'))
+      ->setOptions(array(
+        '' => pht('Show All Tasks'),
+        'true' => pht('Show Tasks Blocking Other Tasks'),
+        'false' => pht('Show Tasks Not Blocking Other Tasks'),));
+    $blocked_control = id(new AphrontFormSelectControl())
+      ->setLabel(pht('Blocked'))
+      ->setName('blocked')
+      ->setValue($this->getBoolFromQuery($saved, 'blocked'))
+      ->setOptions(array(
+        '' => pht('Show All Tasks'),
+        'true' => pht('Show Tasks Blocked By Other Tasks'),
+        'false' => pht('Show Tasks Not Blocked By Other Tasks'),));
+
     $ids = $saved->getParameter('ids', array());
 
     $builtin_orders = $this->getOrderOptions();
@@ -377,7 +405,9 @@ final class ManiphestTaskSearchEngine
           ->setLabel(pht('Contains Words'))
           ->setValue($saved->getParameter('fulltext')))
       ->appendChild($status_control)
-      ->appendChild($priority_control);
+      ->appendChild($priority_control)
+      ->appendChild($blocking_control)
+      ->appendChild($blocked_control);
 
     if (!$this->getIsBoardView()) {
       $form