diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php
index 2fcb27657..2f90c7d35 100644
--- a/src/applications/project/controller/PhabricatorProjectController.php
+++ b/src/applications/project/controller/PhabricatorProjectController.php
@@ -1,157 +1,175 @@
 <?php
 
 abstract class PhabricatorProjectController extends PhabricatorController {
 
   private $project;
 
   protected function setProject(PhabricatorProject $project) {
     $this->project = $project;
     return $this;
   }
 
   protected function getProject() {
     return $this->project;
   }
 
   protected function loadProject() {
     $viewer = $this->getViewer();
     $request = $this->getRequest();
 
     $id = $request->getURIData('id');
     $slug = $request->getURIData('slug');
 
     if ($slug) {
       $normal_slug = PhabricatorSlug::normalizeProjectSlug($slug);
       $is_abnormal = ($slug !== $normal_slug);
       $normal_uri = "/tag/{$normal_slug}/";
     } else {
       $is_abnormal = false;
     }
 
     $query = id(new PhabricatorProjectQuery())
       ->setViewer($viewer)
       ->needMembers(true)
       ->needWatchers(true)
       ->needImages(true)
       ->needSlugs(true);
 
     if ($slug) {
       $query->withSlugs(array($slug));
     } else {
       $query->withIDs(array($id));
     }
 
     $policy_exception = null;
     try {
       $project = $query->executeOne();
     } catch (PhabricatorPolicyException $ex) {
       $policy_exception = $ex;
       $project = null;
     }
 
     if (!$project) {
       // This project legitimately does not exist, so just 404 the user.
       if (!$policy_exception) {
         return new Aphront404Response();
       }
 
       // Here, the project exists but the user can't see it. If they are
       // using a non-canonical slug to view the project, redirect to the
       // canonical slug. If they're already using the canonical slug, rethrow
       // the exception to give them the policy error.
       if ($is_abnormal) {
         return id(new AphrontRedirectResponse())->setURI($normal_uri);
       } else {
         throw $policy_exception;
       }
     }
 
     // The user can view the project, but is using a noncanonical slug.
     // Redirect to the canonical slug.
     $primary_slug = $project->getPrimarySlug();
     if ($slug && ($slug !== $primary_slug)) {
       $primary_uri = "/tag/{$primary_slug}/";
       return id(new AphrontRedirectResponse())->setURI($primary_uri);
     }
 
     $this->setProject($project);
 
     return null;
   }
 
   public function buildApplicationMenu() {
     return $this->buildSideNavView(true)->getMenu();
   }
 
   public function buildSideNavView($for_app = false) {
     $project = $this->getProject();
 
     $nav = new AphrontSideNavFilterView();
     $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
 
     $viewer = $this->getViewer();
 
     $id = null;
     if ($for_app) {
       if ($project) {
         $id = $project->getID();
         $nav->addFilter("profile/{$id}/", pht('Profile'));
         $nav->addFilter("board/{$id}/", pht('Workboard'));
         $nav->addFilter("members/{$id}/", pht('Members'));
         $nav->addFilter("feed/{$id}/", pht('Feed'));
         $nav->addFilter("details/{$id}/", pht('Edit Details'));
       }
       $nav->addFilter('create', pht('Create Project'));
     }
 
     if (!$id) {
       id(new PhabricatorProjectSearchEngine())
         ->setViewer($viewer)
         ->addNavigationItems($nav->getMenu());
     }
 
     $nav->selectFilter(null);
 
     return $nav;
   }
 
   public function buildIconNavView(PhabricatorProject $project) {
     $this->setProject($project);
     $viewer = $this->getViewer();
     $id = $project->getID();
     $picture = $project->getProfileImageURI();
     $name = $project->getName();
 
     $columns = id(new PhabricatorProjectColumnQuery())
       ->setViewer($viewer)
       ->withProjectPHIDs(array($project->getPHID()))
       ->execute();
     if ($columns) {
       $board_icon = 'fa-columns';
     } else {
       $board_icon = 'fa-columns grey';
     }
 
     $nav = new AphrontSideNavFilterView();
     $nav->setIconNav(true);
     $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
     $nav->addIcon("profile/{$id}/", $name, null, $picture);
 
     $class = 'PhabricatorManiphestApplication';
     if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
       $phid = $project->getPHID();
       $nav->addIcon("board/{$id}/", pht('Workboard'), $board_icon);
       $query_uri = urisprintf(
         '/maniphest/?statuses=open()&projects=%s#R',
         $phid);
       $nav->addIcon(null, pht('Open Tasks'), 'fa-anchor', null, $query_uri);
     }
 
     $nav->addIcon("feed/{$id}/", pht('Feed'), 'fa-newspaper-o');
     $nav->addIcon("members/{$id}/", pht('Members'), 'fa-group');
     $nav->addIcon("details/{$id}/", pht('Edit Details'), 'fa-pencil');
 
     return $nav;
   }
 
+  protected function buildApplicationCrumbs() {
+    $crumbs = parent::buildApplicationCrumbs();
+
+    $project = $this->getProject();
+    if ($project) {
+      $ancestors = $project->getAncestorProjects();
+      $ancestors = array_reverse($ancestors);
+      $ancestors[] = $project;
+      foreach ($ancestors as $ancestor) {
+        $crumbs->addTextCrumb(
+          $project->getName(),
+          $project->getURI());
+      }
+    }
+
+    return $crumbs;
+  }
+
 }
diff --git a/src/applications/project/controller/PhabricatorProjectFeedController.php b/src/applications/project/controller/PhabricatorProjectFeedController.php
index 5cd74094f..571148d2c 100644
--- a/src/applications/project/controller/PhabricatorProjectFeedController.php
+++ b/src/applications/project/controller/PhabricatorProjectFeedController.php
@@ -1,60 +1,63 @@
 <?php
 
 final class PhabricatorProjectFeedController
   extends PhabricatorProjectController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getUser();
 
     $response = $this->loadProject();
     if ($response) {
       return $response;
     }
 
     $project = $this->getProject();
     $id = $project->getID();
 
     $stories = id(new PhabricatorFeedQuery())
       ->setViewer($viewer)
       ->setFilterPHIDs(
         array(
           $project->getPHID(),
         ))
       ->setLimit(50)
       ->execute();
 
     $feed = $this->renderStories($stories);
 
     $box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Project Activity'))
       ->appendChild($feed);
 
     $nav = $this->buildIconNavView($project);
     $nav->selectFilter("feed/{$id}/");
     $nav->appendChild($box);
 
-    return $this->buildApplicationPage(
-      $nav,
-      array(
-        'title' => $project->getName(),
-      ));
+    $crumbs = $this->buildApplicationCrumbs();
+    $crumbs->addTextCrumb(pht('Feed'));
+
+    return $this->newPage()
+      ->setNavigation($nav)
+      ->setCrumbs($crumbs)
+      ->setTitle(array($project->getName(), pht('Feed')))
+      ->appendChild($box);
   }
 
   private function renderStories(array $stories) {
     assert_instances_of($stories, 'PhabricatorFeedStory');
 
     $builder = new PhabricatorFeedBuilder($stories);
     $builder->setUser($this->getRequest()->getUser());
     $builder->setShowHovercards(true);
     $view = $builder->buildView();
 
     return phutil_tag_div(
       'profile-feed',
       $view->render());
   }
 
 }
diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php
index f007650e4..b018e9389 100644
--- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php
+++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php
@@ -1,150 +1,152 @@
 <?php
 
 final class PhabricatorProjectMembersEditController
   extends PhabricatorProjectController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
     $id = $request->getURIData('id');
 
     $project = id(new PhabricatorProjectQuery())
       ->setViewer($viewer)
       ->withIDs(array($id))
       ->needMembers(true)
       ->needImages(true)
       ->executeOne();
     if (!$project) {
       return new Aphront404Response();
     }
 
     $member_phids = $project->getMemberPHIDs();
 
     if ($request->isFormPost()) {
       $member_spec = array();
 
       $remove = $request->getStr('remove');
       if ($remove) {
         $member_spec['-'] = array_fuse(array($remove));
       }
 
       $add_members = $request->getArr('phids');
       if ($add_members) {
         $member_spec['+'] = array_fuse($add_members);
       }
 
       $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
 
       $xactions = array();
 
       $xactions[] = id(new PhabricatorProjectTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $type_member)
         ->setNewValue($member_spec);
 
       $editor = id(new PhabricatorProjectTransactionEditor($project))
         ->setActor($viewer)
         ->setContentSourceFromRequest($request)
         ->setContinueOnNoEffect(true)
         ->setContinueOnMissingFields(true)
         ->applyTransactions($project, $xactions);
 
       return id(new AphrontRedirectResponse())
         ->setURI($request->getRequestURI());
     }
 
     $member_phids = array_reverse($member_phids);
     $handles = $this->loadViewerHandles($member_phids);
 
     $state = array();
     foreach ($handles as $handle) {
       $state[] = array(
         'phid' => $handle->getPHID(),
         'name' => $handle->getFullName(),
       );
     }
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $project,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $form_box = null;
     $title = pht('Add Members');
     if ($can_edit) {
       $header_name = pht('Edit Members');
       $view_uri = $this->getApplicationURI('profile/'.$project->getID().'/');
 
       $form = new AphrontFormView();
       $form
         ->setUser($viewer)
         ->appendControl(
           id(new AphrontFormTokenizerControl())
             ->setName('phids')
             ->setLabel(pht('Add Members'))
             ->setDatasource(new PhabricatorPeopleDatasource()))
         ->appendChild(
           id(new AphrontFormSubmitControl())
             ->addCancelButton($view_uri)
             ->setValue(pht('Add Members')));
       $form_box = id(new PHUIObjectBoxView())
         ->setHeaderText($title)
         ->setForm($form);
     }
 
     $member_list = $this->renderMemberList($project, $handles);
 
     $nav = $this->buildIconNavView($project);
     $nav->selectFilter("members/{$id}/");
-    $nav->appendChild($form_box);
-    $nav->appendChild($member_list);
-
-    return $this->buildApplicationPage(
-      $nav,
-      array(
-        'title' => $title,
-      ));
+
+    $crumbs = $this->buildApplicationCrumbs();
+    $crumbs->addTextCrumb(pht('Members'));
+
+    return $this->newPage()
+      ->setNavigation($nav)
+      ->setCrumbs($crumbs)
+      ->setTitle(array($project->getName(), $title))
+      ->appendChild($form_box)
+      ->appendChild($member_list);
   }
 
   private function renderMemberList(
     PhabricatorProject $project,
     array $handles) {
 
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $project,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $list = id(new PHUIObjectItemListView())
       ->setNoDataString(pht('This project does not have any members.'));
 
     foreach ($handles as $handle) {
       $remove_uri = $this->getApplicationURI(
         '/members/'.$project->getID().'/remove/?phid='.$handle->getPHID());
 
       $item = id(new PHUIObjectItemView())
         ->setHeader($handle->getFullName())
         ->setHref($handle->getURI())
         ->setImageURI($handle->getImageURI());
 
       if ($can_edit) {
         $item->addAction(
           id(new PHUIListItemView())
             ->setIcon('fa-times')
             ->setName(pht('Remove'))
             ->setHref($remove_uri)
             ->setWorkflow(true));
       }
 
       $list->addItem($item);
     }
 
     $box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Members'))
       ->setObjectList($list);
 
     return $box;
   }
 }
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index cc246b5a2..fa443ebf6 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,206 +1,209 @@
 <?php
 
 final class PhabricatorProjectProfileController
   extends PhabricatorProjectController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getUser();
 
     $response = $this->loadProject();
     if ($response) {
       return $response;
     }
 
     $project = $this->getProject();
     $id = $project->getID();
     $picture = $project->getProfileImageURI();
 
     $header = id(new PHUIHeaderView())
       ->setHeader($project->getName())
       ->setUser($viewer)
       ->setPolicyObject($project)
       ->setImage($picture);
 
     if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) {
       $header->setStatus('fa-check', 'bluegrey', pht('Active'));
     } else {
       $header->setStatus('fa-ban', 'red', pht('Archived'));
     }
 
     $actions = $this->buildActionListView($project);
     $properties = $this->buildPropertyListView($project, $actions);
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->addPropertyList($properties);
 
     $timeline = $this->buildTransactionTimeline(
       $project,
       new PhabricatorProjectTransactionQuery());
     $timeline->setShouldTerminate(true);
 
     $nav = $this->buildIconNavView($project);
     $nav->selectFilter("profile/{$id}/");
 
+    $crumbs = $this->buildApplicationCrumbs();
+
     return $this->newPage()
       ->setNavigation($nav)
+      ->setCrumbs($crumbs)
       ->setTitle($project->getName())
       ->setPageObjectPHIDs(array($project->getPHID()))
       ->appendChild($object_box)
       ->appendChild($timeline);
   }
 
   private function buildActionListView(PhabricatorProject $project) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $id = $project->getID();
 
     $view = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObject($project);
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $project,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Details'))
         ->setIcon('fa-pencil')
         ->setHref($this->getApplicationURI("details/{$id}/"))
         ->setDisabled(!$can_edit)
         ->setWorkflow(!$can_edit));
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Picture'))
         ->setIcon('fa-picture-o')
         ->setHref($this->getApplicationURI("picture/{$id}/"))
         ->setDisabled(!$can_edit)
         ->setWorkflow(!$can_edit));
 
     if ($project->isArchived()) {
       $view->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Activate Project'))
           ->setIcon('fa-check')
           ->setHref($this->getApplicationURI("archive/{$id}/"))
           ->setDisabled(!$can_edit)
           ->setWorkflow(true));
     } else {
       $view->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Archive Project'))
           ->setIcon('fa-ban')
           ->setHref($this->getApplicationURI("archive/{$id}/"))
           ->setDisabled(!$can_edit)
           ->setWorkflow(true));
     }
 
     $action = null;
     if (!$project->isUserMember($viewer->getPHID())) {
       $can_join = PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $project,
         PhabricatorPolicyCapability::CAN_JOIN);
 
       $action = id(new PhabricatorActionView())
         ->setUser($viewer)
         ->setRenderAsForm(true)
         ->setHref('/project/update/'.$project->getID().'/join/')
         ->setIcon('fa-plus')
         ->setDisabled(!$can_join)
         ->setName(pht('Join Project'));
       $view->addAction($action);
     } else {
       $action = id(new PhabricatorActionView())
         ->setWorkflow(true)
         ->setHref('/project/update/'.$project->getID().'/leave/')
         ->setIcon('fa-times')
         ->setName(pht('Leave Project...'));
       $view->addAction($action);
 
       if (!$project->isUserWatcher($viewer->getPHID())) {
         $action = id(new PhabricatorActionView())
           ->setWorkflow(true)
           ->setHref('/project/watch/'.$project->getID().'/')
           ->setIcon('fa-eye')
           ->setName(pht('Watch Project'));
         $view->addAction($action);
       } else {
         $action = id(new PhabricatorActionView())
           ->setWorkflow(true)
           ->setHref('/project/unwatch/'.$project->getID().'/')
           ->setIcon('fa-eye-slash')
           ->setName(pht('Unwatch Project'));
         $view->addAction($action);
       }
     }
 
     return $view;
   }
 
   private function buildPropertyListView(
     PhabricatorProject $project,
     PhabricatorActionListView $actions) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $view = id(new PHUIPropertyListView())
       ->setUser($viewer)
       ->setObject($project)
       ->setActionList($actions);
 
     $hashtags = array();
     foreach ($project->getSlugs() as $slug) {
       $hashtags[] = id(new PHUITagView())
         ->setType(PHUITagView::TYPE_OBJECT)
         ->setName('#'.$slug->getSlug());
     }
 
     $view->addProperty(pht('Hashtags'), phutil_implode_html(' ', $hashtags));
 
     $view->addProperty(
       pht('Members'),
       $project->getMemberPHIDs()
         ? $viewer
           ->renderHandleList($project->getMemberPHIDs())
           ->setAsInline(true)
         : phutil_tag('em', array(), pht('None')));
 
     $view->addProperty(
       pht('Watchers'),
       $project->getWatcherPHIDs()
         ? $viewer
           ->renderHandleList($project->getWatcherPHIDs())
           ->setAsInline(true)
         : phutil_tag('em', array(), pht('None')));
 
     $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
       $viewer,
       $project);
 
     $view->addProperty(
       pht('Looks Like'),
       $viewer->renderHandle($project->getPHID())->setAsTag(true));
 
     $view->addProperty(
       pht('Joinable By'),
       $descriptions[PhabricatorPolicyCapability::CAN_JOIN]);
 
     $field_list = PhabricatorCustomField::getObjectFields(
       $project,
       PhabricatorCustomField::ROLE_VIEW);
     $field_list->appendFieldsToPropertyList($project, $viewer, $view);
 
     return $view;
   }
 
 
 }
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
index afb4a32e6..6a4ed4afb 100644
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -1,557 +1,562 @@
 <?php
 
 final class PhabricatorProject extends PhabricatorProjectDAO
   implements
     PhabricatorApplicationTransactionInterface,
     PhabricatorFlaggableInterface,
     PhabricatorPolicyInterface,
     PhabricatorExtendedPolicyInterface,
     PhabricatorSubscribableInterface,
     PhabricatorCustomFieldInterface,
     PhabricatorDestructibleInterface,
     PhabricatorFulltextInterface,
     PhabricatorConduitResultInterface {
 
   protected $name;
   protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
   protected $authorPHID;
   protected $primarySlug;
   protected $profileImagePHID;
   protected $icon;
   protected $color;
   protected $mailKey;
 
   protected $viewPolicy;
   protected $editPolicy;
   protected $joinPolicy;
   protected $isMembershipLocked;
 
   protected $parentProjectPHID;
   protected $hasWorkboard;
   protected $hasMilestones;
   protected $hasSubprojects;
   protected $milestoneNumber;
 
   protected $projectPath;
   protected $projectDepth;
   protected $projectPathKey;
 
   private $memberPHIDs = self::ATTACHABLE;
   private $watcherPHIDs = self::ATTACHABLE;
   private $sparseWatchers = self::ATTACHABLE;
   private $sparseMembers = self::ATTACHABLE;
   private $customFields = self::ATTACHABLE;
   private $profileImageFile = self::ATTACHABLE;
   private $slugs = self::ATTACHABLE;
   private $parentProject = self::ATTACHABLE;
 
   const DEFAULT_ICON = 'fa-briefcase';
   const DEFAULT_COLOR = 'blue';
 
   const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
 
   public static function initializeNewProject(PhabricatorUser $actor) {
     $app = id(new PhabricatorApplicationQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withClasses(array('PhabricatorProjectApplication'))
       ->executeOne();
 
     $view_policy = $app->getPolicy(
       ProjectDefaultViewCapability::CAPABILITY);
     $edit_policy = $app->getPolicy(
       ProjectDefaultEditCapability::CAPABILITY);
     $join_policy = $app->getPolicy(
       ProjectDefaultJoinCapability::CAPABILITY);
 
     return id(new PhabricatorProject())
       ->setAuthorPHID($actor->getPHID())
       ->setIcon(self::DEFAULT_ICON)
       ->setColor(self::DEFAULT_COLOR)
       ->setViewPolicy($view_policy)
       ->setEditPolicy($edit_policy)
       ->setJoinPolicy($join_policy)
       ->setIsMembershipLocked(0)
       ->attachMemberPHIDs(array())
       ->attachSlugs(array())
       ->setHasWorkboard(0)
       ->setHasMilestones(0)
       ->setHasSubprojects(0)
       ->attachParentProject(null);
   }
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
       PhabricatorPolicyCapability::CAN_JOIN,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
       case PhabricatorPolicyCapability::CAN_JOIN:
         return $this->getJoinPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     $can_edit = PhabricatorPolicyCapability::CAN_EDIT;
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         if ($this->isUserMember($viewer->getPHID())) {
           // Project members can always view a project.
           return true;
         }
         break;
       case PhabricatorPolicyCapability::CAN_EDIT:
         $parent = $this->getParentProject();
         if ($parent) {
           $can_edit_parent = PhabricatorPolicyFilter::hasCapability(
             $viewer,
             $parent,
             $can_edit);
           if ($can_edit_parent) {
             return true;
           }
         }
         break;
       case PhabricatorPolicyCapability::CAN_JOIN:
         if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
           // Project editors can always join a project.
           return true;
         }
         break;
     }
 
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
 
     // TODO: Clarify the additional rules that parent and subprojects imply.
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return pht('Members of a project can always view it.');
       case PhabricatorPolicyCapability::CAN_JOIN:
         return pht('Users who can edit a project can always join it.');
     }
     return null;
   }
 
   public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
     $extended = array();
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         $parent = $this->getParentProject();
         if ($parent) {
           $extended[] = array(
             $parent,
             PhabricatorPolicyCapability::CAN_VIEW,
           );
         }
         break;
     }
 
     return $extended;
   }
 
 
   public function isUserMember($user_phid) {
     if ($this->memberPHIDs !== self::ATTACHABLE) {
       return in_array($user_phid, $this->memberPHIDs);
     }
     return $this->assertAttachedKey($this->sparseMembers, $user_phid);
   }
 
   public function setIsUserMember($user_phid, $is_member) {
     if ($this->sparseMembers === self::ATTACHABLE) {
       $this->sparseMembers = array();
     }
     $this->sparseMembers[$user_phid] = $is_member;
     return $this;
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'sort128',
         'status' => 'text32',
         'primarySlug' => 'text128?',
         'isMembershipLocked' => 'bool',
         'profileImagePHID' => 'phid?',
         'icon' => 'text32',
         'color' => 'text32',
         'mailKey' => 'bytes20',
         'joinPolicy' => 'policy',
         'parentProjectPHID' => 'phid?',
         'hasWorkboard' => 'bool',
         'hasMilestones' => 'bool',
         'hasSubprojects' => 'bool',
         'milestoneNumber' => 'uint32?',
         'projectPath' => 'hashpath64',
         'projectDepth' => 'uint32',
         'projectPathKey' => 'bytes4',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'key_icon' => array(
           'columns' => array('icon'),
         ),
         'key_color' => array(
           'columns' => array('color'),
         ),
         'name' => array(
           'columns' => array('name'),
           'unique' => true,
         ),
         'key_milestone' => array(
           'columns' => array('parentProjectPHID', 'milestoneNumber'),
           'unique' => true,
         ),
         'key_primaryslug' => array(
           'columns' => array('primarySlug'),
           'unique' => true,
         ),
         'key_path' => array(
           'columns' => array('projectPath', 'projectDepth'),
         ),
         'key_pathkey' => array(
           'columns' => array('projectPathKey'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorProjectProjectPHIDType::TYPECONST);
   }
 
   public function attachMemberPHIDs(array $phids) {
     $this->memberPHIDs = $phids;
     return $this;
   }
 
   public function getMemberPHIDs() {
     return $this->assertAttached($this->memberPHIDs);
   }
 
   public function isArchived() {
     return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
   }
 
   public function getProfileImageURI() {
     return $this->getProfileImageFile()->getBestURI();
   }
 
   public function attachProfileImageFile(PhabricatorFile $file) {
     $this->profileImageFile = $file;
     return $this;
   }
 
   public function getProfileImageFile() {
     return $this->assertAttached($this->profileImageFile);
   }
 
 
   public function isUserWatcher($user_phid) {
     if ($this->watcherPHIDs !== self::ATTACHABLE) {
       return in_array($user_phid, $this->watcherPHIDs);
     }
     return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
   }
 
   public function setIsUserWatcher($user_phid, $is_watcher) {
     if ($this->sparseWatchers === self::ATTACHABLE) {
       $this->sparseWatchers = array();
     }
     $this->sparseWatchers[$user_phid] = $is_watcher;
     return $this;
   }
 
   public function attachWatcherPHIDs(array $phids) {
     $this->watcherPHIDs = $phids;
     return $this;
   }
 
   public function getWatcherPHIDs() {
     return $this->assertAttached($this->watcherPHIDs);
   }
 
   public function attachSlugs(array $slugs) {
     $this->slugs = $slugs;
     return $this;
   }
 
   public function getSlugs() {
     return $this->assertAttached($this->slugs);
   }
 
   public function getColor() {
     if ($this->isArchived()) {
       return PHUITagView::COLOR_DISABLED;
     }
 
     return $this->color;
   }
 
+  public function getURI() {
+    $id = $this->getID();
+    return "/project/view/{$id}/";
+  }
+
   public function save() {
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
 
     if (!strlen($this->getPHID())) {
       $this->setPHID($this->generatePHID());
     }
 
     if (!strlen($this->getProjectPathKey())) {
       $hash = PhabricatorHash::digestForIndex($this->getPHID());
       $hash = substr($hash, 0, 4);
       $this->setProjectPathKey($hash);
     }
 
     $path = array();
     $depth = 0;
     if ($this->parentProjectPHID) {
       $parent = $this->getParentProject();
       $path[] = $parent->getProjectPath();
       $depth = $parent->getProjectDepth() + 1;
     }
     $path[] = $this->getProjectPathKey();
     $path = implode('', $path);
 
     $limit = self::getProjectDepthLimit();
     if ($depth >= $limit) {
       throw new Exception(pht('Project depth is too great.'));
     }
 
     $this->setProjectPath($path);
     $this->setProjectDepth($depth);
 
     $this->openTransaction();
       $result = parent::save();
       $this->updateDatasourceTokens();
     $this->saveTransaction();
 
     return $result;
   }
 
   public static function getProjectDepthLimit() {
     // This is limited by how many path hashes we can fit in the path
     // column.
     return 16;
   }
 
   public function updateDatasourceTokens() {
     $table = self::TABLE_DATASOURCE_TOKEN;
     $conn_w = $this->establishConnection('w');
     $id = $this->getID();
 
     $slugs = queryfx_all(
       $conn_w,
       'SELECT * FROM %T WHERE projectPHID = %s',
       id(new PhabricatorProjectSlug())->getTableName(),
       $this->getPHID());
 
     $all_strings = ipull($slugs, 'slug');
     $all_strings[] = $this->getName();
     $all_strings = implode(' ', $all_strings);
 
     $tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
 
     $sql = array();
     foreach ($tokens as $token) {
       $sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
     }
 
     $this->openTransaction();
       queryfx(
         $conn_w,
         'DELETE FROM %T WHERE projectID = %d',
         $table,
         $id);
 
       foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
         queryfx(
           $conn_w,
           'INSERT INTO %T (projectID, token) VALUES %Q',
           $table,
           $chunk);
       }
     $this->saveTransaction();
   }
 
   public function isMilestone() {
     return ($this->getMilestoneNumber() !== null);
   }
 
   public function getParentProject() {
     return $this->assertAttached($this->parentProject);
   }
 
   public function attachParentProject(PhabricatorProject $project = null) {
     $this->parentProject = $project;
     return $this;
   }
 
   public function getAncestorProjectPaths() {
     $parts = array();
 
     $path = $this->getProjectPath();
     $parent_length = (strlen($path) - 4);
 
     for ($ii = $parent_length; $ii > 0; $ii -= 4) {
       $parts[] = substr($path, 0, $ii);
     }
 
     return $parts;
   }
 
   public function getAncestorProjects() {
     $ancestors = array();
 
     $cursor = $this->getParentProject();
     while ($cursor) {
       $ancestors[] = $cursor;
       $cursor = $cursor->getParentProject();
     }
 
     return $ancestors;
   }
 
 
 /* -(  PhabricatorSubscribableInterface  )----------------------------------- */
 
 
   public function isAutomaticallySubscribed($phid) {
     return false;
   }
 
   public function shouldShowSubscribersProperty() {
     return false;
   }
 
   public function shouldAllowSubscription($phid) {
     return $this->isUserMember($phid) &&
            !$this->isUserWatcher($phid);
   }
 
 
 /* -(  PhabricatorCustomFieldInterface  )------------------------------------ */
 
 
   public function getCustomFieldSpecificationForRole($role) {
     return PhabricatorEnv::getEnvConfig('projects.fields');
   }
 
   public function getCustomFieldBaseClass() {
     return 'PhabricatorProjectCustomField';
   }
 
   public function getCustomFields() {
     return $this->assertAttached($this->customFields);
   }
 
   public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
     $this->customFields = $fields;
     return $this;
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhabricatorProjectTransactionEditor();
   }
 
   public function getApplicationTransactionObject() {
     return $this;
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhabricatorProjectTransaction();
   }
 
   public function willRenderTimeline(
     PhabricatorApplicationTransactionView $timeline,
     AphrontRequest $request) {
 
     return $timeline;
   }
 
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
       $this->delete();
 
       $columns = id(new PhabricatorProjectColumn())
         ->loadAllWhere('projectPHID = %s', $this->getPHID());
       foreach ($columns as $column) {
         $engine->destroyObject($column);
       }
 
       $slugs = id(new PhabricatorProjectSlug())
         ->loadAllWhere('projectPHID = %s', $this->getPHID());
       foreach ($slugs as $slug) {
         $slug->delete();
       }
 
     $this->saveTransaction();
   }
 
 
 /* -(  PhabricatorFulltextInterface  )--------------------------------------- */
 
 
   public function newFulltextEngine() {
     return new PhabricatorProjectFulltextEngine();
   }
 
 
 /* -(  PhabricatorConduitResultInterface  )---------------------------------- */
 
 
   public function getFieldSpecificationsForConduit() {
     return array(
       id(new PhabricatorConduitSearchFieldSpecification())
         ->setKey('name')
         ->setType('string')
         ->setDescription(pht('The name of the project.')),
       id(new PhabricatorConduitSearchFieldSpecification())
         ->setKey('slug')
         ->setType('string')
         ->setDescription(pht('Primary slug/hashtag.')),
     );
   }
 
   public function getFieldValuesForConduit() {
     return array(
       'name' => $this->getName(),
       'slug' => $this->getPrimarySlug(),
     );
   }
 
   public function getConduitSearchAttachments() {
     return array();
   }
 
 }