Page MenuHomePhorge

No OneTemporary

diff --git a/src/applications/conduit/method/differential/query/ConduitAPI_differential_query_Method.php b/src/applications/conduit/method/differential/query/ConduitAPI_differential_query_Method.php
index d1cd3891a7..bc32a69a58 100644
--- a/src/applications/conduit/method/differential/query/ConduitAPI_differential_query_Method.php
+++ b/src/applications/conduit/method/differential/query/ConduitAPI_differential_query_Method.php
@@ -1,193 +1,195 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group conduit
*/
class ConduitAPI_differential_query_Method extends ConduitAPIMethod {
public function getMethodDescription() {
return "Query Differential revisions which match certain criteria.";
}
public function defineParamTypes() {
$hash_types = ArcanistDifferentialRevisionHash::getTypes();
$hash_types = implode(', ', $hash_types);
$status_types = array(
DifferentialRevisionQuery::STATUS_ANY,
DifferentialRevisionQuery::STATUS_OPEN,
+ DifferentialRevisionQuery::STATUS_ACCEPTED,
+ DifferentialRevisionQuery::STATUS_COMMITTED,
);
$status_types = implode(', ', $status_types);
$order_types = array(
DifferentialRevisionQuery::ORDER_MODIFIED,
DifferentialRevisionQuery::ORDER_CREATED,
);
$order_types = implode(', ', $order_types);
return array(
'authors' => 'optional list<phid>',
'ccs' => 'optional list<phid>',
'reviewers' => 'optional list<phid>',
// TODO: Implement this, it needs to accept a repository ID in addition
// to a path so the signature needs to be a little more complicated.
// 'paths' => 'optional list<pair<...>>',
'commitHashes' => 'optional list<pair<enum<'.
$hash_types.'>, string>>',
'status' => 'optional enum<'.$status_types.'>',
'order' => 'optional enum<'.$order_types.'>',
'limit' => 'optional uint',
'offset' => 'optional uint',
'ids' => 'optional list<uint>',
'phids' => 'optional list<phid>',
'subscribers' => 'optional list<phid>',
'responsibleUsers' => 'optional list<phid>',
'branches' => 'optional list<string>',
);
}
public function defineReturnType() {
return 'list<dict>';
}
public function defineErrorTypes() {
return array(
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
);
}
protected function execute(ConduitAPIRequest $request) {
$authors = $request->getValue('authors');
$ccs = $request->getValue('ccs');
$reviewers = $request->getValue('reviewers');
$status = $request->getValue('status');
$order = $request->getValue('order');
$commit_hashes = $request->getValue('commitHashes');
$limit = $request->getValue('limit');
$offset = $request->getValue('offset');
$ids = $request->getValue('ids');
$phids = $request->getValue('phids');
$subscribers = $request->getValue('subscribers');
$responsible_users = $request->getValue('responsibleUsers');
$branches = $request->getValue('branches');
$query = new DifferentialRevisionQuery();
if ($authors) {
$query->withAuthors($authors);
}
if ($ccs) {
$query->withCCs($ccs);
}
if ($reviewers) {
$query->withReviewers($reviewers);
}
/* TODO: Implement.
$paths = $request->getValue('paths');
if ($paths) {
foreach ($paths as $path) {
// (Lookup the repository IDs.)
$query->withPath($repository_id, $path);
}
}
*/
if ($commit_hashes) {
$hash_types = ArcanistDifferentialRevisionHash::getTypes();
foreach ($commit_hashes as $info) {
list($type, $hash) = $info;
if (empty($type) ||
!in_array($type, $hash_types) ||
empty($hash)) {
throw new ConduitException('ERR-INVALID-PARAMETER');
}
}
$query->withCommitHashes($commit_hashes);
}
if ($status) {
$query->withStatus($status);
}
if ($order) {
$query->setOrder($order);
}
if ($limit) {
$query->setLimit($limit);
}
if ($offset) {
$query->setOffset($offset);
}
if ($ids) {
$query->withIDs($ids);
}
if ($phids) {
$query->withPHIDs($phids);
}
if ($responsible_users) {
$query->withResponsibleUsers($responsible_users);
}
if ($subscribers) {
$query->withSubscribers($subscribers);
}
if ($branches) {
$query->withBranches($branches);
}
$query->needRelationships(true);
$query->needCommitPHIDs(true);
$query->needDiffIDs(true);
$query->needActiveDiffs(true);
$revisions = $query->execute();
$results = array();
foreach ($revisions as $revision) {
$diff = $revision->getActiveDiff();
if (!$diff) {
continue;
}
$id = $revision->getID();
$results[] = array(
'id' => $id,
'phid' => $revision->getPHID(),
'title' => $revision->getTitle(),
'uri' => PhabricatorEnv::getProductionURI('/D'.$id),
'dateCreated' => $revision->getDateCreated(),
'dateModified' => $revision->getDateModified(),
'authorPHID' => $revision->getAuthorPHID(),
'status' => $revision->getStatus(),
'statusName' =>
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$revision->getStatus()),
'sourcePath' => $diff->getSourcePath(),
'branch' => $diff->getBranch(),
'summary' => $revision->getSummary(),
'testPlan' => $revision->getTestPlan(),
'lineCount' => $revision->getLineCount(),
'diffs' => $revision->getDiffIDs(),
'commits' => $revision->getCommitPHIDs(),
'reviewers' => array_values($revision->getReviewers()),
'ccs' => array_values($revision->getCCPHIDs()),
);
}
return $results;
}
}
diff --git a/src/applications/differential/query/revision/DifferentialRevisionQuery.php b/src/applications/differential/query/revision/DifferentialRevisionQuery.php
index 33e7b6d650..db5f01606c 100644
--- a/src/applications/differential/query/revision/DifferentialRevisionQuery.php
+++ b/src/applications/differential/query/revision/DifferentialRevisionQuery.php
@@ -1,780 +1,789 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 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 {
// TODO: Replace DifferentialRevisionListData with this class.
private $pathIDs = array();
- private $status = 'status-any';
- const STATUS_ANY = 'status-any';
- const STATUS_OPEN = 'status-open';
- const STATUS_COMMITTED = 'status-committed';
+ private $status = 'status-any';
+ const STATUS_ANY = 'status-any';
+ const STATUS_OPEN = 'status-open';
+ const STATUS_ACCEPTED = 'status-accepted';
+ const STATUS_COMMITTED = 'status-committed';
private $authors = 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 $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 $limit = 1000;
private $offset = 0;
private $needRelationships = false;
private $needActiveDiffs = false;
private $needDiffIDs = false;
private $needCommitPHIDs = false;
/* -( 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 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;
}
/**
* 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 result limit. If unspecified, defaults to 1000.
*
* @param int Result limit.
* @return this
* @task config
*/
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
/**
* Set result offset. If unspecified, defaults to 0.
*
* @param int Result offset.
* @return this
* @task config
*/
public function setOffset($offset) {
$this->offset = $offset;
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;
}
/* -( Query Execution )---------------------------------------------------- */
/**
* Execute the query as configured, returning matching
* @{class:DifferentialRevision} objects.
*
* @return list List of matching DifferentialRevision objects.
* @task exec
*/
public function execute() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
if ($this->shouldUseResponsibleFastPath()) {
$data = $this->loadDataUsingResponsibleFastPath();
} else {
$data = $this->loadData();
}
$revisions = $table->loadAllFromArray($data);
if ($revisions) {
if ($this->needRelationships) {
$this->loadRelationships($conn_r, $revisions);
}
if ($this->needCommitPHIDs) {
$this->loadCommitPHIDs($conn_r, $revisions);
}
$need_active = $this->needActiveDiffs ||
$this->branches;
$need_ids = $need_active ||
$this->needDiffIDs;
if ($need_ids) {
$this->loadDiffIDs($conn_r, $revisions);
}
if ($need_active) {
$this->loadActiveDiffs($conn_r, $revisions);
}
if ($this->branches) {
// TODO: We could filter this in SQL instead and might get better
// performance in some cases.
$branch_map = array_fill_keys($this->branches, true);
foreach ($revisions as $key => $revision) {
$diff = $revision->getActiveDiff();
if (!$diff) {
unset($revisions[$key]);
continue;
}
// TODO: Old arc uploaded the wrong branch name for Mercurial (i.e.,
// with a trailing "\n"). Once the arc version gets bumped, do a
// migration and remove this.
$branch = trim($diff->getBranch());
if (!$diff || empty($branch_map[$branch])) {
unset($revisions[$key]);
continue;
}
}
}
}
return $revisions;
}
/**
* Determine if we should execute an optimized, fast-path query to fetch
* open revisions for one responsible user. This is used by the Differential
* dashboard and much faster when executed as a UNION ALL than with JOIN
* and WHERE, which is why we special case it.
*/
private function shouldUseResponsibleFastPath() {
if ((count($this->responsibles) == 1) &&
($this->status == self::STATUS_OPEN) &&
($this->order == self::ORDER_MODIFIED) &&
!$this->offset &&
!$this->limit &&
!$this->subscribers &&
!$this->reviewers &&
!$this->ccs &&
!$this->authors &&
!$this->revIDs &&
!$this->commitHashes &&
!$this->phids) {
return true;
}
return false;
}
private function loadDataUsingResponsibleFastPath() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
$responsible_phid = reset($this->responsibles);
$open_statuses = array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::ACCEPTED,
);
return queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE authorPHID = %s AND status IN (%Ld)
UNION ALL
SELECT r.* FROM %T r JOIN %T rel
ON rel.revisionID = r.id
AND rel.relation = %s
AND rel.objectPHID = %s
WHERE r.status IN (%Ld) ORDER BY dateModified DESC',
$table->getTableName(),
$responsible_phid,
$open_statuses,
$table->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$responsible_phid,
$open_statuses);
}
private function loadData() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
$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);
$order_by = $this->buildOrderByClause($conn_r);
$limit = '';
if ($this->offset || $this->limit) {
$limit = qsprintf(
$conn_r,
'LIMIT %d, %d',
(int)$this->offset,
$this->limit);
}
return queryfx_all(
$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 reviewer_rel ON reviewer_rel.revisionID = r.id '.
'AND reviewer_rel.relation = %s '.
'AND reviewer_rel.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$this->reviewers);
}
if ($this->subscribers) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T sub_rel ON sub_rel.revisionID = r.id '.
'AND sub_rel.relation IN (%Ls) '.
'AND sub_rel.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
array(
DifferentialRevision::RELATION_SUBSCRIBED,
DifferentialRevision::RELATION_REVIEWER,
),
$this->subscribers);
}
if ($this->responsibles) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T responsibles_rel ON responsibles_rel.revisionID = r.id '.
'AND responsibles_rel.relation = %s '.
'AND responsibles_rel.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$this->responsibles);
}
$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,
'(repositoryID = %d AND pathID IN (%Ld))',
$repository_id,
ipull($paths, 'pathID'));
}
$path_clauses = '('.implode(' OR ', $path_clauses).')';
$where[] = $path_clauses;
}
if ($this->authors) {
$where[] = qsprintf(
$conn_r,
'authorPHID IN (%Ls)',
$this->authors);
}
if ($this->revIDs) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->revIDs);
}
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,
'phid IN (%Ls)',
$this->phids);
}
if ($this->responsibles) {
$where[] = qsprintf(
$conn_r,
'(responsibles_rel.objectPHID IS NOT NULL OR r.authorPHID IN (%Ls))',
$this->responsibles);
}
switch ($this->status) {
case self::STATUS_ANY:
break;
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::ACCEPTED,
));
break;
+ case self::STATUS_ACCEPTED:
+ $where[] = qsprintf(
+ $conn_r,
+ 'status IN (%Ld)',
+ array(
+ ArcanistDifferentialRevisionStatus::ACCEPTED,
+ ));
+ break;
case self::STATUS_COMMITTED:
$where[] = qsprintf(
$conn_r,
'status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::COMMITTED,
));
break;
default:
throw new Exception(
"Unknown revision status filter constant '{$this->status}'!");
}
if ($where) {
$where = 'WHERE '.implode(' AND ', $where);
} else {
$where = '';
}
return $where;
}
/**
* @task internal
*/
private function buildGroupByClause($conn_r) {
$join_triggers = array_merge(
$this->pathIDs,
$this->ccs,
$this->reviewers,
$this->subscribers,
$this->responsibles);
$needs_distinct = (count($join_triggers) > 1);
if ($needs_distinct) {
return 'GROUP BY r.id';
} else {
return '';
}
}
/**
* @task internal
*/
private function buildOrderByClause($conn_r) {
switch ($this->order) {
case self::ORDER_MODIFIED:
return 'ORDER BY r.dateModified DESC';
case self::ORDER_CREATED:
return 'ORDER BY r.dateCreated DESC';
case self::ORDER_PATH_MODIFIED:
if (!$this->pathIDs) {
throw new Exception(
"To use ORDER_PATH_MODIFIED, you must specify withPath().");
}
return 'ORDER BY p.epoch DESC';
default:
throw new Exception("Unknown query order constant '{$this->order}'.");
}
}
private function loadRelationships($conn_r, array $revisions) {
$relationships = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID in (%Ld) ORDER BY sequence',
DifferentialRevision::RELATIONSHIP_TABLE,
mpull($revisions, 'getID'));
$relationships = igroup($relationships, 'revisionID');
foreach ($revisions as $revision) {
$revision->attachRelationships(
idx(
$relationships,
$revision->getID(),
array()));
}
}
private function loadCommitPHIDs($conn_r, array $revisions) {
$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) {
$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) {
$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()));
}
}
}
diff --git a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
index 020703f8a4..f1d0cb8234 100644
--- a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
@@ -1,245 +1,245 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialChangesetListView extends AphrontView {
private $changesets = array();
private $references = array();
private $editable;
private $revision;
private $renderURI = '/differential/changeset/';
private $whitespace;
private $standaloneViews;
private $user;
private $symbolIndexes = array();
private $repository;
private $diff;
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function setEditable($editable) {
$this->editable = $editable;
return $this;
}
public function setStandaloneViews($has_standalone_views) {
$this->standaloneViews = $has_standalone_views;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setRevision(DifferentialRevision $revision) {
$this->revision = $revision;
return $this;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function setRenderingReferences(array $references) {
$this->references = $references;
return $this;
}
public function setSymbolIndexes(array $indexes) {
$this->symbolIndexes = $indexes;
return $this;
}
public function setRenderURI($render_uri) {
$this->renderURI = $render_uri;
return $this;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function render() {
require_celerity_resource('differential-changeset-view-css');
$changesets = $this->changesets;
if ($this->standaloneViews) {
Javelin::initBehavior(
'differential-dropdown-menus',
array());
}
$output = array();
$mapping = array();
$repository = $this->repository;
foreach ($changesets as $key => $changeset) {
$file = $changeset->getFilename();
$class = 'differential-changeset';
if (!$this->editable) {
$class .= ' differential-changeset-noneditable';
}
$ref = $this->references[$key];
$detail = new DifferentialChangesetDetailView();
$detail_button = null;
if ($this->standaloneViews) {
$detail_uri = new PhutilURI($this->renderURI);
$detail_uri->setQueryParams(
array(
'ref' => $ref,
'whitespace' => $this->whitespace,
));
$diffusion_uri = null;
if ($repository) {
$diffusion_uri = $repository->getDiffusionBrowseURIForPath(
$changeset->getAbsoluteRepositoryPath($this->diff, $repository));
}
$meta = array(
'detailURI' => (string)$detail_uri,
'diffusionURI' => $diffusion_uri,
'containerID' => $detail->getID(),
);
$change = $changeset->getChangeType();
if ($change != DifferentialChangeType::TYPE_ADD) {
$meta['leftURI'] = (string)$detail_uri->alter('view', 'old');
}
if ($change != DifferentialChangeType::TYPE_DELETE &&
$change != DifferentialChangeType::TYPE_MULTICOPY) {
$meta['rightURI'] = (string)$detail_uri->alter('view', 'new');
}
- if ($this->user) {
+ if ($this->user && $repository) {
$path = ltrim(
$changeset->getAbsoluteRepositoryPath($this->diff, $repository),
'/');
$line = 1; // TODO: get first changed line
$editor_link = $this->user->loadEditorLink($path, $line, $repository);
if ($editor_link) {
$meta['editor'] = $editor_link;
} else {
$meta['editorConfigure'] = '/settings/page/preferences/';
}
}
$detail_button = javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'meta' => $meta,
'href' => $detail_uri,
'target' => '_blank',
'sigil' => 'differential-view-options',
),
"View Options \xE2\x96\xBC");
}
$detail->setChangeset($changeset);
$detail->addButton($detail_button);
$detail->setSymbolIndex(idx($this->symbolIndexes, $key));
$uniq_id = celerity_generate_unique_node_id();
$detail->appendChild(
phutil_render_tag(
'div',
array(
'id' => $uniq_id,
),
'<div class="differential-loading">Loading...</div>'));
$output[] = $detail->render();
$mapping[$uniq_id] = $ref;
}
Javelin::initBehavior('differential-populate', array(
'registry' => $mapping,
'whitespace' => $this->whitespace,
'uri' => $this->renderURI,
));
Javelin::initBehavior('differential-show-more', array(
'uri' => $this->renderURI,
'whitespace' => $this->whitespace,
));
Javelin::initBehavior('differential-comment-jump', array());
if ($this->editable) {
$undo_templates = $this->renderUndoTemplates();
$revision = $this->revision;
Javelin::initBehavior('differential-edit-inline-comments', array(
'uri' => '/differential/comment/inline/edit/'.$revision->getID().'/',
'undo_templates' => $undo_templates,
));
}
return
'<div class="differential-review-stage" id="differential-review-stage">'.
implode("\n", $output).
'</div>';
}
/**
* Render the "Undo" markup for the inline comment undo feature.
*/
private function renderUndoTemplates() {
$link = javelin_render_tag(
'a',
array(
'href' => '#',
'sigil' => 'differential-inline-comment-undo',
),
'Undo');
$div = phutil_render_tag(
'div',
array(
'class' => 'differential-inline-undo',
),
'Changes discarded. '.$link);
$content = '<th></th><td>'.$div.'</td>';
$empty = '<th></th><td></td>';
$left = array($content, $empty);
$right = array($empty, $content);
return array(
'l' => '<table><tr>'.implode('', $left).'</tr></table>',
'r' => '<table><tr>'.implode('', $right).'</tr></table>',
);
}
}
diff --git a/src/applications/herald/engine/engine/HeraldEngine.php b/src/applications/herald/engine/engine/HeraldEngine.php
index b704e78542..9532a4862d 100644
--- a/src/applications/herald/engine/engine/HeraldEngine.php
+++ b/src/applications/herald/engine/engine/HeraldEngine.php
@@ -1,522 +1,520 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class HeraldEngine {
protected $rules = array();
protected $results = array();
protected $stack = array();
protected $activeRule = null;
protected $fieldCache = array();
protected $object = null;
public static function loadAndApplyRules(HeraldObjectAdapter $object) {
$content_type = $object->getHeraldTypeName();
$rules = HeraldRule::loadAllByContentTypeWithFullData(
$content_type,
$object->getPHID());
$engine = new HeraldEngine();
$effects = $engine->applyRules($rules, $object);
$engine->applyEffects($effects, $object, $rules);
return $engine->getTranscript();
}
public function applyRules(array $rules, HeraldObjectAdapter $object) {
$t_start = microtime(true);
$rules = mpull($rules, null, 'getID');
$this->transcript = new HeraldTranscript();
$this->transcript->setObjectPHID((string)$object->getPHID());
$this->fieldCache = array();
$this->results = array();
$this->rules = $rules;
$this->object = $object;
$effects = array();
foreach ($rules as $id => $rule) {
$this->stack = array();
try {
if (($rule->getRepetitionPolicy() ==
HeraldRepetitionPolicyConfig::FIRST) &&
$rule->getRuleApplied($object->getPHID())) {
// This rule is only supposed to be applied a single time, and it's
// aleady been applied, so this is an automatic failure.
$xscript = id(new HeraldRuleTranscript())
->setRuleID($id)
->setResult(false)
->setRuleName($rule->getName())
->setRuleOwner($rule->getAuthorPHID())
->setReason(
"This rule is only supposed to be repeated a single time, ".
"and it has already been applied."
);
$this->transcript->addRuleTranscript($xscript);
$rule_matches = false;
} else {
$rule_matches = $this->doesRuleMatch($rule, $object);
}
} catch (HeraldRecursiveConditionsException $ex) {
$names = array();
foreach ($this->stack as $rule_id => $ignored) {
$names[] = '"'.$rules[$rule_id]->getName().'"';
}
$names = implode(', ', $names);
foreach ($this->stack as $rule_id => $ignored) {
$xscript = new HeraldRuleTranscript();
$xscript->setRuleID($rule_id);
$xscript->setResult(false);
$xscript->setReason(
"Rules {$names} are recursively dependent upon one another! ".
"Don't do this! You have formed an unresolvable cycle in the ".
"dependency graph!");
$xscript->setRuleName($rules[$rule_id]->getName());
$xscript->setRuleOwner($rules[$rule_id]->getAuthorPHID());
$this->transcript->addRuleTranscript($xscript);
}
$rule_matches = false;
}
$this->results[$id] = $rule_matches;
if ($rule_matches) {
foreach ($this->getRuleEffects($rule, $object) as $effect) {
$effects[] = $effect;
}
}
}
$object_transcript = new HeraldObjectTranscript();
$object_transcript->setPHID($object->getPHID());
$object_transcript->setName($object->getHeraldName());
$object_transcript->setType($object->getHeraldTypeName());
$object_transcript->setFields($this->fieldCache);
$this->transcript->setObjectTranscript($object_transcript);
$t_end = microtime(true);
$this->transcript->setDuration($t_end - $t_start);
return $effects;
}
public function applyEffects(
array $effects,
HeraldObjectAdapter $object,
array $rules) {
$this->transcript->setDryRun($object instanceof HeraldDryRunAdapter);
$xscripts = $object->applyHeraldEffects($effects);
foreach ($xscripts as $apply_xscript) {
if (!($apply_xscript instanceof HeraldApplyTranscript)) {
throw new Exception(
"Heraldable must return HeraldApplyTranscripts from ".
"applyHeraldEffect().");
}
$this->transcript->addApplyTranscript($apply_xscript);
}
if (!$this->transcript->getDryRun()) {
$rules = mpull($rules, null, 'getID');
$applied_ids = array();
$first_policy = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::FIRST);
// Mark all the rules that have had their effects applied as having been
// executed for the current object.
$rule_ids = mpull($xscripts, 'getRuleID');
foreach ($rule_ids as $rule_id) {
if (!$rule_id) {
// Some apply transcripts are purely informational and not associated
// with a rule, e.g. carryover emails from earlier revisions.
continue;
}
$rule = idx($rules, $rule_id);
if (!$rule) {
continue;
}
if ($rule->getRepetitionPolicy() == $first_policy) {
$applied_ids[] = $rule_id;
}
}
if ($applied_ids) {
$conn_w = id(new HeraldRule())->establishConnection('w');
$sql = array();
foreach ($applied_ids as $id) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d)',
$object->getPHID(),
$id);
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
HeraldRule::TABLE_RULE_APPLIED,
implode(', ', $sql));
}
}
-
- die("DERP");
}
public function getTranscript() {
$this->transcript->save();
return $this->transcript;
}
protected function doesRuleMatch(
HeraldRule $rule,
HeraldObjectAdapter $object) {
$id = $rule->getID();
if (isset($this->results[$id])) {
// If we've already evaluated this rule because another rule depends
// on it, we don't need to reevaluate it.
return $this->results[$id];
}
if (isset($this->stack[$id])) {
// We've recursed, fail all of the rules on the stack. This happens when
// there's a dependency cycle with "Rule conditions match for rule ..."
// conditions.
foreach ($this->stack as $rule_id => $ignored) {
$this->results[$rule_id] = false;
}
throw new HeraldRecursiveConditionsException();
}
$this->stack[$id] = true;
$all = $rule->getMustMatchAll();
$conditions = $rule->getConditions();
$result = null;
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
$reason = "Rule could not be processed, it was created with a newer ".
"version of Herald.";
$result = false;
} else if (!$conditions) {
$reason = "Rule failed automatically because it has no conditions.";
$result = false;
/* TOOD: Restore this in some form?
} else if (!is_fb_employee($rule->getAuthorPHID())) {
$reason = "Rule failed automatically because its owner is not an ".
"active employee.";
$result = false;
*/
} else {
foreach ($conditions as $condition) {
$match = $this->doesConditionMatch($rule, $condition, $object);
if (!$all && $match) {
$reason = "Any condition matched.";
$result = true;
break;
}
if ($all && !$match) {
$reason = "Not all conditions matched.";
$result = false;
break;
}
}
if ($result === null) {
if ($all) {
$reason = "All conditions matched.";
$result = true;
} else {
$reason = "No conditions matched.";
$result = false;
}
}
}
$rule_transcript = new HeraldRuleTranscript();
$rule_transcript->setRuleID($rule->getID());
$rule_transcript->setResult($result);
$rule_transcript->setReason($reason);
$rule_transcript->setRuleName($rule->getName());
$rule_transcript->setRuleOwner($rule->getAuthorPHID());
$this->transcript->addRuleTranscript($rule_transcript);
return $result;
}
protected function doesConditionMatch(
HeraldRule $rule,
HeraldCondition $condition,
HeraldObjectAdapter $object) {
$object_value = $this->getConditionObjectValue($condition, $object);
$test_value = $condition->getValue();
$cond = $condition->getFieldCondition();
$transcript = new HeraldConditionTranscript();
$transcript->setRuleID($rule->getID());
$transcript->setConditionID($condition->getID());
$transcript->setFieldName($condition->getFieldName());
$transcript->setCondition($cond);
$transcript->setTestValue($test_value);
$result = null;
switch ($cond) {
case HeraldConditionConfig::CONDITION_CONTAINS:
// "Contains" can take an array of strings, as in "Any changed
// filename" for diffs.
foreach ((array)$object_value as $value) {
$result = (stripos($value, $test_value) !== false);
if ($result) {
break;
}
}
break;
case HeraldConditionConfig::CONDITION_NOT_CONTAINS:
$result = (stripos($object_value, $test_value) === false);
break;
case HeraldConditionConfig::CONDITION_IS:
$result = ($object_value == $test_value);
break;
case HeraldConditionConfig::CONDITION_IS_NOT:
$result = ($object_value != $test_value);
break;
case HeraldConditionConfig::CONDITION_IS_ME:
$result = ($object_value == $rule->getAuthorPHID());
break;
case HeraldConditionConfig::CONDITION_IS_NOT_ME:
$result = ($object_value != $rule->getAuthorPHID());
break;
case HeraldConditionConfig::CONDITION_IS_ANY:
$test_value = array_flip($test_value);
$result = isset($test_value[$object_value]);
break;
case HeraldConditionConfig::CONDITION_IS_NOT_ANY:
$test_value = array_flip($test_value);
$result = !isset($test_value[$object_value]);
break;
case HeraldConditionConfig::CONDITION_INCLUDE_ALL:
if (!is_array($object_value)) {
$transcript->setNote('Object produced bad value!');
$result = false;
} else {
$have = array_select_keys(array_flip($object_value),
$test_value);
$result = (count($have) == count($test_value));
}
break;
case HeraldConditionConfig::CONDITION_INCLUDE_ANY:
$result = (bool)array_select_keys(array_flip($object_value),
$test_value);
break;
case HeraldConditionConfig::CONDITION_INCLUDE_NONE:
$result = !array_select_keys(array_flip($object_value),
$test_value);
break;
case HeraldConditionConfig::CONDITION_EXISTS:
$result = (bool)$object_value;
break;
case HeraldConditionConfig::CONDITION_NOT_EXISTS:
$result = !$object_value;
break;
case HeraldConditionConfig::CONDITION_REGEXP:
foreach ((array)$object_value as $value) {
$result = @preg_match($test_value, $value);
if ($result === false) {
$transcript->setNote(
"Regular expression is not valid!");
break;
}
if ($result) {
break;
}
}
$result = (bool)$result;
break;
case HeraldConditionConfig::CONDITION_REGEXP_PAIR:
// Match a JSON-encoded pair of regular expressions against a
// dictionary. The first regexp must match the dictionary key, and the
// second regexp must match the dictionary value. If any key/value pair
// in the dictionary matches both regexps, the condition is satisfied.
$regexp_pair = json_decode($test_value, true);
if (!is_array($regexp_pair)) {
$result = false;
$transcript->setNote("Regular expression pair is not valid JSON!");
break;
}
if (count($regexp_pair) != 2) {
$result = false;
$transcript->setNote("Regular expression pair is not a pair!");
break;
}
$key_regexp = array_shift($regexp_pair);
$value_regexp = array_shift($regexp_pair);
foreach ((array)$object_value as $key => $value) {
$key_matches = @preg_match($key_regexp, $key);
if ($key_matches === false) {
$result = false;
$transcript->setNote("First regular expression is invalid!");
break 2;
}
if ($key_matches) {
$value_matches = @preg_match($value_regexp, $value);
if ($value_matches === false) {
$result = false;
$transcript->setNote("Second regular expression is invalid!");
break 2;
}
if ($value_matches) {
$result = true;
break 2;
}
}
}
$result = false;
break;
case HeraldConditionConfig::CONDITION_RULE:
case HeraldConditionConfig::CONDITION_NOT_RULE:
$rule = idx($this->rules, $test_value);
if (!$rule) {
$transcript->setNote(
"Condition references a rule which does not exist!");
$result = false;
} else {
$is_not = ($cond == HeraldConditionConfig::CONDITION_NOT_RULE);
$result = $this->doesRuleMatch($rule, $object);
if ($is_not) {
$result = !$result;
}
}
break;
default:
throw new HeraldInvalidConditionException(
"Unknown condition '{$cond}'.");
}
$transcript->setResult($result);
$this->transcript->addConditionTranscript($transcript);
return $result;
}
protected function getConditionObjectValue(
HeraldCondition $condition,
HeraldObjectAdapter $object) {
$field = $condition->getFieldName();
return $this->getObjectFieldValue($field);
}
public function getObjectFieldValue($field) {
if (isset($this->fieldCache[$field])) {
return $this->fieldCache[$field];
}
$result = null;
switch ($field) {
case HeraldFieldConfig::FIELD_RULE:
$result = null;
break;
case HeraldFieldConfig::FIELD_TITLE:
case HeraldFieldConfig::FIELD_BODY:
case HeraldFieldConfig::FIELD_DIFF_FILE:
case HeraldFieldConfig::FIELD_DIFF_CONTENT:
// TODO: Type should be string.
$result = $this->object->getHeraldField($field);
break;
case HeraldFieldConfig::FIELD_AUTHOR:
case HeraldFieldConfig::FIELD_REPOSITORY:
case HeraldFieldConfig::FIELD_MERGE_REQUESTER:
// TODO: Type should be PHID.
$result = $this->object->getHeraldField($field);
break;
case HeraldFieldConfig::FIELD_TAGS:
case HeraldFieldConfig::FIELD_REVIEWER:
case HeraldFieldConfig::FIELD_REVIEWERS:
case HeraldFieldConfig::FIELD_CC:
case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVIEWERS:
case HeraldFieldConfig::FIELD_DIFFERENTIAL_CCS:
// TODO: Type should be list.
$result = $this->object->getHeraldField($field);
break;
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE:
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE_OWNER:
case HeraldFieldConfig::FIELD_NEED_AUDIT_FOR_PACKAGE:
$result = $this->object->getHeraldField($field);
if (!is_array($result)) {
throw new HeraldInvalidFieldException(
"Value of field type {$field} is not an array!");
}
break;
case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVISION:
// TODO: Type should be boolean I guess.
$result = $this->object->getHeraldField($field);
break;
default:
throw new HeraldInvalidConditionException(
"Unknown field type '{$field}'!");
}
$this->fieldCache[$field] = $result;
return $result;
}
protected function getRuleEffects(
HeraldRule $rule,
HeraldObjectAdapter $object) {
$effects = array();
foreach ($rule->getActions() as $action) {
$effect = new HeraldEffect();
$effect->setObjectPHID($object->getPHID());
$effect->setAction($action->getAction());
$effect->setTarget($action->getTarget());
$effect->setRuleID($rule->getID());
$name = $rule->getName();
$id = $rule->getID();
$effect->setReason(
'Conditions were met for Herald rule "'.$name.'" (#'.$id.').');
$effects[] = $effect;
}
return $effects;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 15:35 (3 w, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126044
Default Alt Text
(53 KB)

Event Timeline