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 @@ -3686,6 +3686,7 @@ 'PhabricatorProjectLockTransaction' => 'applications/project/xaction/PhabricatorProjectLockTransaction.php', 'PhabricatorProjectLogicalAncestorDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAncestorDatasource.php', 'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php', + 'PhabricatorProjectLogicalOnlyDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOnlyDatasource.php', 'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php', 'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php', 'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php', @@ -9179,6 +9180,7 @@ 'PhabricatorProjectLockTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectLogicalAncestorDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorProjectLogicalOnlyDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource', diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php --- a/src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php @@ -21,6 +21,7 @@ new PhabricatorProjectLogicalAncestorDatasource(), new PhabricatorProjectLogicalOrNotDatasource(), new PhabricatorProjectLogicalViewerDatasource(), + new PhabricatorProjectLogicalOnlyDatasource(), new PhabricatorProjectLogicalUserDatasource(), ); } diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalOnlyDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalOnlyDatasource.php new file mode 100644 --- /dev/null +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalOnlyDatasource.php @@ -0,0 +1,76 @@ + array( + 'name' => pht('Only Match Other Constraints'), + 'summary' => pht( + 'Find results with only the specified tags.'), + 'description' => pht( + "This function is used with other tags, and causes the query to ". + "match only results with exactly those tags. For example, to find ". + "tasks tagged only iOS:". + "\n\n". + "> ios, only()". + "\n\n". + "This will omit results with any other project tag."), + ), + ); + } + + public function loadResults() { + $results = array( + $this->renderOnlyFunctionToken(), + ); + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_ONLY, + null); + + return $results; + } + + public function renderFunctionTokens( + $function, + array $argv_list) { + + $tokens = array(); + foreach ($argv_list as $argv) { + $tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->renderOnlyFunctionToken()); + } + + return $tokens; + } + + private function renderOnlyFunctionToken() { + return $this->newFunctionResult() + ->setName(pht('Only')) + ->setPHID('only()') + ->setIcon('fa-asterisk') + ->setUnique(true) + ->addAttribute( + pht('Select only results with exactly the other specified tags.')); + } + +} diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php --- a/src/applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php @@ -29,7 +29,7 @@ "\n\n". "This normally means //your// projects, but if you save a query ". "using this function and send it to someone else, it will mean ". - "//their// projects when they run it (they become the currnet ". + "//their// projects when they run it (they become the current ". "viewer). This can be useful for building dashboard panels."), ), ); diff --git a/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php b/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php --- a/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php +++ b/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php @@ -8,6 +8,7 @@ const OPERATOR_NULL = 'null'; const OPERATOR_ANCESTOR = 'ancestor'; const OPERATOR_EMPTY = 'empty'; + const OPERATOR_ONLY = 'only'; private $operator; private $value; diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -2069,6 +2069,27 @@ $op_null = PhabricatorQueryConstraint::OPERATOR_NULL; $has_null = isset($constraints[$op_null]); + // If we're going to process an only() operator, build a list of the + // acceptable set of PHIDs first. We'll only match results which have + // no edges to any other PHIDs. + $all_phids = array(); + if (isset($constraints[PhabricatorQueryConstraint::OPERATOR_ONLY])) { + foreach ($constraints as $operator => $list) { + switch ($operator) { + case PhabricatorQueryConstraint::OPERATOR_ANCESTOR: + case PhabricatorQueryConstraint::OPERATOR_AND: + case PhabricatorQueryConstraint::OPERATOR_OR: + foreach ($list as $constraint) { + $value = (array)$constraint->getValue(); + foreach ($value as $v) { + $all_phids[$v] = $v; + } + } + break; + } + } + } + foreach ($constraints as $operator => $list) { $alias = $this->getEdgeLogicTableAlias($operator, $type); @@ -2133,6 +2154,20 @@ $alias, $type); break; + case PhabricatorQueryConstraint::OPERATOR_ONLY: + $joins[] = qsprintf( + $conn, + 'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d + AND %T.dst NOT IN (%Ls)', + $edge_table, + $alias, + $phid_column, + $alias, + $alias, + $type, + $alias, + $all_phids); + break; } } } @@ -2159,6 +2194,7 @@ $alias = $this->getEdgeLogicTableAlias($operator, $type); switch ($operator) { case PhabricatorQueryConstraint::OPERATOR_NOT: + case PhabricatorQueryConstraint::OPERATOR_ONLY: $full[] = qsprintf( $conn, '%T.dst IS NULL', @@ -2258,6 +2294,7 @@ // discussion, see T12753. return true; case PhabricatorQueryConstraint::OPERATOR_NULL: + case PhabricatorQueryConstraint::OPERATOR_ONLY: return true; } } @@ -2375,6 +2412,34 @@ } } + $op_and = PhabricatorQueryConstraint::OPERATOR_AND; + $op_or = PhabricatorQueryConstraint::OPERATOR_OR; + $op_ancestor = PhabricatorQueryConstraint::OPERATOR_ANCESTOR; + + foreach ($this->edgeLogicConstraints as $type => $constraints) { + foreach ($constraints as $operator => $list) { + switch ($operator) { + case PhabricatorQueryConstraint::OPERATOR_ONLY: + if (count($list) > 1) { + throw new PhabricatorEmptyQueryException( + pht( + 'This query specifies only() more than once.')); + } + + $have_and = idx($constraints, $op_and); + $have_or = idx($constraints, $op_or); + $have_ancestor = idx($constraints, $op_ancestor); + if (!$have_and && !$have_or && !$have_ancestor) { + throw new PhabricatorEmptyQueryException( + pht( + 'This query specifies only(), but no other constraints '. + 'which it can apply to.')); + } + break; + } + } + } + $this->edgeLogicConstraintsAreValid = true; return $this;