Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php
index d455ddb19f..8dfcd60648 100644
--- a/src/applications/differential/query/DifferentialRevisionQuery.php
+++ b/src/applications/differential/query/DifferentialRevisionQuery.php
@@ -1,1084 +1,1095 @@
<?php
/**
* Flexible query API for Differential revisions. Example:
*
* // Load open revisions
* $revisions = id(new DifferentialRevisionQuery())
* ->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
* ->execute();
*
* @task config Query Configuration
* @task exec Query Execution
* @task internal Internals
*/
final class DifferentialRevisionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $pathIDs = array();
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_ACCEPTED = 'status-accepted';
const STATUS_NEEDS_REVIEW = 'status-needs-review';
const STATUS_NEEDS_REVISION = 'status-needs-revision';
const STATUS_CLOSED = 'status-closed'; // NOTE: Same as 'committed'
const STATUS_COMMITTED = 'status-committed'; // TODO: Remove.
const STATUS_ABANDONED = 'status-abandoned';
private $authors = array();
private $draftAuthors = array();
private $ccs = array();
private $reviewers = array();
private $revIDs = array();
private $commitHashes = array();
private $phids = array();
private $subscribers = array();
private $responsibles = array();
private $branches = array();
private $arcanistProjectPHIDs = array();
private $draftRevisions = array();
private $repositoryPHIDs;
private $order = 'order-modified';
const ORDER_MODIFIED = 'order-modified';
const ORDER_CREATED = 'order-created';
/**
* This is essentially a denormalized copy of the revision modified time that
* should perform better for path queries with a LIMIT. Critically, when you
* browse "/", every revision in that repository for all time will match so
* the query benefits from being able to stop before fully materializing the
* result set.
*/
const ORDER_PATH_MODIFIED = 'order-path-modified';
private $needRelationships = false;
private $needActiveDiffs = false;
private $needDiffIDs = false;
private $needCommitPHIDs = false;
private $needHashes = false;
private $needReviewerStatus = false;
private $buildingGlobalOrder;
/* -( Query Configuration )------------------------------------------------ */
/**
* Filter results to revisions which affect a Diffusion path ID in a given
* repository. You can call this multiple times to select revisions for
* several paths.
*
* @param int Diffusion repository ID.
* @param int Diffusion path ID.
* @return this
* @task config
*/
public function withPath($repository_id, $path_id) {
$this->pathIDs[] = array(
'repositoryID' => $repository_id,
'pathID' => $path_id,
);
return $this;
}
/**
* Filter results to revisions authored by one of the given PHIDs. Calling
* this function will clear anything set by previous calls to
* @{method:withAuthors}.
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withAuthors(array $author_phids) {
$this->authors = $author_phids;
return $this;
}
/**
* Filter results to revisions with comments authored bythe given PHIDs
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withDraftRepliesByAuthors(array $author_phids) {
$this->draftAuthors = $author_phids;
return $this;
}
/**
* Filter results to revisions which CC one of the listed people. Calling this
* function will clear anything set by previous calls to @{method:withCCs}.
*
* @param array List of PHIDs of subscribers
* @return this
* @task config
*/
public function withCCs(array $cc_phids) {
$this->ccs = $cc_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided PHIDs as
* reviewers. Calling this function will clear anything set by previous calls
* to @{method:withReviewers}.
*
* @param array List of PHIDs of reviewers
* @return this
* @task config
*/
public function withReviewers(array $reviewer_phids) {
$this->reviewers = $reviewer_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided commit hashes.
* Calling this function will clear anything set by previous calls to
* @{method:withCommitHashes}.
*
* @param array List of pairs <Class
* ArcanistDifferentialRevisionHash::HASH_$type constant,
* hash>
* @return this
* @task config
*/
public function withCommitHashes(array $commit_hashes) {
$this->commitHashes = $commit_hashes;
return $this;
}
/**
* Filter results to revisions with a given status. Provide a class constant,
* such as ##DifferentialRevisionQuery::STATUS_OPEN##.
*
* @param const Class STATUS constant, like STATUS_OPEN.
* @return this
* @task config
*/
public function withStatus($status_constant) {
$this->status = $status_constant;
return $this;
}
/**
* Filter results to revisions on given branches.
*
* @param list List of branch names.
* @return this
* @task config
*/
public function withBranches(array $branches) {
$this->branches = $branches;
return $this;
}
/**
* Filter results to only return revisions whose ids are in the given set.
*
* @param array List of revision ids
* @return this
* @task config
*/
public function withIDs(array $ids) {
$this->revIDs = $ids;
return $this;
}
/**
* Filter results to only return revisions whose PHIDs are in the given set.
*
* @param array List of revision PHIDs
* @return this
* @task config
*/
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
/**
* Given a set of users, filter results to return only revisions they are
* responsible for (i.e., they are either authors or reviewers).
*
* @param array List of user PHIDs.
* @return this
* @task config
*/
public function withResponsibleUsers(array $responsible_phids) {
$this->responsibles = $responsible_phids;
return $this;
}
/**
* Filter results to only return revisions with a given set of subscribers
* (i.e., they are authors, reviewers or CC'd).
*
* @param array List of user PHIDs.
* @return this
* @task config
*/
public function withSubscribers(array $subscriber_phids) {
$this->subscribers = $subscriber_phids;
return $this;
}
/**
* Filter results to only return revisions with a given set of arcanist
* projects.
*
* @param array List of project PHIDs.
* @return this
* @task config
*/
public function withArcanistProjectPHIDs(array $arc_project_phids) {
$this->arcanistProjectPHIDs = $arc_project_phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
/**
* Set result ordering. Provide a class constant, such as
* ##DifferentialRevisionQuery::ORDER_CREATED##.
*
* @task config
*/
public function setOrder($order_constant) {
$this->order = $order_constant;
return $this;
}
/**
* Set whether or not the query will load and attach relationships.
*
* @param bool True to load and attach relationships.
* @return this
* @task config
*/
public function needRelationships($need_relationships) {
$this->needRelationships = $need_relationships;
return $this;
}
/**
* Set whether or not the query should load the active diff for each
* revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needActiveDiffs($need_active_diffs) {
$this->needActiveDiffs = $need_active_diffs;
return $this;
}
/**
* Set whether or not the query should load the associated commit PHIDs for
* each revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needCommitPHIDs($need_commit_phids) {
$this->needCommitPHIDs = $need_commit_phids;
return $this;
}
/**
* Set whether or not the query should load associated diff IDs for each
* revision.
*
* @param bool True to load and attach diff IDs.
* @return this
* @task config
*/
public function needDiffIDs($need_diff_ids) {
$this->needDiffIDs = $need_diff_ids;
return $this;
}
/**
* Set whether or not the query should load associated commit hashes for each
* revision.
*
* @param bool True to load and attach commit hashes.
* @return this
* @task config
*/
public function needHashes($need_hashes) {
$this->needHashes = $need_hashes;
return $this;
}
/**
* Set whether or not the query should load associated reviewer status.
*
* @param bool True to load and attach reviewers.
* @return this
* @task config
*/
public function needReviewerStatus($need_reviewer_status) {
$this->needReviewerStatus = $need_reviewer_status;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
/**
* Execute the query as configured, returning matching
* @{class:DifferentialRevision} objects.
*
* @return list List of matching DifferentialRevision objects.
* @task exec
*/
public function loadPage() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
$data = $this->loadData();
return $table->loadAllFromArray($data);
}
public function willFilterPage(array $revisions) {
$viewer = $this->getViewer();
$repository_phids = mpull($revisions, 'getRepositoryPHID');
$repository_phids = array_filter($repository_phids);
$repositories = array();
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
}
// If a revision is associated with a repository:
//
// - the viewer must be able to see the repository; or
// - the viewer must have an automatic view capability.
//
// In the latter case, we'll load the revision but not load the repository.
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
foreach ($revisions as $key => $revision) {
$repo_phid = $revision->getRepositoryPHID();
if (!$repo_phid) {
// The revision has no associated repository. Attach `null` and move on.
$revision->attachRepository(null);
continue;
}
$repository = idx($repositories, $repo_phid);
if ($repository) {
// The revision has an associated repository, and the viewer can see
// it. Attach it and move on.
$revision->attachRepository($repository);
continue;
}
if ($revision->hasAutomaticCapability($can_view, $viewer)) {
// The revision has an associated repository which the viewer can not
// see, but the viewer has an automatic capability on this revision.
// Load the revision without attaching a repository.
$revision->attachRepository(null);
continue;
}
// The revision has an associated repository, and the viewer can't see
// it, and the viewer has no special capabilities. Filter out this
// revision.
$this->didRejectResult($revision);
unset($revisions[$key]);
}
if (!$revisions) {
return array();
}
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
if ($this->needRelationships) {
$this->loadRelationships($conn_r, $revisions);
}
if ($this->needCommitPHIDs) {
$this->loadCommitPHIDs($conn_r, $revisions);
}
$need_active = $this->needActiveDiffs;
$need_ids = $need_active || $this->needDiffIDs;
if ($need_ids) {
$this->loadDiffIDs($conn_r, $revisions);
}
if ($need_active) {
$this->loadActiveDiffs($conn_r, $revisions);
}
if ($this->needHashes) {
$this->loadHashes($conn_r, $revisions);
}
if ($this->needReviewerStatus) {
$this->loadReviewers($conn_r, $revisions);
}
return $revisions;
}
private function loadData() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
if ($this->draftAuthors) {
$this->draftRevisions = array();
$draft_key = 'differential-comment-';
$drafts = id(new PhabricatorDraft())->loadAllWhere(
'authorPHID IN (%Ls) AND draftKey LIKE %> AND draft != %s',
$this->draftAuthors,
$draft_key,
'');
$len = strlen($draft_key);
foreach ($drafts as $draft) {
$this->draftRevisions[] = substr($draft->getDraftKey(), $len);
}
$inlines = id(new DifferentialInlineCommentQuery())
->withDraftsByAuthors($this->draftAuthors)
->execute();
foreach ($inlines as $inline) {
$this->draftRevisions[] = $inline->getRevisionID();
}
if (!$this->draftRevisions) {
return array();
}
}
$selects = array();
// NOTE: If the query includes "responsiblePHIDs", we execute it as a
// UNION of revisions they own and revisions they're reviewing. This has
// much better performance than doing it with JOIN/WHERE.
if ($this->responsibles) {
$basic_authors = $this->authors;
$basic_reviewers = $this->reviewers;
+
+ $authority_projects = id(new PhabricatorProjectQuery())
+ ->setViewer($this->getViewer())
+ ->withMemberPHIDs($this->responsibles)
+ ->execute();
+ $authority_phids = mpull($authority_projects, 'getPHID');
+
try {
// Build the query where the responsible users are authors.
$this->authors = array_merge($basic_authors, $this->responsibles);
$this->reviewers = $basic_reviewers;
$selects[] = $this->buildSelectStatement($conn_r);
- // Build the query where the responsible users are reviewers.
+ // Build the query where the responsible users are reviewers, or
+ // projects they are members of are reviewers.
$this->authors = $basic_authors;
- $this->reviewers = array_merge($basic_reviewers, $this->responsibles);
+ $this->reviewers = array_merge(
+ $basic_reviewers,
+ $this->responsibles,
+ $authority_phids);
$selects[] = $this->buildSelectStatement($conn_r);
// Put everything back like it was.
$this->authors = $basic_authors;
$this->reviewers = $basic_reviewers;
} catch (Exception $ex) {
$this->authors = $basic_authors;
$this->reviewers = $basic_reviewers;
throw $ex;
}
} else {
$selects[] = $this->buildSelectStatement($conn_r);
}
if (count($selects) > 1) {
$this->buildingGlobalOrder = true;
$query = qsprintf(
$conn_r,
'%Q %Q %Q',
implode(' UNION DISTINCT ', $selects),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
} else {
$query = head($selects);
}
return queryfx_all($conn_r, '%Q', $query);
}
private function buildSelectStatement(AphrontDatabaseConnection $conn_r) {
$table = new DifferentialRevision();
$select = qsprintf(
$conn_r,
'SELECT r.* FROM %T r',
$table->getTableName());
$joins = $this->buildJoinsClause($conn_r);
$where = $this->buildWhereClause($conn_r);
$group_by = $this->buildGroupByClause($conn_r);
$this->buildingGlobalOrder = false;
$order_by = $this->buildOrderClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
return qsprintf(
$conn_r,
'(%Q %Q %Q %Q %Q %Q)',
$select,
$joins,
$where,
$group_by,
$order_by,
$limit);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function buildJoinsClause($conn_r) {
$joins = array();
if ($this->pathIDs) {
$path_table = new DifferentialAffectedPath();
$joins[] = qsprintf(
$conn_r,
'JOIN %T p ON p.revisionID = r.id',
$path_table->getTableName());
}
if ($this->commitHashes) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T hash_rel ON hash_rel.revisionID = r.id',
ArcanistDifferentialRevisionHash::TABLE_NAME);
}
if ($this->ccs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T cc_rel ON cc_rel.revisionID = r.id '.
'AND cc_rel.relation = %s '.
'AND cc_rel.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_SUBSCRIBED,
$this->ccs);
}
if ($this->reviewers) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T e_reviewers ON e_reviewers.src = r.phid '.
'AND e_reviewers.type = %s '.
'AND e_reviewers.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER,
$this->reviewers);
}
if ($this->subscribers) {
// TODO: These can be expressed as a JOIN again (and the corresponding
// WHERE clause removed) once subscribers move to edges.
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T sub_rel_cc ON sub_rel_cc.revisionID = r.id '.
'AND sub_rel_cc.relation = %s '.
'AND sub_rel_cc.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_SUBSCRIBED,
$this->subscribers);
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T sub_rel_reviewer ON sub_rel_reviewer.src = r.phid '.
'AND sub_rel_reviewer.type = %s '.
'AND sub_rel_reviewer.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER,
$this->subscribers);
}
$joins = implode(' ', $joins);
return $joins;
}
/**
* @task internal
*/
private function buildWhereClause($conn_r) {
$where = array();
if ($this->pathIDs) {
$path_clauses = array();
$repo_info = igroup($this->pathIDs, 'repositoryID');
foreach ($repo_info as $repository_id => $paths) {
$path_clauses[] = qsprintf(
$conn_r,
'(p.repositoryID = %d AND p.pathID IN (%Ld))',
$repository_id,
ipull($paths, 'pathID'));
}
$path_clauses = '('.implode(' OR ', $path_clauses).')';
$where[] = $path_clauses;
}
if ($this->authors) {
$where[] = qsprintf(
$conn_r,
'r.authorPHID IN (%Ls)',
$this->authors);
}
if ($this->draftRevisions) {
$where[] = qsprintf(
$conn_r,
'r.id IN (%Ld)',
$this->draftRevisions);
}
if ($this->revIDs) {
$where[] = qsprintf(
$conn_r,
'r.id IN (%Ld)',
$this->revIDs);
}
if ($this->repositoryPHIDs) {
$where[] = qsprintf(
$conn_r,
'r.repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->commitHashes) {
$hash_clauses = array();
foreach ($this->commitHashes as $info) {
list($type, $hash) = $info;
$hash_clauses[] = qsprintf(
$conn_r,
'(hash_rel.type = %s AND hash_rel.hash = %s)',
$type,
$hash);
}
$hash_clauses = '('.implode(' OR ', $hash_clauses).')';
$where[] = $hash_clauses;
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'r.phid IN (%Ls)',
$this->phids);
}
if ($this->branches) {
$where[] = qsprintf(
$conn_r,
'r.branchName in (%Ls)',
$this->branches);
}
if ($this->arcanistProjectPHIDs) {
$where[] = qsprintf(
$conn_r,
'r.arcanistProjectPHID in (%Ls)',
$this->arcanistProjectPHIDs);
}
if ($this->subscribers) {
$where[] = qsprintf(
$conn_r,
'(sub_rel_cc.objectPHID IS NOT NULL)
OR (sub_rel_reviewer.dst IS NOT NULL)');
}
switch ($this->status) {
case self::STATUS_ANY:
break;
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::ACCEPTED,
));
break;
case self::STATUS_NEEDS_REVIEW:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
));
break;
case self::STATUS_NEEDS_REVISION:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
));
break;
case self::STATUS_ACCEPTED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::ACCEPTED,
));
break;
case self::STATUS_COMMITTED:
phlog(
"WARNING: DifferentialRevisionQuery using deprecated ".
"STATUS_COMMITTED constant. This will be removed soon. ".
"Use STATUS_CLOSED.");
// fallthrough
case self::STATUS_CLOSED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::CLOSED,
));
break;
case self::STATUS_ABANDONED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::ABANDONED,
));
break;
default:
throw new Exception(
"Unknown revision status filter constant '{$this->status}'!");
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
/**
* @task internal
*/
private function buildGroupByClause($conn_r) {
$join_triggers = array_merge(
$this->pathIDs,
$this->ccs,
$this->reviewers,
$this->subscribers);
$needs_distinct = (count($join_triggers) > 1);
if ($needs_distinct) {
return 'GROUP BY r.id';
} else {
return '';
}
}
private function loadCursorObject($id) {
$results = id(new DifferentialRevisionQuery())
->setViewer($this->getPagingViewer())
->withIDs(array((int)$id))
->execute();
return head($results);
}
protected function buildPagingClause(AphrontDatabaseConnection $conn_r) {
$default = parent::buildPagingClause($conn_r);
$before_id = $this->getBeforeID();
$after_id = $this->getAfterID();
if (!$before_id && !$after_id) {
return $default;
}
if ($before_id) {
$cursor = $this->loadCursorObject($before_id);
} else {
$cursor = $this->loadCursorObject($after_id);
}
if (!$cursor) {
return null;
}
$columns = array();
switch ($this->order) {
case self::ORDER_CREATED:
return $default;
case self::ORDER_MODIFIED:
$columns[] = array(
'name' => 'r.dateModified',
'value' => $cursor->getDateModified(),
'type' => 'int',
);
break;
case self::ORDER_PATH_MODIFIED:
$columns[] = array(
'name' => 'p.epoch',
'value' => $cursor->getDateCreated(),
'type' => 'int',
);
break;
}
$columns[] = array(
'name' => 'r.id',
'value' => $cursor->getID(),
'type' => 'int',
);
return $this->buildPagingClauseFromMultipleColumns(
$conn_r,
$columns,
array(
'reversed' => (bool)($before_id xor $this->getReversePaging()),
));
}
protected function getPagingColumn() {
$is_global = $this->buildingGlobalOrder;
switch ($this->order) {
case self::ORDER_MODIFIED:
if ($is_global) {
return 'dateModified';
}
return 'r.dateModified';
case self::ORDER_CREATED:
if ($is_global) {
return 'id';
}
return 'r.id';
case self::ORDER_PATH_MODIFIED:
if (!$this->pathIDs) {
throw new Exception(
"To use ORDER_PATH_MODIFIED, you must specify withPath().");
}
return 'p.epoch';
default:
throw new Exception("Unknown query order constant '{$this->order}'.");
}
}
private function loadRelationships($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$relationships = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID in (%Ld)
AND relation != %s ORDER BY sequence',
DifferentialRevision::RELATIONSHIP_TABLE,
mpull($revisions, 'getID'),
DifferentialRevision::RELATION_REVIEWER);
$relationships = igroup($relationships, 'revisionID');
$type_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($revisions, 'getPHID'))
->withEdgeTypes(array($type_reviewer))
->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST)
->execute();
foreach ($revisions as $revision) {
$data = idx($relationships, $revision->getID(), array());
$revision_edges = $edges[$revision->getPHID()][$type_reviewer];
foreach ($revision_edges as $dst_phid => $edge_data) {
$data[] = array(
'relation' => DifferentialRevision::RELATION_REVIEWER,
'objectPHID' => $dst_phid,
'reasonPHID' => null,
);
}
$revision->attachRelationships($data);
}
}
private function loadCommitPHIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$commit_phids = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
DifferentialRevision::TABLE_COMMIT,
mpull($revisions, 'getID'));
$commit_phids = igroup($commit_phids, 'revisionID');
foreach ($revisions as $revision) {
$phids = idx($commit_phids, $revision->getID(), array());
$phids = ipull($phids, 'commitPHID');
$revision->attachCommitPHIDs($phids);
}
}
private function loadDiffIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$diff_ids = queryfx_all(
$conn_r,
'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld)
ORDER BY id DESC',
$diff_table->getTableName(),
mpull($revisions, 'getID'));
$diff_ids = igroup($diff_ids, 'revisionID');
foreach ($revisions as $revision) {
$ids = idx($diff_ids, $revision->getID(), array());
$ids = ipull($ids, 'id');
$revision->attachDiffIDs($ids);
}
}
private function loadActiveDiffs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$load_ids = array();
foreach ($revisions as $revision) {
$diffs = $revision->getDiffIDs();
if ($diffs) {
$load_ids[] = max($diffs);
}
}
$active_diffs = array();
if ($load_ids) {
$active_diffs = $diff_table->loadAllWhere(
'id IN (%Ld)',
$load_ids);
}
$active_diffs = mpull($active_diffs, null, 'getRevisionID');
foreach ($revisions as $revision) {
$revision->attachActiveDiff(idx($active_diffs, $revision->getID()));
}
}
private function loadHashes(
AphrontDatabaseConnection $conn_r,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
'differential_revisionhash',
mpull($revisions, 'getID'));
$data = igroup($data, 'revisionID');
foreach ($revisions as $revision) {
$hashes = idx($data, $revision->getID(), array());
$list = array();
foreach ($hashes as $hash) {
$list[] = array($hash['type'], $hash['hash']);
}
$revision->attachHashes($list);
}
}
private function loadReviewers(
AphrontDatabaseConnection $conn_r,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($revisions, 'getPHID'))
->withEdgeTypes(array($edge_type))
->needEdgeData(true)
->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST)
->execute();
foreach ($revisions as $revision) {
$revision_edges = $edges[$revision->getPHID()][$edge_type];
$reviewers = array();
foreach ($revision_edges as $user_phid => $edge) {
$data = $edge['data'];
$reviewers[] = new DifferentialReviewer(
$user_phid,
idx($data, 'status'),
idx($data, 'diff'));
}
$revision->attachReviewerStatus($reviewers);
}
}
public static function splitResponsible(array $revisions, array $user_phids) {
$blocking = array();
$active = array();
$waiting = array();
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
// Bucket revisions into $blocking (revisions where you are blocking
// others), $active (revisions you need to do something about) and $waiting
// (revisions you're waiting on someone else to do something about).
foreach ($revisions as $revision) {
$needs_review = ($revision->getStatus() == $status_review);
$filter_is_author = in_array($revision->getAuthorPHID(), $user_phids);
if (!$revision->getReviewers()) {
$needs_review = false;
}
// If exactly one of "needs review" and "the user is the author" is
// true, the user needs to act on it. Otherwise, they're waiting on
// it.
if ($needs_review ^ $filter_is_author) {
if ($needs_review) {
array_unshift($blocking, $revision);
} else {
$active[] = $revision;
}
} else {
$waiting[] = $revision;
}
}
return array($blocking, $active, $waiting);
}
}
diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php
index 377d2b600d..b9e2969cce 100644
--- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php
+++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php
@@ -1,239 +1,244 @@
<?php
final class DifferentialRevisionSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getPageSize(PhabricatorSavedQuery $saved) {
if ($saved->getQueryKey() == 'active') {
return 0xFFFF;
}
return parent::getPageSize($saved);
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter(
'responsiblePHIDs',
$this->readUsersFromRequest($request, 'responsibles'));
$saved->setParameter(
'authorPHIDs',
$this->readUsersFromRequest($request, 'authors'));
$saved->setParameter(
'reviewerPHIDs',
- $this->readUsersFromRequest($request, 'reviewers'));
+ $this->readUsersFromRequest(
+ $request,
+ 'reviewers',
+ array(
+ PhabricatorProjectPHIDTypeProject::TYPECONST,
+ )));
$saved->setParameter(
'subscriberPHIDs',
$this->readUsersFromRequest($request, 'subscribers'));
$saved->setParameter(
'repositoryPHIDs',
$request->getArr('repositories'));
$saved->setParameter(
'draft',
$request->getBool('draft'));
$saved->setParameter(
'order',
$request->getStr('order'));
$saved->setParameter(
'status',
$request->getStr('status'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new DifferentialRevisionQuery())
->needRelationships(true);
$responsible_phids = $saved->getParameter('responsiblePHIDs', array());
if ($responsible_phids) {
$query->withResponsibleUsers($responsible_phids);
}
$author_phids = $saved->getParameter('authorPHIDs', array());
if ($author_phids) {
$query->withAuthors($author_phids);
}
$reviewer_phids = $saved->getParameter('reviewerPHIDs', array());
if ($reviewer_phids) {
$query->withReviewers($reviewer_phids);
}
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
if ($subscriber_phids) {
$query->withCCs($subscriber_phids);
}
$repository_phids = $saved->getParameter('repositoryPHIDs', array());
if ($repository_phids) {
$query->withRepositoryPHIDs($repository_phids);
}
$draft = $saved->getParameter('draft', false);
if ($draft && $this->requireViewer()->isLoggedIn()) {
$query->withDraftRepliesByAuthors(
array($this->requireViewer()->getPHID()));
}
$status = $saved->getParameter('status');
if (idx($this->getStatusOptions(), $status)) {
$query->withStatus($status);
}
$order = $saved->getParameter('order');
if (idx($this->getOrderOptions(), $order)) {
$query->setOrder($order);
} else {
$query->setOrder(DifferentialRevisionQuery::ORDER_CREATED);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$responsible_phids = $saved->getParameter('responsiblePHIDs', array());
$author_phids = $saved->getParameter('authorPHIDs', array());
$reviewer_phids = $saved->getParameter('reviewerPHIDs', array());
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$repository_phids = $saved->getParameter('repositoryPHIDs', array());
$only_draft = $saved->getParameter('draft', false);
$all_phids = array_mergev(
array(
$responsible_phids,
$author_phids,
$reviewer_phids,
$subscriber_phids,
$repository_phids,
));
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->withPHIDs($all_phids)
->execute();
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Responsible Users'))
->setName('responsibles')
->setDatasource('/typeahead/common/accounts/')
->setValue(array_select_keys($handles, $responsible_phids)))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Authors'))
->setName('authors')
->setDatasource('/typeahead/common/accounts/')
->setValue(array_select_keys($handles, $author_phids)))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Reviewers'))
->setName('reviewers')
- ->setDatasource('/typeahead/common/accounts/')
+ ->setDatasource('/typeahead/common/accountsorprojects/')
->setValue(array_select_keys($handles, $reviewer_phids)))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Subscribers'))
->setName('subscribers')
->setDatasource('/typeahead/common/allmailable/')
->setValue(array_select_keys($handles, $subscriber_phids)))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Repositories'))
->setName('repositories')
->setDatasource('/typeahead/common/repositories/')
->setValue(array_select_keys($handles, $repository_phids)))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setOptions($this->getStatusOptions())
->setValue($saved->getParameter('status')));
if ($this->requireViewer()->isLoggedIn()) {
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'draft',
1,
pht('Show only revisions with a draft comment.'),
$only_draft));
}
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Order'))
->setName('order')
->setOptions($this->getOrderOptions())
->setValue($saved->getParameter('order')));
}
protected function getURI($path) {
return '/differential/'.$path;
}
public function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['active'] = pht('Active Revisions');
$names['authored'] = pht('Authored');
}
$names['all'] = pht('All Revisions');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer = $this->requireViewer();
switch ($query_key) {
case 'active':
return $query
->setParameter('responsiblePHIDs', array($viewer->getPHID()))
->setParameter('status', DifferentialRevisionQuery::STATUS_OPEN);
case 'authored':
return $query
->setParameter('authorPHIDs', array($viewer->getPHID()));
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getStatusOptions() {
return array(
DifferentialRevisionQuery::STATUS_ANY => pht('All'),
DifferentialRevisionQuery::STATUS_OPEN => pht('Open'),
DifferentialRevisionQuery::STATUS_CLOSED => pht('Closed'),
DifferentialRevisionQuery::STATUS_ABANDONED => pht('Abandoned'),
);
}
private function getOrderOptions() {
return array(
DifferentialRevisionQuery::ORDER_CREATED => pht('Created'),
DifferentialRevisionQuery::ORDER_MODIFIED => pht('Updated'),
);
}
}
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index 70cc7536ec..f960631a13 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,523 +1,532 @@
<?php
/**
* Represents an abstract search engine for an application. It supports
* creating and storing saved queries.
*
* @task builtin Builtin Queries
* @task uri Query URIs
* @task dates Date Filters
* @task read Reading Utilities
*
* @group search
*/
abstract class PhabricatorApplicationSearchEngine {
private $viewer;
private $errors = array();
private $customFields = false;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
protected function requireViewer() {
if (!$this->viewer) {
throw new Exception("Call setViewer() before using an engine!");
}
return $this->viewer;
}
/**
* Create a saved query object from the request.
*
* @param AphrontRequest The search request.
* @return PhabricatorSavedQuery
*/
abstract public function buildSavedQueryFromRequest(
AphrontRequest $request);
/**
* Executes the saved query.
*
* @param PhabricatorSavedQuery The saved query to operate on.
* @return The result of the query.
*/
abstract public function buildQueryFromSavedQuery(
PhabricatorSavedQuery $saved);
/**
* Builds the search form using the request.
*
* @param AphrontFormView Form to populate.
* @param PhabricatorSavedQuery The query from which to build the form.
* @return void
*/
abstract public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $query);
public function getErrors() {
return $this->errors;
}
public function addError($error) {
$this->errors[] = $error;
return $this;
}
/**
* Return an application URI corresponding to the results page of a query.
* Normally, this is something like `/application/query/QUERYKEY/`.
*
* @param string The query key to build a URI for.
* @return string URI where the query can be executed.
* @task uri
*/
public function getQueryResultsPageURI($query_key) {
return $this->getURI('query/'.$query_key.'/');
}
/**
* Return an application URI for query management. This is used when, e.g.,
* a query deletion operation is cancelled.
*
* @return string URI where queries can be managed.
* @task uri
*/
public function getQueryManagementURI() {
return $this->getURI('query/edit/');
}
/**
* Return the URI to a path within the application. Used to construct default
* URIs for management and results.
*
* @return string URI to path.
* @task uri
*/
abstract protected function getURI($path);
public function newSavedQuery() {
return id(new PhabricatorSavedQuery())
->setEngineClassName(get_class($this));
}
public function addNavigationItems(PHUIListView $menu) {
$viewer = $this->requireViewer();
$menu->newLabel(pht('Queries'));
$named_queries = $this->loadEnabledNamedQueries();
foreach ($named_queries as $query) {
$key = $query->getQueryKey();
$uri = $this->getQueryResultsPageURI($key);
$menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
}
if ($viewer->isLoggedIn()) {
$manage_uri = $this->getQueryManagementURI();
$menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
}
$menu->newLabel(pht('Search'));
$advanced_uri = $this->getQueryResultsPageURI('advanced');
$menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
return $this;
}
public function loadAllNamedQueries() {
$viewer = $this->requireViewer();
$named_queries = id(new PhabricatorNamedQueryQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withEngineClassNames(array(get_class($this)))
->execute();
$named_queries = mpull($named_queries, null, 'getQueryKey');
$builtin = $this->getBuiltinQueries($viewer);
$builtin = mpull($builtin, null, 'getQueryKey');
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin()) {
if (isset($builtin[$key])) {
$named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
unset($builtin[$key]);
} else {
unset($named_queries[$key]);
}
}
unset($builtin[$key]);
}
$named_queries = msort($named_queries, 'getSortKey');
return $named_queries + $builtin;
}
public function loadEnabledNamedQueries() {
$named_queries = $this->loadAllNamedQueries();
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
unset($named_queries[$key]);
}
}
return $named_queries;
}
/* -( Builtin Queries )---------------------------------------------------- */
/**
* @task builtin
*/
public function getBuiltinQueries() {
$names = $this->getBuiltinQueryNames();
$queries = array();
$sequence = 0;
foreach ($names as $key => $name) {
$queries[$key] = id(new PhabricatorNamedQuery())
->setUserPHID($this->requireViewer()->getPHID())
->setEngineClassName(get_class($this))
->setQueryName($name)
->setQueryKey($key)
->setSequence((1 << 24) + $sequence++)
->setIsBuiltin(true);
}
return $queries;
}
/**
* @task builtin
*/
public function getBuiltinQuery($query_key) {
if (!$this->isBuiltinQuery($query_key)) {
throw new Exception("'{$query_key}' is not a builtin!");
}
return idx($this->getBuiltinQueries(), $query_key);
}
/**
* @task builtin
*/
protected function getBuiltinQueryNames() {
return array();
}
/**
* @task builtin
*/
public function isBuiltinQuery($query_key) {
$builtins = $this->getBuiltinQueries();
return isset($builtins[$query_key]);
}
/**
* @task builtin
*/
public function buildSavedQueryFromBuiltin($query_key) {
throw new Exception("Builtin '{$query_key}' is not supported!");
}
/* -( Reading Utilities )--------------------------------------------------- */
/**
* Read a list of user PHIDs from a request in a flexible way. This method
* supports either of these forms:
*
* users[]=alincoln&users[]=htaft
* users=alincoln,htaft
*
* Additionally, users can be specified either by PHID or by name.
*
* The main goal of this flexibility is to allow external programs to generate
* links to pages (like "alincoln's open revisions") without needing to make
* API calls.
*
* @param AphrontRequest Request to read user PHIDs from.
* @param string Key to read in the request.
+ * @param list<const> Other permitted PHID types.
* @return list<phid> List of user PHIDs.
*
* @task read
*/
- protected function readUsersFromRequest(AphrontRequest $request, $key) {
+ protected function readUsersFromRequest(
+ AphrontRequest $request,
+ $key,
+ array $allow_types = array()) {
$list = $request->getArr($key, null);
if ($list === null) {
$list = $request->getStrList($key);
}
$phids = array();
$names = array();
+ $allow_types = array_fuse($allow_types);
$user_type = PhabricatorPHIDConstants::PHID_TYPE_USER;
foreach ($list as $item) {
- if (phid_get_type($item) == $user_type) {
+ $type = phid_get_type($item);
+ phlog($type);
+ if ($type == $user_type) {
+ $phids[] = $item;
+ } else if (isset($allow_types[$type])) {
$phids[] = $item;
} else {
$names[] = $item;
}
}
if ($names) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireViewer())
->withUsernames($names)
->execute();
foreach ($users as $user) {
$phids[] = $user->getPHID();
}
$phids = array_unique($phids);
}
return $phids;
}
/* -( Dates )-------------------------------------------------------------- */
/**
* @task dates
*/
protected function parseDateTime($date_time) {
if (!strlen($date_time)) {
return null;
}
return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
}
/**
* @task dates
*/
protected function buildDateRange(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query,
$start_key,
$start_name,
$end_key,
$end_name) {
$start_str = $saved_query->getParameter($start_key);
$start = null;
if (strlen($start_str)) {
$start = $this->parseDateTime($start_str);
if (!$start) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$start_name));
}
}
$end_str = $saved_query->getParameter($end_key);
$end = null;
if (strlen($end_str)) {
$end = $this->parseDateTime($end_str);
if (!$end) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$end_name));
}
}
if ($start && $end && ($start >= $end)) {
$this->addError(
pht(
'"%s" must be a date before "%s".',
$start_name,
$end_name));
}
$form
->appendChild(
id(new PHUIFormFreeformDateControl())
->setName($start_key)
->setLabel($start_name)
->setValue($start_str))
->appendChild(
id(new AphrontFormTextControl())
->setName($end_key)
->setLabel($end_name)
->setValue($end_str));
}
/* -( Pagination )--------------------------------------------------------- */
public function getPageSize(PhabricatorSavedQuery $saved) {
return $saved->getParameter('limit', 100);
}
/* -( Application Search )------------------------------------------------- */
/**
* Retrieve an object to use to define custom fields for this search.
*
* To integrate with custom fields, subclasses should override this method
* and return an instance of the application object which implements
* @{interface:PhabricatorCustomFieldInterface}.
*
* @return PhabricatorCustomFieldInterface|null Object with custom fields.
* @task appsearch
*/
public function getCustomFieldObject() {
return null;
}
/**
* Get the custom fields for this search.
*
* @return PhabricatorCustomFieldList|null Custom fields, if this search
* supports custom fields.
* @task appsearch
*/
public function getCustomFieldList() {
if ($this->customFields === false) {
$object = $this->getCustomFieldObject();
if ($object) {
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
} else {
$fields = null;
}
$this->customFields = $fields;
}
return $this->customFields;
}
/**
* Moves data from the request into a saved query.
*
* @param AphrontRequest Request to read.
* @param PhabricatorSavedQuery Query to write to.
* @return void
* @task appsearch
*/
protected function readCustomFieldsFromRequest(
AphrontRequest $request,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $field->readApplicationSearchValueFromRequest(
$this,
$request);
$saved->setParameter($key, $value);
}
}
/**
* Applies data from a saved query to an executable query.
*
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
* @param PhabricatorSavedQuery Saved query to read.
* @return void
*/
protected function applyCustomFieldsToQuery(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $field->applyApplicationSearchConstraintToQuery(
$this,
$query,
$saved->getParameter($key));
}
}
/**
* Get a unique key identifying a field.
*
* @param PhabricatorCustomField Field to identify.
* @return string Unique identifier, suitable for use as an input name.
*/
public function getKeyForCustomField(PhabricatorCustomField $field) {
return 'custom:'.$field->getFieldIndex();
}
/**
* Add inputs to an application search form so the user can query on custom
* fields.
*
* @param AphrontFormView Form to update.
* @param PhabricatorSavedQuery Values to prefill.
* @return void
*/
protected function appendCustomFieldsToForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
$phids = array();
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $saved->getParameter($key);
$phids[$key] = $field->getRequiredHandlePHIDsForApplicationSearch($value);
}
$all_phids = array_mergev($phids);
$handles = array();
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->withPHIDs($all_phids)
->execute();
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $saved->getParameter($key);
$field->appendToApplicationSearchForm(
$this,
$form,
$value,
array_select_keys($handles, $phids[$key]));
}
}
}
diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
index 87881dc2e5..fb0c5a66d2 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
@@ -1,374 +1,379 @@
<?php
final class PhabricatorTypeaheadCommonDatasourceController
extends PhabricatorTypeaheadDatasourceController {
private $type;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->type = $data['type'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$query = $request->getStr('q');
$raw_query = $request->getStr('raw');
$need_rich_data = false;
$need_users = false;
$need_agents = false;
$need_applications = false;
$need_all_users = false;
$need_lists = false;
$need_projs = false;
$need_repos = false;
$need_packages = false;
$need_upforgrabs = false;
$need_arcanist_projects = false;
$need_noproject = false;
$need_symbols = false;
$need_jump_objects = false;
switch ($this->type) {
case 'mainsearch':
$need_users = true;
$need_applications = true;
$need_rich_data = true;
$need_symbols = true;
$need_projs = true;
$need_jump_objects = true;
break;
case 'searchowner':
$need_users = true;
$need_upforgrabs = true;
break;
case 'searchproject':
$need_projs = true;
$need_noproject = true;
break;
case 'users':
$need_users = true;
break;
case 'authors':
$need_users = true;
$need_agents = true;
break;
case 'mailable':
$need_users = true;
$need_lists = true;
break;
case 'allmailable':
$need_users = true;
$need_all_users = true;
$need_lists = true;
break;
case 'projects':
$need_projs = true;
break;
case 'usersorprojects':
$need_users = true;
$need_projs = true;
break;
case 'repositories':
$need_repos = true;
break;
case 'packages':
$need_packages = true;
break;
case 'accounts':
$need_users = true;
$need_all_users = true;
break;
+ case 'accountsorprojects':
+ $need_users = true;
+ $need_all_users = true;
+ $need_projs = true;
+ break;
case 'arcanistprojects':
$need_arcanist_projects = true;
break;
}
$results = array();
if ($need_upforgrabs) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName('upforgrabs (Up For Grabs)')
->setPHID(ManiphestTaskOwner::OWNER_UP_FOR_GRABS);
}
if ($need_noproject) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName('noproject (No Project)')
->setPHID(ManiphestTaskOwner::PROJECT_NO_PROJECT);
}
if ($need_users) {
$columns = array(
'isSystemAgent',
'isAdmin',
'isDisabled',
'userName',
'realName',
'phid');
if ($query) {
// This is an arbitrary limit which is just larger than any limit we
// actually use in the application.
// TODO: The datasource should pass this in the query.
$limit = 15;
$user_table = new PhabricatorUser();
$conn_r = $user_table->establishConnection('r');
$ids = queryfx_all(
$conn_r,
'SELECT id FROM %T WHERE username LIKE %>
ORDER BY username ASC LIMIT %d',
$user_table->getTableName(),
$query,
$limit);
$ids = ipull($ids, 'id');
if (count($ids) < $limit) {
// If we didn't find enough username hits, look for real name hits.
// We need to pull the entire pagesize so that we end up with the
// right number of items if this query returns many duplicate IDs
// that we've already selected.
$realname_ids = queryfx_all(
$conn_r,
'SELECT DISTINCT userID FROM %T WHERE token LIKE %>
ORDER BY token ASC LIMIT %d',
PhabricatorUser::NAMETOKEN_TABLE,
$query,
$limit);
$realname_ids = ipull($realname_ids, 'userID');
$ids = array_merge($ids, $realname_ids);
$ids = array_unique($ids);
$ids = array_slice($ids, 0, $limit);
}
// Always add the logged-in user because some tokenizers autosort them
// first. They'll be filtered out on the client side if they don't
// match the query.
$ids[] = $request->getUser()->getID();
if ($ids) {
$users = id(new PhabricatorUser())->loadColumnsWhere(
$columns,
'id IN (%Ld)',
$ids);
} else {
$users = array();
}
} else {
$users = id(new PhabricatorUser())->loadColumns($columns);
}
if ($need_rich_data) {
$phids = mpull($users, 'getPHID');
$handles = $this->loadViewerHandles($phids);
}
foreach ($users as $user) {
if (!$need_all_users) {
if (!$need_agents && $user->getIsSystemAgent()) {
continue;
}
if ($user->getIsDisabled()) {
continue;
}
}
$result = id(new PhabricatorTypeaheadResult())
->setName($user->getFullName())
->setURI('/p/'.$user->getUsername())
->setPHID($user->getPHID())
->setPriorityString($user->getUsername());
if ($need_rich_data) {
$display_type = 'User';
if ($user->getIsAdmin()) {
$display_type = 'Administrator';
}
$result->setDisplayType($display_type);
$result->setImageURI($handles[$user->getPHID()]->getImageURI());
$result->setPriorityType('user');
}
$results[] = $result;
}
}
if ($need_lists) {
$lists = id(new PhabricatorMailingListQuery())
->setViewer($viewer)
->execute();
foreach ($lists as $list) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($list->getName())
->setURI($list->getURI())
->setPHID($list->getPHID());
}
}
if ($need_projs) {
$projs = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withStatus(PhabricatorProjectQuery::STATUS_OPEN)
->execute();
foreach ($projs as $proj) {
$proj_result = id(new PhabricatorTypeaheadResult())
->setName($proj->getName())
->setDisplayType("Project")
->setURI('/project/view/'.$proj->getID().'/')
->setPHID($proj->getPHID());
$prof = $proj->loadProfile();
if ($prof) {
$proj_result->setImageURI($prof->loadProfileImageURI());
}
$results[] = $proj_result;
}
}
if ($need_repos) {
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->execute();
foreach ($repos as $repo) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName('r'.$repo->getCallsign().' ('.$repo->getName().')')
->setURI('/diffusion/'.$repo->getCallsign().'/')
->setPHID($repo->getPHID())
->setPriorityString('r'.$repo->getCallsign());
}
}
if ($need_packages) {
$packages = id(new PhabricatorOwnersPackage())->loadAll();
foreach ($packages as $package) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($package->getName())
->setURI('/owners/package/'.$package->getID().'/')
->setPHID($package->getPHID());
}
}
if ($need_arcanist_projects) {
$arcprojs = id(new PhabricatorRepositoryArcanistProject())->loadAll();
foreach ($arcprojs as $proj) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($proj->getName())
->setPHID($proj->getPHID());
}
}
if ($need_applications) {
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
$uri = $application->getTypeaheadURI();
if (!$uri) {
continue;
}
$name = $application->getName().' '.$application->getShortDescription();
$results[] = id(new PhabricatorTypeaheadResult())
->setName($name)
->setURI($uri)
->setPHID($application->getPHID())
->setPriorityString($application->getName())
->setDisplayName($application->getName())
->setDisplayType($application->getShortDescription())
->setImageuRI($application->getIconURI())
->setPriorityType('apps');
}
}
if ($need_symbols) {
$symbols = id(new DiffusionSymbolQuery())
->setNamePrefix($query)
->setLimit(15)
->needArcanistProjects(true)
->needRepositories(true)
->needPaths(true)
->execute();
foreach ($symbols as $symbol) {
$lang = $symbol->getSymbolLanguage();
$name = $symbol->getSymbolName();
$type = $symbol->getSymbolType();
$proj = $symbol->getArcanistProject()->getName();
$results[] = id(new PhabricatorTypeaheadResult())
->setName($name)
->setURI($symbol->getURI())
->setPHID(md5($symbol->getURI())) // Just needs to be unique.
->setDisplayName($name)
->setDisplayType(strtoupper($lang).' '.ucwords($type).' ('.$proj.')')
->setPriorityType('symb');
}
}
if ($need_jump_objects) {
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($raw_query))
->execute();
if ($objects) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(mpull($objects, 'getPHID'))
->execute();
$handle = head($handles);
if ($handle) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($handle->getFullName())
->setDisplayType($handle->getTypeName())
->setURI($handle->getURI())
->setPHID($handle->getPHID())
->setPriorityType('jump');
}
}
}
$content = mpull($results, 'getWireFormat');
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent($content);
}
// If there's a non-Ajax request to this endpoint, show results in a tabular
// format to make it easier to debug typeahead output.
$rows = array();
foreach ($results as $result) {
$wire = $result->getWireFormat();
$rows[] = $wire;
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Name',
'URI',
'PHID',
'Priority',
'Display Name',
'Display Type',
'Image URI',
'Priority Type',
));
$panel = new AphrontPanelView();
$panel->setHeader('Typeahead Results');
$panel->appendChild($table);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Typeahead Results',
));
}
}
diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php
index f8670c9dbe..c9a56f96c4 100644
--- a/src/view/form/control/AphrontFormTokenizerControl.php
+++ b/src/view/form/control/AphrontFormTokenizerControl.php
@@ -1,112 +1,113 @@
<?php
final class AphrontFormTokenizerControl extends AphrontFormControl {
private $datasource;
private $disableBehavior;
private $limit;
private $placeholder;
public function setDatasource($datasource) {
$this->datasource = $datasource;
return $this;
}
public function setDisableBehavior($disable) {
$this->disableBehavior = $disable;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-tokenizer';
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setPlaceholder($placeholder) {
$this->placeholder = $placeholder;
return $this;
}
protected function renderInput() {
$name = $this->getName();
$values = nonempty($this->getValue(), array());
// TODO: Convert tokenizers to always take raw handles. For now, we
// accept either a list of handles or a `map<phid, string>`.
try {
assert_instances_of($values, 'PhabricatorObjectHandle');
$values = mpull($values, 'getFullName', 'getPHID');
} catch (InvalidArgumentException $ex) {
// Ignore this, just use the values as provided.
}
if ($this->getID()) {
$id = $this->getID();
} else {
$id = celerity_generate_unique_node_id();
}
if (!$this->placeholder) {
$this->placeholder = $this->getDefaultPlaceholder();
}
$template = new AphrontTokenizerTemplateView();
$template->setName($name);
$template->setID($id);
$template->setValue($values);
$username = null;
if ($this->user) {
$username = $this->user->getUsername();
}
if (!$this->disableBehavior) {
Javelin::initBehavior('aphront-basic-tokenizer', array(
'id' => $id,
'src' => $this->datasource,
'value' => $values,
'limit' => $this->limit,
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'username' => $username,
'placeholder' => $this->placeholder,
));
}
return $template->render();
}
private function getDefaultPlaceholder() {
$datasource = $this->datasource;
$matches = null;
if (!preg_match('@^/typeahead/common/(.*)/$@', $datasource, $matches)) {
return null;
}
$request = $matches[1];
$map = array(
'users' => pht('Type a user name...'),
'authors' => pht('Type a user name...'),
'usersorprojects' => pht('Type a user or project name...'),
'searchowner' => pht('Type a user name...'),
'accounts' => pht('Type a user name...'),
'mailable' => pht('Type a user or mailing list...'),
'allmailable' => pht('Type a user or mailing list...'),
'searchproject' => pht('Type a project name...'),
'projects' => pht('Type a project name...'),
'repositories' => pht('Type a repository name...'),
'packages' => pht('Type a package name...'),
'arcanistproject' => pht('Type an arc project name...'),
+ 'accountsorprojects' => pht('Type a user or project name...'),
);
return idx($map, $request);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Nov 6, 2:22 AM (17 h, 22 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
321390
Default Alt Text
(68 KB)

Event Timeline