Materialize parent project memberships

Summary:
Ref T10010. Subprojects have the following general membership rule: if you are a member of a subproject ("Engineering > Backend"), you are also a member of the parent project.

It would be unreasonably difficult to implement this rule directly in SQL when querying `withMemberPHIDs()`, because we'd have to do an arbitrarily large number of arbitrarily deep joins, or fetch and then requery a lot of data.

Instead, introduce "materailized members", which are just a copy of all the effective members of a project. When a subproject has a membership change, we go recompute the effective membership of all the parent projects. Then we can just JOIN to satisfy `withMemberPHIDs()`.

Having this process avialable will also be useful in the future, when a project's membership might be defined by some external source.

Also make milestones mostly work like we'd expect them to with respect to membership and visibility.

Test Plan:
  - Added and executed unit tests.
  - Changed project members, verified materialized members populated correctly in the database.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10010

Differential Revision: https://secure.phabricator.com/D14863
This commit is contained in:
epriestley
2015-12-23 03:12:20 -08:00
parent 70f6bf306f
commit 26ba4e8717
7 changed files with 268 additions and 9 deletions

View File

@@ -14,6 +14,7 @@ final class PhabricatorProjectQuery
private $ancestorPHIDs;
private $parentPHIDs;
private $isMilestone;
private $hasSubprojects;
private $minDepth;
private $maxDepth;
@@ -89,6 +90,15 @@ final class PhabricatorProjectQuery
return $this;
}
public function withHasSubprojects($has_subprojects) {
$this->hasSubprojects = $has_subprojects;
return $this;
}
public function getProperty() {
return $this->property;
}
public function withDepthBetween($min, $max) {
$this->minDepth = $min;
$this->maxDepth = $max;
@@ -156,12 +166,8 @@ final class PhabricatorProjectQuery
}
protected function willFilterPage(array $projects) {
$project_phids = array();
$ancestor_paths = array();
foreach ($projects as $project) {
$project_phids[] = $project->getPHID();
foreach ($project->getAncestorProjectPaths() as $path) {
$ancestor_paths[$path] = $path;
}
@@ -178,7 +184,6 @@ final class PhabricatorProjectQuery
$projects = $this->linkProjectGraph($projects, $ancestors);
$viewer_phid = $this->getViewer()->getPHID();
$project_phids = mpull($projects, 'getPHID');
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
@@ -189,8 +194,18 @@ final class PhabricatorProjectQuery
$types[] = $watcher_type;
}
$all_sources = array();
foreach ($projects as $project) {
if ($project->isMilestone()) {
$phid = $project->getParentProjectPHID();
} else {
$phid = $project->getPHID();
}
$all_sources[$phid] = $phid;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($project_phids)
->withSourcePHIDs($all_sources)
->withEdgeTypes($types);
// If we only need to know if the viewer is a member, we can restrict
@@ -205,8 +220,14 @@ final class PhabricatorProjectQuery
foreach ($projects as $project) {
$project_phid = $project->getPHID();
if ($project->isMilestone()) {
$source_phids = array($project->getParentProjectPHID());
} else {
$source_phids = array($project_phid);
}
$member_phids = $edge_query->getDestinationPHIDs(
array($project_phid),
$source_phids,
array($member_type));
if (in_array($viewer_phid, $member_phids)) {
@@ -219,7 +240,7 @@ final class PhabricatorProjectQuery
if ($this->needWatchers) {
$watcher_phids = $edge_query->getDestinationPHIDs(
array($project_phid),
$source_phids,
array($watcher_type));
$project->attachWatcherPHIDs($watcher_phids);
$project->setIsUserWatcher(
@@ -408,6 +429,13 @@ final class PhabricatorProjectQuery
}
}
if ($this->hasSubprojects !== null) {
$where[] = qsprintf(
$conn,
'hasSubprojects = %d',
(int)$this->hasSubprojects);
}
if ($this->minDepth !== null) {
$where[] = qsprintf(
$conn,