@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Make queries for Project "X" mean "X, or any subproject of X"

Summary:
Ref T10010. I think this is the desired/expected default behavior (e.g., searching for "Maniphest" should find tasks in any subproject or sprint of that project).

I'll probably add an "exact(...)" function later to mean "only the Maniphest superproject, exactly, not any of its children".

Test Plan:
- Added and executed unit tests.
- Ran various queries from the web UI.
- Got sensible-seeming results.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10010

Differential Revision: https://secure.phabricator.com/D14910

+327 -44
+2 -2
src/__phutil_library_map__.php
··· 2857 2857 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 2858 2858 'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php', 2859 2859 'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php', 2860 - 'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php', 2860 + 'PhabricatorProjectLogicalAncestorDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAncestorDatasource.php', 2861 2861 'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php', 2862 2862 'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php', 2863 2863 'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php', ··· 7209 7209 'PhabricatorProjectListController' => 'PhabricatorProjectController', 7210 7210 'PhabricatorProjectListView' => 'AphrontView', 7211 7211 'PhabricatorProjectLockController' => 'PhabricatorProjectController', 7212 - 'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 7212 + 'PhabricatorProjectLogicalAncestorDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 7213 7213 'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 7214 7214 'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 7215 7215 'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
+151
src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
··· 711 711 pht('Leave allowed without any permission.')); 712 712 } 713 713 714 + 715 + public function testComplexConstraints() { 716 + $user = $this->createUser(); 717 + $user->save(); 718 + 719 + $engineering = $this->createProject($user); 720 + $engineering_scan = $this->createProject($user, $engineering); 721 + $engineering_warp = $this->createProject($user, $engineering); 722 + 723 + $exploration = $this->createProject($user); 724 + $exploration_diplomacy = $this->createProject($user, $exploration); 725 + 726 + $task_engineering = $this->newTask( 727 + $user, 728 + array($engineering), 729 + pht('Engineering Only')); 730 + 731 + $task_exploration = $this->newTask( 732 + $user, 733 + array($exploration), 734 + pht('Exploration Only')); 735 + 736 + $task_warp_explore = $this->newTask( 737 + $user, 738 + array($engineering_warp, $exploration), 739 + pht('Warp to New Planet')); 740 + 741 + $task_diplomacy_scan = $this->newTask( 742 + $user, 743 + array($engineering_scan, $exploration_diplomacy), 744 + pht('Scan Diplomat')); 745 + 746 + $task_diplomacy = $this->newTask( 747 + $user, 748 + array($exploration_diplomacy), 749 + pht('Diplomatic Meeting')); 750 + 751 + $task_warp_scan = $this->newTask( 752 + $user, 753 + array($engineering_scan, $engineering_warp), 754 + pht('Scan Warp Drives')); 755 + 756 + $this->assertQueryByProjects( 757 + $user, 758 + array( 759 + $task_engineering, 760 + $task_warp_explore, 761 + $task_diplomacy_scan, 762 + $task_warp_scan, 763 + ), 764 + array($engineering), 765 + pht('All Engineering')); 766 + 767 + $this->assertQueryByProjects( 768 + $user, 769 + array( 770 + $task_diplomacy_scan, 771 + $task_warp_scan, 772 + ), 773 + array($engineering_scan), 774 + pht('All Scan')); 775 + 776 + $this->assertQueryByProjects( 777 + $user, 778 + array( 779 + $task_warp_explore, 780 + $task_diplomacy_scan, 781 + ), 782 + array($engineering, $exploration), 783 + pht('Engineering + Exploration')); 784 + 785 + // This is testing that a query for "Parent" and "Parent > Child" works 786 + // properly. 787 + $this->assertQueryByProjects( 788 + $user, 789 + array( 790 + $task_diplomacy_scan, 791 + $task_warp_scan, 792 + ), 793 + array($engineering, $engineering_scan), 794 + pht('Engineering + Scan')); 795 + } 796 + 797 + private function newTask( 798 + PhabricatorUser $viewer, 799 + array $projects, 800 + $name = null) { 801 + 802 + $task = ManiphestTask::initializeNewTask($viewer); 803 + 804 + if (!strlen($name)) { 805 + $name = pht('Test Task'); 806 + } 807 + 808 + $xactions = array(); 809 + 810 + $xactions[] = id(new ManiphestTransaction()) 811 + ->setTransactionType(ManiphestTransaction::TYPE_TITLE) 812 + ->setNewValue($name); 813 + 814 + if ($projects) { 815 + $xactions[] = id(new ManiphestTransaction()) 816 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 817 + ->setMetadataValue( 818 + 'edge:type', 819 + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) 820 + ->setNewValue( 821 + array( 822 + '=' => array_fuse(mpull($projects, 'getPHID')), 823 + )); 824 + } 825 + 826 + $editor = id(new ManiphestTransactionEditor()) 827 + ->setActor($viewer) 828 + ->setContentSource(PhabricatorContentSource::newConsoleSource()) 829 + ->setContinueOnNoEffect(true) 830 + ->applyTransactions($task, $xactions); 831 + 832 + return $task; 833 + } 834 + 835 + private function assertQueryByProjects( 836 + PhabricatorUser $viewer, 837 + array $expect, 838 + array $projects, 839 + $label = null) { 840 + 841 + $datasource = id(new PhabricatorProjectLogicalDatasource()) 842 + ->setViewer($viewer); 843 + 844 + $project_phids = mpull($projects, 'getPHID'); 845 + $constraints = $datasource->evaluateTokens($project_phids); 846 + 847 + $query = id(new ManiphestTaskQuery()) 848 + ->setViewer($viewer); 849 + 850 + $query->withEdgeLogicConstraints( 851 + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 852 + $constraints); 853 + 854 + $tasks = $query->execute(); 855 + 856 + $expect_phids = mpull($expect, 'getTitle', 'getPHID'); 857 + ksort($expect_phids); 858 + 859 + $actual_phids = mpull($tasks, 'getTitle', 'getPHID'); 860 + ksort($actual_phids); 861 + 862 + $this->assertEqual($expect_phids, $actual_phids, $label); 863 + } 864 + 714 865 private function refreshProject( 715 866 PhabricatorProject $project, 716 867 PhabricatorUser $viewer,
+95
src/applications/project/typeahead/PhabricatorProjectLogicalAncestorDatasource.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectLogicalAncestorDatasource 4 + extends PhabricatorTypeaheadCompositeDatasource { 5 + 6 + public function getBrowseTitle() { 7 + return pht('Browse Projects'); 8 + } 9 + 10 + public function getPlaceholderText() { 11 + return pht('Type a project name...'); 12 + } 13 + 14 + public function getDatasourceApplicationClass() { 15 + return 'PhabricatorProjectApplication'; 16 + } 17 + 18 + public function getComponentDatasources() { 19 + return array( 20 + new PhabricatorProjectDatasource(), 21 + ); 22 + } 23 + 24 + protected function didEvaluateTokens(array $results) { 25 + $phids = array(); 26 + 27 + foreach ($results as $result) { 28 + if (!is_string($result)) { 29 + continue; 30 + } 31 + $phids[] = $result; 32 + } 33 + 34 + $map = array(); 35 + $skip = array(); 36 + if ($phids) { 37 + $phids = array_fuse($phids); 38 + $viewer = $this->getViewer(); 39 + 40 + $all_projects = id(new PhabricatorProjectQuery()) 41 + ->setViewer($viewer) 42 + ->withAncestorProjectPHIDs($phids) 43 + ->execute(); 44 + 45 + foreach ($phids as $phid) { 46 + $map[$phid][] = $phid; 47 + } 48 + 49 + foreach ($all_projects as $project) { 50 + $project_phid = $project->getPHID(); 51 + $map[$project_phid][] = $project_phid; 52 + foreach ($project->getAncestorProjects() as $ancestor) { 53 + $ancestor_phid = $ancestor->getPHID(); 54 + 55 + if (isset($phids[$project_phid]) && isset($phids[$ancestor_phid])) { 56 + // This is a descendant of some other project in the query, so 57 + // we don't need to query for that project. This happens if a user 58 + // runs a query for both "Engineering" and "Engineering > Warp 59 + // Drive". We can only ever match the "Warp Drive" results, so 60 + // we do not need to add the weaker "Engineering" constraint. 61 + $skip[$ancestor_phid] = true; 62 + } 63 + 64 + $map[$ancestor_phid][] = $project_phid; 65 + } 66 + } 67 + } 68 + 69 + foreach ($results as $key => $result) { 70 + if (!is_string($result)) { 71 + continue; 72 + } 73 + 74 + if (empty($map[$result])) { 75 + continue; 76 + } 77 + 78 + // This constraint is implied by another, stronger constraint. 79 + if (isset($skip[$result])) { 80 + unset($results[$key]); 81 + continue; 82 + } 83 + 84 + // If we have duplicates, don't apply the second constraint. 85 + $skip[$result] = true; 86 + 87 + $results[$key] = new PhabricatorQueryConstraint( 88 + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, 89 + $map[$result]); 90 + } 91 + 92 + return $results; 93 + } 94 + 95 + }
-36
src/applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php
··· 1 - <?php 2 - 3 - final class PhabricatorProjectLogicalAndDatasource 4 - extends PhabricatorTypeaheadCompositeDatasource { 5 - 6 - public function getBrowseTitle() { 7 - return pht('Browse Projects'); 8 - } 9 - 10 - public function getPlaceholderText() { 11 - return pht('Type a project name...'); 12 - } 13 - 14 - public function getDatasourceApplicationClass() { 15 - return 'PhabricatorProjectApplication'; 16 - } 17 - 18 - public function getComponentDatasources() { 19 - return array( 20 - new PhabricatorProjectDatasource(), 21 - ); 22 - } 23 - 24 - protected function didEvaluateTokens(array $results) { 25 - foreach ($results as $key => $result) { 26 - if (is_string($result)) { 27 - $results[$key] = new PhabricatorQueryConstraint( 28 - PhabricatorQueryConstraint::OPERATOR_AND, 29 - $result); 30 - } 31 - } 32 - 33 - return $results; 34 - } 35 - 36 - }
+1 -1
src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php
··· 18 18 public function getComponentDatasources() { 19 19 return array( 20 20 new PhabricatorProjectNoProjectsDatasource(), 21 - new PhabricatorProjectLogicalAndDatasource(), 21 + new PhabricatorProjectLogicalAncestorDatasource(), 22 22 new PhabricatorProjectLogicalOrNotDatasource(), 23 23 new PhabricatorProjectLogicalViewerDatasource(), 24 24 new PhabricatorProjectLogicalUserDatasource(),
+1
src/infrastructure/query/constraint/PhabricatorQueryConstraint.php
··· 6 6 const OPERATOR_OR = 'or'; 7 7 const OPERATOR_NOT = 'not'; 8 8 const OPERATOR_NULL = 'null'; 9 + const OPERATOR_ANCESTOR = 'ancestor'; 9 10 10 11 private $operator; 11 12 private $value;
+77 -5
src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
··· 1516 1516 $constraints = mgroup($constraints, 'getOperator'); 1517 1517 foreach ($constraints as $operator => $list) { 1518 1518 foreach ($list as $item) { 1519 - $value = $item->getValue(); 1520 - $this->edgeLogicConstraints[$edge_type][$operator][$value] = $item; 1519 + $this->edgeLogicConstraints[$edge_type][$operator][] = $item; 1521 1520 } 1522 1521 } 1523 1522 ··· 1548 1547 $this->buildEdgeLogicTableAliasCount($alias)); 1549 1548 } 1550 1549 break; 1550 + case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: 1551 + // This is tricky. We have a query which specifies multiple 1552 + // projects, each of which may have an arbitrarily large number 1553 + // of descendants. 1554 + 1555 + // Suppose the projects are "Engineering" and "Operations", and 1556 + // "Engineering" has subprojects X, Y and Z. 1557 + 1558 + // We first use `FIELD(dst, X, Y, Z)` to produce a 0 if a row 1559 + // is not part of Engineering at all, or some number other than 1560 + // 0 if it is. 1561 + 1562 + // Then we use `IF(..., idx, NULL)` to convert the 0 to a NULL and 1563 + // any other value to an index (say, 1) for the ancestor. 1564 + 1565 + // We build these up for every ancestor, then use `COALESCE(...)` 1566 + // to select the non-null one, giving us an ancestor which this 1567 + // row is a member of. 1568 + 1569 + // From there, we use `COUNT(DISTINCT(...))` to make sure that 1570 + // each result row is a member of all ancestors. 1571 + if (count($list) > 1) { 1572 + $idx = 1; 1573 + $parts = array(); 1574 + foreach ($list as $constraint) { 1575 + $parts[] = qsprintf( 1576 + $conn, 1577 + 'IF(FIELD(%T.dst, %Ls) != 0, %d, NULL)', 1578 + $alias, 1579 + (array)$constraint->getValue(), 1580 + $idx++); 1581 + } 1582 + $parts = implode(', ', $parts); 1583 + 1584 + $select[] = qsprintf( 1585 + $conn, 1586 + 'COUNT(DISTINCT(COALESCE(%Q))) %T', 1587 + $parts, 1588 + $this->buildEdgeLogicTableAliasAncestor($alias)); 1589 + } 1590 + break; 1551 1591 default: 1552 1592 break; 1553 1593 } ··· 1573 1613 1574 1614 foreach ($constraints as $operator => $list) { 1575 1615 $alias = $this->getEdgeLogicTableAlias($operator, $type); 1616 + 1617 + $phids = array(); 1618 + foreach ($list as $constraint) { 1619 + $value = (array)$constraint->getValue(); 1620 + foreach ($value as $v) { 1621 + $phids[$v] = $v; 1622 + } 1623 + } 1624 + $phids = array_keys($phids); 1625 + 1576 1626 switch ($operator) { 1577 1627 case PhabricatorQueryConstraint::OPERATOR_NOT: 1578 1628 $joins[] = qsprintf( ··· 1586 1636 $alias, 1587 1637 $type, 1588 1638 $alias, 1589 - mpull($list, 'getValue')); 1639 + $phids); 1590 1640 break; 1641 + case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: 1591 1642 case PhabricatorQueryConstraint::OPERATOR_AND: 1592 1643 case PhabricatorQueryConstraint::OPERATOR_OR: 1593 1644 // If we're including results with no matches, we have to degrade ··· 1611 1662 $alias, 1612 1663 $type, 1613 1664 $alias, 1614 - mpull($list, 'getValue')); 1665 + $phids); 1615 1666 break; 1616 1667 case PhabricatorQueryConstraint::OPERATOR_NULL: 1617 1668 $joins[] = qsprintf( ··· 1711 1762 count($list)); 1712 1763 } 1713 1764 break; 1765 + case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: 1766 + if (count($list) > 1) { 1767 + $having[] = qsprintf( 1768 + $conn, 1769 + '%T = %d', 1770 + $this->buildEdgeLogicTableAliasAncestor($alias), 1771 + count($list)); 1772 + } 1773 + break; 1714 1774 } 1715 1775 } 1716 1776 } ··· 1729 1789 case PhabricatorQueryConstraint::OPERATOR_NOT: 1730 1790 case PhabricatorQueryConstraint::OPERATOR_AND: 1731 1791 case PhabricatorQueryConstraint::OPERATOR_OR: 1792 + case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: 1732 1793 if (count($list) > 1) { 1733 1794 return true; 1734 1795 } ··· 1758 1819 return $alias.'_count'; 1759 1820 } 1760 1821 1822 + /** 1823 + * @task edgelogic 1824 + */ 1825 + private function buildEdgeLogicTableAliasAncestor($alias) { 1826 + return $alias.'_ancestor'; 1827 + } 1828 + 1761 1829 1762 1830 /** 1763 1831 * Select certain edge logic constraint values. ··· 1781 1849 } 1782 1850 foreach ($constraints as $operator => $list) { 1783 1851 foreach ($list as $constraint) { 1784 - $values[] = $constraint->getValue(); 1852 + $value = (array)$constraint->getValue(); 1853 + foreach ($value as $v) { 1854 + $values[] = $v; 1855 + } 1785 1856 } 1786 1857 } 1787 1858 } ··· 1812 1883 PhabricatorQueryConstraint::OPERATOR_AND, 1813 1884 PhabricatorQueryConstraint::OPERATOR_OR, 1814 1885 PhabricatorQueryConstraint::OPERATOR_NOT, 1886 + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, 1815 1887 )); 1816 1888 if ($project_phids) { 1817 1889 $projects = id(new PhabricatorProjectQuery())