diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -234,6 +234,9 @@ $editor->save(); } } + + // See also PhabricatorProject#destroyObjectPermanently() where we must + // restore the original situation after the last child is deleted. } // TODO: We should dump an informational transaction onto the parent diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -734,6 +734,16 @@ PhabricatorDestructionEngine $engine) { $this->openTransaction(); + + // My milestones cannot live without me. Drop them as well early. + $child_milestones = id(new self()) + ->loadAllWhere('milestoneNumber IS NOT NULL AND parentProjectPHID = %s', + $this->getPHID()); + foreach ($child_milestones as $child_milestone) { + $child_milestone->attachParentProject($this); + $engine->destroyObject($child_milestone); + } + $this->delete(); $columns = id(new PhabricatorProjectColumn()) @@ -748,9 +758,88 @@ $slug->delete(); } + // Update my parent, since it may be childless now. + $this->onDestroyTouchChildlessParent(); + + // Update my "fatherless children", since they have broken depth now. + $this->onDestroyTouchFatherlessChildren(); + $this->saveTransaction(); } + /** + * On destroy, eventually touch our childless parent. + * + * @see PhabricatorProjectTransactionEditor#applyFinalEffects() + */ + private function onDestroyTouchChildlessParent() { + // Do nothing special if I'm a simple root-project. + $parent = $this->getParentProject(); + if (!$parent) { + return; + } + + // Do nothing special if our parent has more children projects. + $sisters = id(new self())->loadAllWhere( + 'parentProjectPHID = %s AND '. + 'milestoneNumber IS NULL AND '. + 'phid != %s LIMIT 1', + $parent->getPHID(), + $this->getPHID()); + if ($sisters) { + return; + } + + // Get direct project memberships. + $member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; + $project_members = PhabricatorEdgeQuery::loadDestinationPHIDs( + $this->getPHID(), + $member_type); + + // We were the last subproject of our parent. + // So, copy all of the real members to the parent. + if ($project_members) { + $editor = new PhabricatorEdgeEditor(); + foreach ($project_members as $member_phid) { + $editor->addEdge($parent->getPHID(), $member_type, $member_phid); + } + $editor->save(); + + id(new PhabricatorProjectsMembershipIndexEngineExtension()) + ->rematerialize($parent); + } + + // Set our parent as childless. + $parent + ->setHasSubprojects(0) + ->save(); + } + + /** + * On destroy, eventually touch our childless parent. + * + * @see PhabricatorProjectTransactionEditor#applyFinalEffects() + */ + private function onDestroyTouchFatherlessChildren() { + // Get all children milestones and sub-projects. + // This query must use the Query builder to make save() happy. + $children = id(new PhabricatorProjectQuery()) + ->withParentProjectPHIDs(array($this->getPHID())) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->execute(); + + // Consolidate children debth, bubbling up them. + $parent_phid = $this->getParentProjectPHID(); + $parent = $this->getParentProject(); + foreach ($children as $child) { + $child->setParentProjectPHID($parent_phid); + $child->setParentProject($parent); + $child->attachParentProject($parent); + $child->save(); + $child->onDestroyTouchFatherlessChildren(); + } + } + /* -( PhabricatorFulltextInterface )--------------------------------------- */