
Summary: Ref T13546. Companion change to D21372. Move URI normalization code to Arcanist to we can more-often resolve remote URIs correctly. Test Plan: Grepped for affected symbols. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21373
707 lines
18 KiB
PHP
707 lines
18 KiB
PHP
<?php
|
|
|
|
final class PhabricatorRepositoryQuery
|
|
extends PhabricatorCursorPagedPolicyAwareQuery {
|
|
|
|
private $ids;
|
|
private $phids;
|
|
private $callsigns;
|
|
private $types;
|
|
private $uuids;
|
|
private $uris;
|
|
private $datasourceQuery;
|
|
private $slugs;
|
|
private $almanacServicePHIDs;
|
|
|
|
private $numericIdentifiers;
|
|
private $callsignIdentifiers;
|
|
private $phidIdentifiers;
|
|
private $monogramIdentifiers;
|
|
private $slugIdentifiers;
|
|
|
|
private $identifierMap;
|
|
|
|
const STATUS_OPEN = 'status-open';
|
|
const STATUS_CLOSED = 'status-closed';
|
|
const STATUS_ALL = 'status-all';
|
|
private $status = self::STATUS_ALL;
|
|
|
|
const HOSTED_PHABRICATOR = 'hosted-phab';
|
|
const HOSTED_REMOTE = 'hosted-remote';
|
|
const HOSTED_ALL = 'hosted-all';
|
|
private $hosted = self::HOSTED_ALL;
|
|
|
|
private $needMostRecentCommits;
|
|
private $needCommitCounts;
|
|
private $needProjectPHIDs;
|
|
private $needURIs;
|
|
private $needProfileImage;
|
|
|
|
public function withIDs(array $ids) {
|
|
$this->ids = $ids;
|
|
return $this;
|
|
}
|
|
|
|
public function withPHIDs(array $phids) {
|
|
$this->phids = $phids;
|
|
return $this;
|
|
}
|
|
|
|
public function withCallsigns(array $callsigns) {
|
|
$this->callsigns = $callsigns;
|
|
return $this;
|
|
}
|
|
|
|
public function withIdentifiers(array $identifiers) {
|
|
$identifiers = array_fuse($identifiers);
|
|
|
|
$ids = array();
|
|
$callsigns = array();
|
|
$phids = array();
|
|
$monograms = array();
|
|
$slugs = array();
|
|
|
|
foreach ($identifiers as $identifier) {
|
|
if (ctype_digit((string)$identifier)) {
|
|
$ids[$identifier] = $identifier;
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^(r[A-Z]+|R[1-9]\d*)\z/', $identifier)) {
|
|
$monograms[$identifier] = $identifier;
|
|
continue;
|
|
}
|
|
|
|
$repository_type = PhabricatorRepositoryRepositoryPHIDType::TYPECONST;
|
|
if (phid_get_type($identifier) === $repository_type) {
|
|
$phids[$identifier] = $identifier;
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^[A-Z]+\z/', $identifier)) {
|
|
$callsigns[$identifier] = $identifier;
|
|
continue;
|
|
}
|
|
|
|
$slugs[$identifier] = $identifier;
|
|
}
|
|
|
|
$this->numericIdentifiers = $ids;
|
|
$this->callsignIdentifiers = $callsigns;
|
|
$this->phidIdentifiers = $phids;
|
|
$this->monogramIdentifiers = $monograms;
|
|
$this->slugIdentifiers = $slugs;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function withStatus($status) {
|
|
$this->status = $status;
|
|
return $this;
|
|
}
|
|
|
|
public function withHosted($hosted) {
|
|
$this->hosted = $hosted;
|
|
return $this;
|
|
}
|
|
|
|
public function withTypes(array $types) {
|
|
$this->types = $types;
|
|
return $this;
|
|
}
|
|
|
|
public function withUUIDs(array $uuids) {
|
|
$this->uuids = $uuids;
|
|
return $this;
|
|
}
|
|
|
|
public function withURIs(array $uris) {
|
|
$this->uris = $uris;
|
|
return $this;
|
|
}
|
|
|
|
public function withDatasourceQuery($query) {
|
|
$this->datasourceQuery = $query;
|
|
return $this;
|
|
}
|
|
|
|
public function withSlugs(array $slugs) {
|
|
$this->slugs = $slugs;
|
|
return $this;
|
|
}
|
|
|
|
public function withAlmanacServicePHIDs(array $phids) {
|
|
$this->almanacServicePHIDs = $phids;
|
|
return $this;
|
|
}
|
|
|
|
public function needCommitCounts($need_counts) {
|
|
$this->needCommitCounts = $need_counts;
|
|
return $this;
|
|
}
|
|
|
|
public function needMostRecentCommits($need_commits) {
|
|
$this->needMostRecentCommits = $need_commits;
|
|
return $this;
|
|
}
|
|
|
|
public function needProjectPHIDs($need_phids) {
|
|
$this->needProjectPHIDs = $need_phids;
|
|
return $this;
|
|
}
|
|
|
|
public function needURIs($need_uris) {
|
|
$this->needURIs = $need_uris;
|
|
return $this;
|
|
}
|
|
|
|
public function needProfileImage($need) {
|
|
$this->needProfileImage = $need;
|
|
return $this;
|
|
}
|
|
|
|
public function getBuiltinOrders() {
|
|
return array(
|
|
'committed' => array(
|
|
'vector' => array('committed', 'id'),
|
|
'name' => pht('Most Recent Commit'),
|
|
),
|
|
'name' => array(
|
|
'vector' => array('name', 'id'),
|
|
'name' => pht('Name'),
|
|
),
|
|
'callsign' => array(
|
|
'vector' => array('callsign'),
|
|
'name' => pht('Callsign'),
|
|
),
|
|
'size' => array(
|
|
'vector' => array('size', 'id'),
|
|
'name' => pht('Size'),
|
|
),
|
|
) + parent::getBuiltinOrders();
|
|
}
|
|
|
|
public function getIdentifierMap() {
|
|
if ($this->identifierMap === null) {
|
|
throw new PhutilInvalidStateException('execute');
|
|
}
|
|
return $this->identifierMap;
|
|
}
|
|
|
|
protected function willExecute() {
|
|
$this->identifierMap = array();
|
|
}
|
|
|
|
public function newResultObject() {
|
|
return new PhabricatorRepository();
|
|
}
|
|
|
|
protected function loadPage() {
|
|
$table = $this->newResultObject();
|
|
$data = $this->loadStandardPageRows($table);
|
|
$repositories = $table->loadAllFromArray($data);
|
|
|
|
if ($this->needCommitCounts) {
|
|
$sizes = ipull($data, 'size', 'id');
|
|
foreach ($repositories as $id => $repository) {
|
|
$repository->attachCommitCount(nonempty($sizes[$id], 0));
|
|
}
|
|
}
|
|
|
|
if ($this->needMostRecentCommits) {
|
|
$commit_ids = ipull($data, 'lastCommitID', 'id');
|
|
$commit_ids = array_filter($commit_ids);
|
|
if ($commit_ids) {
|
|
$commits = id(new DiffusionCommitQuery())
|
|
->setViewer($this->getViewer())
|
|
->withIDs($commit_ids)
|
|
->needCommitData(true)
|
|
->needIdentities(true)
|
|
->execute();
|
|
} else {
|
|
$commits = array();
|
|
}
|
|
foreach ($repositories as $id => $repository) {
|
|
$commit = null;
|
|
if (idx($commit_ids, $id)) {
|
|
$commit = idx($commits, $commit_ids[$id]);
|
|
}
|
|
$repository->attachMostRecentCommit($commit);
|
|
}
|
|
}
|
|
|
|
return $repositories;
|
|
}
|
|
|
|
protected function willFilterPage(array $repositories) {
|
|
assert_instances_of($repositories, 'PhabricatorRepository');
|
|
|
|
// TODO: Denormalize repository status into the PhabricatorRepository
|
|
// table so we can do this filtering in the database.
|
|
foreach ($repositories as $key => $repo) {
|
|
$status = $this->status;
|
|
switch ($status) {
|
|
case self::STATUS_OPEN:
|
|
if (!$repo->isTracked()) {
|
|
unset($repositories[$key]);
|
|
}
|
|
break;
|
|
case self::STATUS_CLOSED:
|
|
if ($repo->isTracked()) {
|
|
unset($repositories[$key]);
|
|
}
|
|
break;
|
|
case self::STATUS_ALL:
|
|
break;
|
|
default:
|
|
throw new Exception("Unknown status '{$status}'!");
|
|
}
|
|
|
|
// TODO: This should also be denormalized.
|
|
$hosted = $this->hosted;
|
|
switch ($hosted) {
|
|
case self::HOSTED_PHABRICATOR:
|
|
if (!$repo->isHosted()) {
|
|
unset($repositories[$key]);
|
|
}
|
|
break;
|
|
case self::HOSTED_REMOTE:
|
|
if ($repo->isHosted()) {
|
|
unset($repositories[$key]);
|
|
}
|
|
break;
|
|
case self::HOSTED_ALL:
|
|
break;
|
|
default:
|
|
throw new Exception(pht("Unknown hosted failed '%s'!", $hosted));
|
|
}
|
|
}
|
|
|
|
// Build the identifierMap
|
|
if ($this->numericIdentifiers) {
|
|
foreach ($this->numericIdentifiers as $id) {
|
|
if (isset($repositories[$id])) {
|
|
$this->identifierMap[$id] = $repositories[$id];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->callsignIdentifiers) {
|
|
$repository_callsigns = mpull($repositories, null, 'getCallsign');
|
|
|
|
foreach ($this->callsignIdentifiers as $callsign) {
|
|
if (isset($repository_callsigns[$callsign])) {
|
|
$this->identifierMap[$callsign] = $repository_callsigns[$callsign];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->phidIdentifiers) {
|
|
$repository_phids = mpull($repositories, null, 'getPHID');
|
|
|
|
foreach ($this->phidIdentifiers as $phid) {
|
|
if (isset($repository_phids[$phid])) {
|
|
$this->identifierMap[$phid] = $repository_phids[$phid];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->monogramIdentifiers) {
|
|
$monogram_map = array();
|
|
foreach ($repositories as $repository) {
|
|
foreach ($repository->getAllMonograms() as $monogram) {
|
|
$monogram_map[$monogram] = $repository;
|
|
}
|
|
}
|
|
|
|
foreach ($this->monogramIdentifiers as $monogram) {
|
|
if (isset($monogram_map[$monogram])) {
|
|
$this->identifierMap[$monogram] = $monogram_map[$monogram];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->slugIdentifiers) {
|
|
$slug_map = array();
|
|
foreach ($repositories as $repository) {
|
|
$slug = $repository->getRepositorySlug();
|
|
if ($slug === null) {
|
|
continue;
|
|
}
|
|
|
|
$normal = phutil_utf8_strtolower($slug);
|
|
$slug_map[$normal] = $repository;
|
|
}
|
|
|
|
foreach ($this->slugIdentifiers as $slug) {
|
|
$normal = phutil_utf8_strtolower($slug);
|
|
if (isset($slug_map[$normal])) {
|
|
$this->identifierMap[$slug] = $slug_map[$normal];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $repositories;
|
|
}
|
|
|
|
protected function didFilterPage(array $repositories) {
|
|
if ($this->needProjectPHIDs) {
|
|
$type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
|
|
|
|
$edge_query = id(new PhabricatorEdgeQuery())
|
|
->withSourcePHIDs(mpull($repositories, 'getPHID'))
|
|
->withEdgeTypes(array($type_project));
|
|
$edge_query->execute();
|
|
|
|
foreach ($repositories as $repository) {
|
|
$project_phids = $edge_query->getDestinationPHIDs(
|
|
array(
|
|
$repository->getPHID(),
|
|
));
|
|
$repository->attachProjectPHIDs($project_phids);
|
|
}
|
|
}
|
|
|
|
$viewer = $this->getViewer();
|
|
|
|
if ($this->needURIs) {
|
|
$uris = id(new PhabricatorRepositoryURIQuery())
|
|
->setViewer($viewer)
|
|
->withRepositories($repositories)
|
|
->execute();
|
|
$uri_groups = mgroup($uris, 'getRepositoryPHID');
|
|
foreach ($repositories as $repository) {
|
|
$repository_uris = idx($uri_groups, $repository->getPHID(), array());
|
|
$repository->attachURIs($repository_uris);
|
|
}
|
|
}
|
|
|
|
if ($this->needProfileImage) {
|
|
$default = null;
|
|
|
|
$file_phids = mpull($repositories, 'getProfileImagePHID');
|
|
$file_phids = array_filter($file_phids);
|
|
if ($file_phids) {
|
|
$files = id(new PhabricatorFileQuery())
|
|
->setParentQuery($this)
|
|
->setViewer($this->getViewer())
|
|
->withPHIDs($file_phids)
|
|
->execute();
|
|
$files = mpull($files, null, 'getPHID');
|
|
} else {
|
|
$files = array();
|
|
}
|
|
|
|
foreach ($repositories as $repository) {
|
|
$file = idx($files, $repository->getProfileImagePHID());
|
|
if (!$file) {
|
|
if (!$default) {
|
|
$default = PhabricatorFile::loadBuiltin(
|
|
$this->getViewer(),
|
|
'repo/code.png');
|
|
}
|
|
$file = $default;
|
|
}
|
|
$repository->attachProfileImageFile($file);
|
|
}
|
|
}
|
|
|
|
return $repositories;
|
|
}
|
|
|
|
protected function getPrimaryTableAlias() {
|
|
return 'r';
|
|
}
|
|
|
|
public function getOrderableColumns() {
|
|
return parent::getOrderableColumns() + array(
|
|
'committed' => array(
|
|
'table' => 's',
|
|
'column' => 'epoch',
|
|
'type' => 'int',
|
|
'null' => 'tail',
|
|
),
|
|
'callsign' => array(
|
|
'table' => 'r',
|
|
'column' => 'callsign',
|
|
'type' => 'string',
|
|
'unique' => true,
|
|
'reverse' => true,
|
|
'null' => 'tail',
|
|
),
|
|
'name' => array(
|
|
'table' => 'r',
|
|
'column' => 'name',
|
|
'type' => 'string',
|
|
'reverse' => true,
|
|
),
|
|
'size' => array(
|
|
'table' => 's',
|
|
'column' => 'size',
|
|
'type' => 'int',
|
|
'null' => 'tail',
|
|
),
|
|
);
|
|
}
|
|
|
|
protected function newPagingMapFromCursorObject(
|
|
PhabricatorQueryCursor $cursor,
|
|
array $keys) {
|
|
|
|
$repository = $cursor->getObject();
|
|
|
|
$map = array(
|
|
'id' => (int)$repository->getID(),
|
|
'callsign' => $repository->getCallsign(),
|
|
'name' => $repository->getName(),
|
|
);
|
|
|
|
if (isset($keys['committed'])) {
|
|
$map['committed'] = $cursor->getRawRowProperty('epoch');
|
|
}
|
|
|
|
if (isset($keys['size'])) {
|
|
$map['size'] = $cursor->getRawRowProperty('size');
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
|
|
$parts = parent::buildSelectClauseParts($conn);
|
|
|
|
if ($this->shouldJoinSummaryTable()) {
|
|
$parts[] = qsprintf($conn, 's.*');
|
|
}
|
|
|
|
return $parts;
|
|
}
|
|
|
|
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
|
|
$joins = parent::buildJoinClauseParts($conn);
|
|
|
|
if ($this->shouldJoinSummaryTable()) {
|
|
$joins[] = qsprintf(
|
|
$conn,
|
|
'LEFT JOIN %T s ON r.id = s.repositoryID',
|
|
PhabricatorRepository::TABLE_SUMMARY);
|
|
}
|
|
|
|
if ($this->shouldJoinURITable()) {
|
|
$joins[] = qsprintf(
|
|
$conn,
|
|
'LEFT JOIN %R uri ON r.phid = uri.repositoryPHID',
|
|
new PhabricatorRepositoryURIIndex());
|
|
}
|
|
|
|
return $joins;
|
|
}
|
|
|
|
protected function shouldGroupQueryResultRows() {
|
|
if ($this->shouldJoinURITable()) {
|
|
return true;
|
|
}
|
|
|
|
return parent::shouldGroupQueryResultRows();
|
|
}
|
|
|
|
private function shouldJoinURITable() {
|
|
return ($this->uris !== null);
|
|
}
|
|
|
|
private function shouldJoinSummaryTable() {
|
|
if ($this->needCommitCounts) {
|
|
return true;
|
|
}
|
|
|
|
if ($this->needMostRecentCommits) {
|
|
return true;
|
|
}
|
|
|
|
$vector = $this->getOrderVector();
|
|
if ($vector->containsKey('committed')) {
|
|
return true;
|
|
}
|
|
|
|
if ($vector->containsKey('size')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
|
$where = parent::buildWhereClauseParts($conn);
|
|
|
|
if ($this->ids !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'r.id IN (%Ld)',
|
|
$this->ids);
|
|
}
|
|
|
|
if ($this->phids !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'r.phid IN (%Ls)',
|
|
$this->phids);
|
|
}
|
|
|
|
if ($this->callsigns !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'r.callsign IN (%Ls)',
|
|
$this->callsigns);
|
|
}
|
|
|
|
if ($this->numericIdentifiers ||
|
|
$this->callsignIdentifiers ||
|
|
$this->phidIdentifiers ||
|
|
$this->monogramIdentifiers ||
|
|
$this->slugIdentifiers) {
|
|
$identifier_clause = array();
|
|
|
|
if ($this->numericIdentifiers) {
|
|
$identifier_clause[] = qsprintf(
|
|
$conn,
|
|
'r.id IN (%Ld)',
|
|
$this->numericIdentifiers);
|
|
}
|
|
|
|
if ($this->callsignIdentifiers) {
|
|
$identifier_clause[] = qsprintf(
|
|
$conn,
|
|
'r.callsign IN (%Ls)',
|
|
$this->callsignIdentifiers);
|
|
}
|
|
|
|
if ($this->phidIdentifiers) {
|
|
$identifier_clause[] = qsprintf(
|
|
$conn,
|
|
'r.phid IN (%Ls)',
|
|
$this->phidIdentifiers);
|
|
}
|
|
|
|
if ($this->monogramIdentifiers) {
|
|
$monogram_callsigns = array();
|
|
$monogram_ids = array();
|
|
|
|
foreach ($this->monogramIdentifiers as $identifier) {
|
|
if ($identifier[0] == 'r') {
|
|
$monogram_callsigns[] = substr($identifier, 1);
|
|
} else {
|
|
$monogram_ids[] = substr($identifier, 1);
|
|
}
|
|
}
|
|
|
|
if ($monogram_ids) {
|
|
$identifier_clause[] = qsprintf(
|
|
$conn,
|
|
'r.id IN (%Ld)',
|
|
$monogram_ids);
|
|
}
|
|
|
|
if ($monogram_callsigns) {
|
|
$identifier_clause[] = qsprintf(
|
|
$conn,
|
|
'r.callsign IN (%Ls)',
|
|
$monogram_callsigns);
|
|
}
|
|
}
|
|
|
|
if ($this->slugIdentifiers) {
|
|
$identifier_clause[] = qsprintf(
|
|
$conn,
|
|
'r.repositorySlug IN (%Ls)',
|
|
$this->slugIdentifiers);
|
|
}
|
|
|
|
$where[] = qsprintf($conn, '%LO', $identifier_clause);
|
|
}
|
|
|
|
if ($this->types) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'r.versionControlSystem IN (%Ls)',
|
|
$this->types);
|
|
}
|
|
|
|
if ($this->uuids) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'r.uuid IN (%Ls)',
|
|
$this->uuids);
|
|
}
|
|
|
|
if (strlen($this->datasourceQuery)) {
|
|
// This handles having "rP" match callsigns starting with "P...".
|
|
$query = trim($this->datasourceQuery);
|
|
if (preg_match('/^r/', $query)) {
|
|
$callsign = substr($query, 1);
|
|
} else {
|
|
$callsign = $query;
|
|
}
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'r.name LIKE %> OR r.callsign LIKE %> OR r.repositorySlug LIKE %>',
|
|
$query,
|
|
$callsign,
|
|
$query);
|
|
}
|
|
|
|
if ($this->slugs !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'r.repositorySlug IN (%Ls)',
|
|
$this->slugs);
|
|
}
|
|
|
|
if ($this->uris !== null) {
|
|
$try_uris = $this->getNormalizedURIs();
|
|
$try_uris = array_fuse($try_uris);
|
|
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'uri.repositoryURI IN (%Ls)',
|
|
$try_uris);
|
|
}
|
|
|
|
if ($this->almanacServicePHIDs !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'r.almanacServicePHID IN (%Ls)',
|
|
$this->almanacServicePHIDs);
|
|
}
|
|
|
|
return $where;
|
|
}
|
|
|
|
public function getQueryApplicationClass() {
|
|
return 'PhabricatorDiffusionApplication';
|
|
}
|
|
|
|
private function getNormalizedURIs() {
|
|
$normalized_uris = array();
|
|
|
|
// Since we don't know which type of repository this URI is in the general
|
|
// case, just generate all the normalizations. We could refine this in some
|
|
// cases: if the query specifies VCS types, or the URI is a git-style URI
|
|
// or an `svn+ssh` URI, we could deduce how to normalize it. However, this
|
|
// would be more complicated and it's not clear if it matters in practice.
|
|
|
|
$domain_map = PhabricatorRepositoryURI::getURINormalizerDomainMap();
|
|
|
|
$types = ArcanistRepositoryURINormalizer::getAllURITypes();
|
|
foreach ($this->uris as $uri) {
|
|
foreach ($types as $type) {
|
|
$normalized_uri = new ArcanistRepositoryURINormalizer($type, $uri);
|
|
$normalized_uri->setDomainMap($domain_map);
|
|
$normalized_uris[] = $normalized_uri->getNormalizedURI();
|
|
}
|
|
}
|
|
|
|
return array_unique($normalized_uris);
|
|
}
|
|
|
|
}
|