diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -3394,6 +3394,7 @@
     'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php',
     'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php',
     'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php',
+    'PhabricatorSearchRelationshipController' => 'applications/search/controller/PhabricatorSearchRelationshipController.php',
     'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php',
     'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php',
     'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
@@ -8213,6 +8214,7 @@
     'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchOrderField' => 'PhabricatorSearchField',
     'PhabricatorSearchRelationship' => 'Phobject',
+    'PhabricatorSearchRelationshipController' => 'PhabricatorSearchBaseController',
     'PhabricatorSearchResultBucket' => 'Phobject',
     'PhabricatorSearchResultBucketGroup' => 'Phobject',
     'PhabricatorSearchResultView' => 'AphrontView',
diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php
--- a/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php
+++ b/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php
@@ -24,4 +24,20 @@
     return false;
   }
 
+  public function canRelateObjects($src, $dst) {
+    return ($dst instanceof PhabricatorRepositoryCommit);
+  }
+
+  public function getDialogTitleText() {
+    return pht('Edit Related Commits');
+  }
+
+  public function getDialogHeaderText() {
+    return pht('Current Commits');
+  }
+
+  public function getDialogButtonText() {
+    return pht('Save Related Commits');
+  }
+
 }
diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php
--- a/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php
+++ b/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php
@@ -17,4 +17,20 @@
     return 'fa-camera-retro';
   }
 
+  public function canRelateObjects($src, $dst) {
+    return ($dst instanceof PholioMock);
+  }
+
+  public function getDialogTitleText() {
+    return pht('Edit Related Mocks');
+  }
+
+  public function getDialogHeaderText() {
+    return pht('Current Mocks');
+  }
+
+  public function getDialogButtonText() {
+    return pht('Save Related Mocks');
+  }
+
 }
diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php
--- a/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php
+++ b/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php
@@ -17,4 +17,20 @@
     return 'fa-cog';
   }
 
+  public function canRelateObjects($src, $dst) {
+    return ($dst instanceof DifferentialRevision);
+  }
+
+  public function getDialogTitleText() {
+    return pht('Edit Related Revisions');
+  }
+
+  public function getDialogHeaderText() {
+    return pht('Current Revisions');
+  }
+
+  public function getDialogButtonText() {
+    return pht('Save Related Revisions');
+  }
+
 }
diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php
--- a/src/applications/search/application/PhabricatorSearchApplication.php
+++ b/src/applications/search/application/PhabricatorSearchApplication.php
@@ -41,6 +41,8 @@
         'delete/(?P<queryKey>[^/]+)/(?P<engine>[^/]+)/'
           => 'PhabricatorSearchDeleteController',
         'order/(?P<engine>[^/]+)/' => 'PhabricatorSearchOrderController',
+        'rel/(?P<relationshipKey>[^/]+)/(?P<sourcePHID>[^/]+)/'
+          => 'PhabricatorSearchRelationshipController',
       ),
     );
   }
diff --git a/src/applications/search/controller/PhabricatorSearchRelationshipController.php b/src/applications/search/controller/PhabricatorSearchRelationshipController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/search/controller/PhabricatorSearchRelationshipController.php
@@ -0,0 +1,198 @@
+<?php
+
+final class PhabricatorSearchRelationshipController
+  extends PhabricatorSearchBaseController {
+
+  public function handleRequest(AphrontRequest $request) {
+    $viewer = $this->getViewer();
+
+    $phid = $request->getURIData('sourcePHID');
+    $object = id(new PhabricatorObjectQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($phid))
+      ->requireCapabilities(
+        array(
+          PhabricatorPolicyCapability::CAN_VIEW,
+          PhabricatorPolicyCapability::CAN_EDIT,
+        ))
+      ->executeOne();
+    if (!$object) {
+      return new Aphront404Response();
+    }
+
+    $list = PhabricatorObjectRelationshipList::newForObject(
+      $viewer,
+      $object);
+
+    $relationship_key = $request->getURIData('relationshipKey');
+    $relationship = $list->getRelationship($relationship_key);
+    if (!$relationship) {
+      return new Aphront404Response();
+    }
+
+    $src_phid = $object->getPHID();
+    $edge_type = $relationship->getEdgeConstant();
+
+    $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+      $src_phid,
+      $edge_type);
+
+    $all_phids = $dst_phids;
+    $all_phids[] = $src_phid;
+
+    $handles = $viewer->loadHandles($all_phids);
+    $src_handle = $handles[$src_phid];
+
+    $done_uri = $src_handle->getURI();
+
+    if ($request->isFormPost()) {
+      $phids = explode(';', $request->getStr('phids'));
+      $phids = array_filter($phids);
+      $phids = array_values($phids);
+
+      // TODO: Embed these in the form instead, to gracefully resolve
+      // concurrent edits like we do for subscribers and projects.
+      $old_phids = $dst_phids;
+
+      $add_phids = $phids;
+      $rem_phids = array_diff($old_phids, $add_phids);
+
+      if ($add_phids) {
+        $dst_objects = id(new PhabricatorObjectQuery())
+          ->setViewer($viewer)
+          ->withPHIDs($phids)
+          ->setRaisePolicyExceptions(true)
+          ->execute();
+        $dst_objects = mpull($dst_objects, null, 'getPHID');
+      } else {
+        $dst_objects = array();
+      }
+
+      try {
+        foreach ($add_phids as $add_phid) {
+          $dst_object = idx($dst_objects, $add_phid);
+          if (!$dst_object) {
+            throw new Exception(
+              pht(
+                'You can not create a relationship to object "%s" because '.
+                'the object does not exist or could not be loaded.',
+                $add_phid));
+          }
+
+          if (!$relationship->canRelateObjects($object, $dst_object)) {
+            throw new Exception(
+              pht(
+                'You can not create a relationship (of type "%s") to object '.
+                '"%s" because it is not the right type of object for this '.
+                'relationship.',
+                $relationship->getRelationshipConstant(),
+                $add_phid));
+          }
+        }
+      } catch (Exception $ex) {
+        return $this->newUnrelatableObjectResponse($ex, $done_uri);
+      }
+
+      $editor = $object->getApplicationTransactionEditor()
+        ->setActor($viewer)
+        ->setContentSourceFromRequest($request)
+        ->setContinueOnMissingFields(true)
+        ->setContinueOnNoEffect(true);
+
+      $xactions = array();
+      $xactions[] = $object->getApplicationTransactionTemplate()
+        ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+        ->setMetadataValue('edge:type', $edge_type)
+        ->setNewValue(array(
+          '+' => array_fuse($add_phids),
+          '-' => array_fuse($rem_phids),
+        ));
+
+      try {
+        $editor->applyTransactions($object, $xactions);
+
+        return id(new AphrontRedirectResponse())->setURI($done_uri);
+      } catch (PhabricatorEdgeCycleException $ex) {
+        return $this->newGraphCycleResponse($ex, $done_uri);
+      }
+    }
+
+    $handles = iterator_to_array($handles);
+    $handles = array_select_keys($handles, $dst_phids);
+
+    // TODO: These are hard-coded for now.
+    $filters = array(
+      'assigned' => pht('Assigned to Me'),
+      'created' => pht('Created By Me'),
+      'open' => pht('All Open Objects'),
+      'all' => pht('All Objects'),
+    );
+
+    $dialog_title = $relationship->getDialogTitleText();
+    $dialog_header = $relationship->getDialogHeaderText();
+    $dialog_button = $relationship->getDialogButtonText();
+    $dialog_instructions = $relationship->getDialogInstructionsText();
+
+    // TODO: Remove this, this is just legacy support.
+    $legacy_kinds = array(
+      ManiphestTaskHasCommitEdgeType::EDGECONST => 'CMIT',
+      ManiphestTaskHasMockEdgeType::EDGECONST => 'MOCK',
+      ManiphestTaskHasRevisionEdgeType::EDGECONST => 'DREV',
+    );
+
+    $edge_type = $relationship->getEdgeConstant();
+    $legacy_kind = idx($legacy_kinds, $edge_type);
+    if (!$legacy_kind) {
+      throw new Exception(
+        pht('Only specific legacy relationships are supported!'));
+    }
+
+    return id(new PhabricatorObjectSelectorDialog())
+      ->setUser($viewer)
+      ->setHandles($handles)
+      ->setFilters($filters)
+      ->setSelectedFilter('created')
+      ->setExcluded($phid)
+      ->setCancelURI($done_uri)
+      ->setSearchURI("/search/select/{$legacy_kind}/edge/")
+      ->setTitle($dialog_title)
+      ->setHeader($dialog_header)
+      ->setButtonText($dialog_button)
+      ->setInstructions($dialog_instructions)
+      ->buildDialog();
+  }
+
+  private function newGraphCycleResponse(
+    PhabricatorEdgeCycleException $ex,
+    $done_uri) {
+
+    $viewer = $this->getViewer();
+    $cycle = $ex->getCycle();
+
+    $handles = $this->loadViewerHandles($cycle);
+    $names = array();
+    foreach ($cycle as $cycle_phid) {
+      $names[] = $handles[$cycle_phid]->getFullName();
+    }
+
+    $message = pht(
+      'You can not create that relationship because it would create a '.
+      'circular dependency: %s.',
+      implode(" \xE2\x86\x92 ", $names));
+
+    return $this->newDialog()
+      ->setTitle(pht('Circular Dependency'))
+      ->appendParagraph($message)
+      ->addCancelButton($done_uri);
+  }
+
+  private function newUnrelatableObjectResponse(Exception $ex, $done_uri) {
+    $message = $ex->getMessage();
+
+    return $this->newDialog()
+      ->setTitle(pht('Invalid Relationship'))
+      ->appendParagraph($message)
+      ->addCancelButton($done_uri);
+  }
+
+}
diff --git a/src/applications/search/relationship/PhabricatorObjectRelationship.php b/src/applications/search/relationship/PhabricatorObjectRelationship.php
--- a/src/applications/search/relationship/PhabricatorObjectRelationship.php
+++ b/src/applications/search/relationship/PhabricatorObjectRelationship.php
@@ -24,6 +24,16 @@
   abstract protected function getActionName();
   abstract protected function getActionIcon();
 
+  abstract public function canRelateObjects($src, $dst);
+
+  abstract public function getDialogTitleText();
+  abstract public function getDialogHeaderText();
+  abstract public function getDialogButtonText();
+
+  public function getDialogInstructionsText() {
+    return null;
+  }
+
   public function shouldAppearInActionMenu() {
     return true;
   }
@@ -58,24 +68,8 @@
 
   private function getActionURI($object) {
     $phid = $object->getPHID();
-
-    // TODO: Remove this, this is just legacy support for the current
-    // controller until a new one gets built.
-    $legacy_kinds = array(
-      ManiphestTaskHasCommitEdgeType::EDGECONST => 'CMIT',
-      ManiphestTaskHasMockEdgeType::EDGECONST => 'MOCK',
-      ManiphestTaskHasRevisionEdgeType::EDGECONST => 'DREV',
-    );
-
-    $edge_type = $this->getEdgeConstant();
-    $legacy_kind = idx($legacy_kinds, $edge_type);
-    if (!$legacy_kind) {
-      throw new Exception(
-        pht(
-          'Only specific legacy relationships are supported!'));
-    }
-
-    return "/search/attach/{$phid}/{$legacy_kind}/";
+    $type = $this->getRelationshipConstant();
+    return "/search/rel/{$type}/{$phid}/";
   }
 
 }
diff --git a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php
--- a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php
+++ b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php
@@ -71,6 +71,10 @@
       ->setSubmenu($actions);
   }
 
+  public function getRelationship($key) {
+    return idx($this->relationships, $key);
+  }
+
   public static function newForObject(PhabricatorUser $viewer, $object) {
     $relationships = PhabricatorObjectRelationship::getAllRelationships();