Page MenuHomePhorge

No OneTemporary

diff --git a/src/applications/differential/controller/revisionlist/DifferentialRevisionListController.php b/src/applications/differential/controller/revisionlist/DifferentialRevisionListController.php
index d6b3e48d47..ede38e9c6c 100644
--- a/src/applications/differential/controller/revisionlist/DifferentialRevisionListController.php
+++ b/src/applications/differential/controller/revisionlist/DifferentialRevisionListController.php
@@ -1,267 +1,451 @@
<?php
/*
* Copyright 2011 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 DifferentialRevisionListController extends DifferentialController {
private $filter;
public function shouldRequireLogin() {
return !$this->allowsAnonymousAccess();
}
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer_is_anonymous = !$user->isLoggedIn();
if ($request->isFormPost()) {
$phid_arr = $request->getArr('view_user');
$view_target = head($phid_arr);
return id(new AphrontRedirectResponse())
->setURI($request->getRequestURI()->alter('phid', $view_target));
}
- $filters = array();
- if (!$viewer_is_anonymous) {
- $filters = array(
- 'User Revisions',
- 'active' => array(
- 'name' => 'Active Revisions',
- 'queries' => array(
- array(
- 'query'
- => DifferentialRevisionListData::QUERY_NEED_ACTION_FROM_SELF,
- 'header' => 'Action Required',
- 'nodata' => 'You have no revisions requiring action.',
- ),
- array(
- 'query'
- => DifferentialRevisionListData::QUERY_NEED_ACTION_FROM_OTHERS,
- 'header' => 'Waiting on Others',
- 'nodata' => 'You have no revisions waiting on others',
- ),
- ),
- ),
- 'open' => array(
- 'name' => 'Open Revisions',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_OPEN_OWNED,
- 'header' => 'Your Open Revisions',
- ),
- ),
- ),
- 'reviews' => array(
- 'name' => 'Open Reviews',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_OPEN_REVIEWER,
- 'header' => 'Your Open Reviews',
- ),
- ),
- ),
- 'all' => array(
- 'name' => 'All Revisions',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_OWNED,
- 'header' => 'Your Revisions',
- ),
- ),
- ),
- 'related' => array(
- 'name' => 'All Revisions and Reviews',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_OWNED_OR_REVIEWER,
- 'header' => 'Your Revisions and Reviews',
- ),
- ),
- ),
- '<hr />'
- );
- }
- $filters = array_merge($filters, array(
- 'All Revisions',
- 'allopen' => array(
- 'name' => 'Open',
- 'nofilter' => true,
- 'queries' => array(
+ $params = array_filter(
+ array(
+ 'phid' => $request->getStr('phid'),
+ 'status' => $request->getStr('status'),
+ 'order' => $request->getStr('order'),
+ ));
+
+ $default_filter = ($viewer_is_anonymous ? 'all' : 'active');
+ $filters = $this->getFilters();
+ $this->filter = $this->selectFilter(
+ $filters,
+ $this->filter,
+ $default_filter);
+
+ $uri = new PhutilURI('/differential/filter/'.$this->filter.'/');
+ $uri->setQueryParams($params);
+
+ // Fill in the defaults we'll actually use for calculations if any
+ // parameters are missing.
+ $params += array(
+ 'phid' => $user->getPHID(),
+ 'status' => 'open',
+ 'order' => 'modified',
+ );
+
+ $side_nav = new AphrontSideNavView();
+ foreach ($filters as $filter) {
+ list($filter_name, $display_name) = $filter;
+ if ($filter_name) {
+ $href = clone $uri;
+ $href->setPath('/differential/filter/'.$filter_name.'/');
+ if ($filter_name == $this->filter) {
+ $class = 'aphront-side-nav-selected';
+ } else {
+ $class = null;
+ }
+ $item = phutil_render_tag(
+ 'a',
array(
- 'query' => DifferentialRevisionListData::QUERY_ALL_OPEN,
- 'header' => 'All Open Revisions',
+ 'href' => (string)$href,
+ 'class' => $class,
),
- ),
- ),
- ));
-
- if (empty($filters[$this->filter])) {
- if (!$viewer_is_anonymous) {
- $this->filter = 'active';
+ phutil_escape_html($display_name));
} else {
- $this->filter = 'allopen';
+ $item = phutil_render_tag(
+ 'span',
+ array(),
+ phutil_escape_html($display_name));
}
+ $side_nav->addNavItem($item);
}
- $view_phid = nonempty($request->getStr('phid'), $user->getPHID());
-
- $queries = array();
- $filter = $filters[$this->filter];
- foreach ($filter['queries'] as $query) {
- $query_object = new DifferentialRevisionListData(
- $query['query'],
- array($view_phid));
- $queries[] = array(
- 'object' => $query_object,
- ) + $query;
- }
+ $panels = array();
+ $handles = array();
+ $controls = $this->getFilterControls($this->filter);
+ if ($this->getFilterRequiresUser($this->filter) && !$params['phid']) {
+ // In the anonymous case, we still want to let you see some user's
+ // list, but we don't have a default PHID to provide (normally, we use
+ // the viewing user's). Show a warning instead.
+ $warning = new AphrontErrorView();
+ $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
+ $warning->setTitle('User Required');
+ $warning->appendChild(
+ 'This filter requires that a user be specified above.');
+ $panels[] = $warning;
+ } else {
+ $query = $this->buildQuery($this->filter, $params['phid']);
- $side_nav = new AphrontSideNavView();
+ $pager = null;
+ if ($this->getFilterAllowsPaging($this->filter)) {
+ $pager = new AphrontPagerView();
+ $pager->setOffset($request->getInt('page'));
+ $pager->setPageSize(1000);
+ $pager->setURI($uri, 'page');
- $query = null;
- if ($view_phid) {
- $query = '?phid='.$view_phid;
- }
+ $query->setOffset($pager->getOffset());
+ $query->setLimit($pager->getPageSize() + 1);
+ }
+
+ foreach ($controls as $control) {
+ $this->applyControlToQuery($control, $query, $params);
+ }
+
+ $revisions = $query->execute();
+
+ if ($pager) {
+ $revisions = $pager->sliceResults($revisions);
+ }
+
+ $views = $this->buildViews($this->filter, $params['phid'], $revisions);
- foreach ($filters as $filter_name => $filter_desc) {
- if (is_int($filter_name)) {
- $side_nav->addNavItem(
- phutil_render_tag(
- 'span',
- array(),
- $filter_desc));
- continue;
+ $view_objects = ipull($views, 'view');
+ $phids = array_mergev(mpull($view_objects, 'getRequiredHandlePHIDs'));
+ $phids[] = $params['phid'];
+ $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
+
+ foreach ($views as $view) {
+ $view['view']->setHandles($handles);
+ $panel = new AphrontPanelView();
+ $panel->setHeader($view['title']);
+ $panel->appendChild($view['view']);
+ if ($pager) {
+ $panel->appendChild($pager);
+ }
+ $panels[] = $panel;
}
- $selected = ($filter_name == $this->filter);
- $side_nav->addNavItem(
+ }
+
+ $filter_form = id(new AphrontFormView())
+ ->setAction('/differential/filter/'.$this->filter.'/')
+ ->setUser($user);
+ foreach ($controls as $control) {
+ $control_view = $this->renderControl($control, $handles, $uri, $params);
+ $filter_form->appendChild($control_view);
+ }
+ $filter_form
+ ->addHiddenInput('status', $params['status'])
+ ->addHiddenInput('order', $params['order'])
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->setValue('Filter Revisions'));
+
+ $filter_view = new AphrontListFilterView();
+ $filter_view->appendChild($filter_form);
+
+ if (!$viewer_is_anonymous) {
+ $create_uri = new PhutilURI('/differential/diff/create/');
+ $filter_view->addButton(
phutil_render_tag(
'a',
array(
- 'href' => '/differential/filter/'.$filter_name.'/'.$query,
- 'class' => $selected ? 'aphront-side-nav-selected' : null,
+ 'href' => (string)$create_uri,
+ 'class' => 'green button',
),
- phutil_escape_html($filter_desc['name'])));
+ 'Create Revision'));
+ }
+
+ $side_nav->appendChild($filter_view);
+
+ foreach ($panels as $panel) {
+ $side_nav->appendChild($panel);
}
- $rev_ids = array();
- foreach ($queries as $key => $query) {
- $revisions = $query['object']->loadRevisions();
- foreach ($revisions as $revision) {
- $rev_ids[$revision->getID()] = true;
+ return $this->buildStandardPageResponse(
+ $side_nav,
+ array(
+ 'title' => 'Differential Home',
+ 'tab' => 'revisions',
+ ));
+ }
+
+ private function getFilters() {
+ return array(
+ array(null, 'User Revisions'),
+ array('active', 'Active'),
+ array('revisions', 'Revisions'),
+ array('reviews', 'Reviews'),
+ array('subscribed', 'Subscribed'),
+ array(null, 'All Revisions'),
+ array('all', 'All'),
+ );
+ }
+
+ private function selectFilter(
+ array $filters,
+ $requested_filter,
+ $default_filter) {
+
+ // If the user requested a filter, make sure it actually exists.
+ if ($requested_filter) {
+ foreach ($filters as $filter) {
+ if ($filter[0] === $requested_filter) {
+ return $requested_filter;
+ }
}
- $queries[$key]['revisions'] = $revisions;
}
- if ($rev_ids) {
- $rev = new DifferentialRevision();
- $relationships = queryfx_all(
- $rev->establishConnection('r'),
- 'SELECT * FROM %T WHERE revisionID IN (%Ld) ORDER BY sequence',
- DifferentialRevision::RELATIONSHIP_TABLE,
- array_keys($rev_ids));
- $relationships = igroup($relationships, 'revisionID');
- } else {
- $relationships = array();
+ // If not, return the default filter.
+ return $default_filter;
+ }
+
+ private function getFilterRequiresUser($filter) {
+ static $requires = array(
+ 'active' => true,
+ 'revisions' => true,
+ 'reviews' => true,
+ 'subscribed' => true,
+ 'all' => false,
+ );
+ if (!isset($requires[$filter])) {
+ throw new Exception("Unknown filter '{$filter}'!");
}
+ return $requires[$filter];
+ }
- foreach ($queries as $query) {
- foreach ($query['revisions'] as $revision) {
- $revision->attachRelationships(
- idx(
- $relationships,
- $revision->getID(),
- array()));
- }
+ private function getFilterAllowsPaging($filter) {
+ static $allows = array(
+ 'active' => false,
+ 'revisions' => true,
+ 'reviews' => true,
+ 'subscribed' => true,
+ 'all' => true,
+ );
+ if (!isset($allows[$filter])) {
+ throw new Exception("Unknown filter '{$filter}'!");
}
+ return $allows[$filter];
+ }
- $phids = array();
- foreach ($queries as $key => $query) {
- $view = id(new DifferentialRevisionListView())
- ->setRevisions($query['revisions'])
- ->setUser($user)
- ->setNoDataString(idx($query, 'nodata'));
- $phids[] = $view->getRequiredHandlePHIDs();
- $queries[$key]['view'] = $view;
+ private function getFilterControls($filter) {
+ static $controls = array(
+ 'active' => array('phid'),
+ 'revisions' => array('phid', 'status', 'order'),
+ 'reviews' => array('phid', 'status', 'order'),
+ 'subscribed' => array('phid', 'status', 'order'),
+ 'all' => array('status', 'order'),
+ );
+ if (!isset($controls[$filter])) {
+ throw new Exception("Unknown filter '{$filter}'!");
}
- $phids = array_mergev($phids);
- $phids[] = $view_phid;
+ return $controls[$filter];
+ }
+
+ private function buildQuery($filter, $user_phid) {
+ $query = new DifferentialRevisionQuery();
- $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
+ $query->needRelationships(true);
- foreach ($queries as $query) {
- $query['view']->setHandles($handles);
+ switch ($filter) {
+ case 'active':
+ $query->withResponsibleUsers(array($user_phid));
+ $query->withStatus(DifferentialRevisionQuery::STATUS_OPEN);
+ $query->setLimit(null);
+ break;
+ case 'revisions':
+ $query->withAuthors(array($user_phid));
+ break;
+ case 'reviews':
+ $query->withReviewers(array($user_phid));
+ break;
+ case 'subscribed':
+ $query->withSubscribers(array($user_phid));
+ break;
+ case 'all':
+ break;
+ default:
+ throw new Exception("Unknown filter '{$filter}'!");
}
+ return $query;
+ }
- if (empty($filters[$this->filter]['nofilter'])) {
- $filter_form = id(new AphrontFormView())
- ->setUser($user)
- ->appendChild(
- id(new AphrontFormTokenizerControl())
- ->setDatasource('/typeahead/common/users/')
- ->setLabel('View User')
- ->setName('view_user')
- ->setValue(
- array(
- $view_phid => $handles[$view_phid]->getFullName(),
- ))
- ->setLimit(1))
- ->appendChild(
- id(new AphrontFormSubmitControl())
- ->setValue('Filter Revisions'));
- $filter_view = new AphrontListFilterView();
- $filter_view->appendChild($filter_form);
-
- $viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn();
- if (!$viewer_is_anonymous) {
- $create_uri = new PhutilURI('/differential/diff/create/');
- $filter_view->addButton(
- phutil_render_tag(
- 'a',
- array(
- 'href' => (string)$create_uri,
- 'class' => 'green button',
- ),
- 'Create Revision'));
- }
+ private function renderControl(
+ $control,
+ array $handles,
+ PhutilURI $uri,
+ array $params) {
+ switch ($control) {
+ case 'phid':
+ $view_phid = $params['phid'];
+ $value = array();
+ if ($view_phid) {
+ $value = array(
+ $view_phid => $handles[$view_phid]->getFullName(),
+ );
+ }
+ return id(new AphrontFormTokenizerControl())
+ ->setDatasource('/typeahead/common/users/')
+ ->setLabel('View User')
+ ->setName('view_user')
+ ->setValue($value)
+ ->setLimit(1);
+ case 'status':
+ $links = $this->renderToggleButtons(
+ array(
+ 'open' => 'Open',
+ 'all' => 'All',
+ ),
+ $params['status'],
+ $uri,
+ 'status');
+ return id(new AphrontFormToggleButtonsControl())
+ ->setLabel('Status')
+ ->setValue($links);
+ case 'order':
+ $links = $this->renderToggleButtons(
+ array(
+ 'modified' => 'Modified',
+ 'created' => 'Created',
+ ),
+ $params['order'],
+ $uri,
+ 'order');
+ return id(new AphrontFormToggleButtonsControl())
+ ->setLabel('Order')
+ ->setValue($links);
+ default:
+ throw new Exception("Unknown control '{$control}'!");
+ }
+ }
- $side_nav->appendChild($filter_view);
+ private function applyControlToQuery($control, $query, array $params) {
+ switch ($control) {
+ case 'phid':
+ // Already applied by query construction.
+ break;
+ case 'status':
+ if ($params['status'] == 'open') {
+ $query->withStatus(DifferentialRevisionQuery::STATUS_OPEN);
+ }
+ break;
+ case 'order':
+ if ($params['order'] == 'created') {
+ $query->setOrder(DifferentialRevisionQuery::ORDER_CREATED);
+ }
+ break;
+ default:
+ throw new Exception("Unknown control '{$control}'!");
}
+ }
- foreach ($queries as $query) {
- $table = $query['view']->render();
+ private function buildViews($filter, $user_phid, array $revisions) {
+ $user = $this->getRequest()->getUser();
- $panel = new AphrontPanelView();
- $panel->setHeader($query['header']);
- $panel->appendChild($table);
+ $views = array();
+ switch ($filter) {
+ case 'active':
+ $active = array();
+ $waiting = array();
- $side_nav->appendChild($panel);
+ // Bucket revisions into $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) {
+ $status_review = DifferentialRevisionStatus::NEEDS_REVIEW;
+ $needs_review = ($revision->getStatus() == $status_review);
+ $filter_is_author = ($revision->getAuthorPHID() == $user_phid);
+
+ // 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) {
+ $active[] = $revision;
+ } else {
+ $waiting[] = $revision;
+ }
+ }
+
+ $view = id(new DifferentialRevisionListView())
+ ->setRevisions($active)
+ ->setUser($user)
+ ->setNoDataString("You have no active revisions requiring action.");
+ $views[] = array(
+ 'title' => 'Action Required',
+ 'view' => $view,
+ );
+
+ $view = id(new DifferentialRevisionListView())
+ ->setRevisions($waiting)
+ ->setUser($user)
+ ->setNoDataString("You have no active revisions waiting on others.");
+ $views[] = array(
+ 'title' => 'Waiting On Others',
+ 'view' => $view,
+ );
+ break;
+ case 'revisions':
+ case 'reviews':
+ case 'subscribed':
+ case 'all':
+ $titles = array(
+ 'revisions' => 'Revisions by Author',
+ 'reviews' => 'Revisions by Reviewer',
+ 'subscribed' => 'Revisions by Subscriber',
+ 'all' => 'Revisions',
+ );
+ $view = id(new DifferentialRevisionListView())
+ ->setRevisions($revisions)
+ ->setUser($user);
+ $views[] = array(
+ 'title' => idx($titles, $filter),
+ 'view' => $view,
+ );
+ break;
+ default:
+ throw new Exception("Unknown filter '{$filter}'!");
}
+ return $views;
+ }
- return $this->buildStandardPageResponse(
- $side_nav,
- array(
- 'title' => 'Differential Home',
- 'tab' => 'revisions',
- ));
+ private function renderToggleButtons($buttons, $selected, $uri, $param) {
+ $links = array();
+ foreach ($buttons as $value => $name) {
+ if ($value == $selected) {
+ $more = ' toggle-selected toggle-fixed';
+ } else {
+ $more = null;
+ }
+ $links[] = phutil_render_tag(
+ 'a',
+ array(
+ 'class' => 'toggle'.$more,
+ 'href' => $uri->alter($param, $value),
+ ),
+ phutil_escape_html($name));
+ }
+ return implode('', $links);
}
}
diff --git a/src/applications/differential/controller/revisionlist/__init__.php b/src/applications/differential/controller/revisionlist/__init__.php
index b22360ded1..ec0e48b2e7 100644
--- a/src/applications/differential/controller/revisionlist/__init__.php
+++ b/src/applications/differential/controller/revisionlist/__init__.php
@@ -1,28 +1,30 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/redirect');
+phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
phutil_require_module('phabricator', 'applications/differential/controller/base');
-phutil_require_module('phabricator', 'applications/differential/data/revisionlist');
-phutil_require_module('phabricator', 'applications/differential/storage/revision');
+phutil_require_module('phabricator', 'applications/differential/query/revision');
phutil_require_module('phabricator', 'applications/differential/view/revisionlist');
phutil_require_module('phabricator', 'applications/phid/handle/data');
-phutil_require_module('phabricator', 'storage/queryfx');
+phutil_require_module('phabricator', 'view/control/pager');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');
+phutil_require_module('phabricator', 'view/form/control/togglebuttons');
phutil_require_module('phabricator', 'view/form/control/tokenizer');
+phutil_require_module('phabricator', 'view/form/error');
phutil_require_module('phabricator', 'view/layout/listfilter');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phabricator', 'view/layout/sidenav');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'parser/uri');
phutil_require_module('phutil', 'utils');
phutil_require_source('DifferentialRevisionListController.php');
diff --git a/src/applications/differential/query/revision/DifferentialRevisionQuery.php b/src/applications/differential/query/revision/DifferentialRevisionQuery.php
index bc80898e75..28ffebbbee 100644
--- a/src/applications/differential/query/revision/DifferentialRevisionQuery.php
+++ b/src/applications/differential/query/revision/DifferentialRevisionQuery.php
@@ -1,497 +1,567 @@
<?php
/*
* Copyright 2011 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';
private $authors = array();
private $ccs = array();
private $reviewers = array();
private $revIDs = array();
private $phids = array();
private $subscribers = array();
private $responsibles = 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;
/* -( 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.
*
* @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 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 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;
}
/* -( 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');
- $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 = qsprintf(
- $conn_r,
- 'LIMIT %d, %d',
- (int)$this->offset,
- $this->limit);
-
- $data = queryfx_all(
- $conn_r,
- '%Q %Q %Q %Q %Q %Q',
- $select,
- $joins,
- $where,
- $group_by,
- $order_by,
- $limit);
+ if ($this->shouldUseResponsibleFastPath()) {
+ $data = $this->loadDataUsingResponsibleFastPath();
+ } else {
+ $data = $this->loadData();
+ }
$revisions = $table->loadAllFromArray($data);
if ($revisions && $this->needRelationships) {
$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()));
}
}
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->phids) {
+ return true;
+ }
+ return false;
+ }
+
+
+ private function loadDataUsingResponsibleFastPath() {
+ $table = new DifferentialRevision();
+ $conn_r = $table->establishConnection('r');
+
+ $responsible_phid = reset($this->responsibles);
+ $open_statuses = array(
+ DifferentialRevisionStatus::NEEDS_REVIEW,
+ DifferentialRevisionStatus::NEEDS_REVISION,
+ DifferentialRevisionStatus::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)',
+ $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->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->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(
DifferentialRevisionStatus::NEEDS_REVIEW,
DifferentialRevisionStatus::NEEDS_REVISION,
DifferentialRevisionStatus::ACCEPTED,
));
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}'.");
}
}
}
diff --git a/src/storage/qsprintf/qsprintf.php b/src/storage/qsprintf/qsprintf.php
index e0323be161..c2b41657d1 100644
--- a/src/storage/qsprintf/qsprintf.php
+++ b/src/storage/qsprintf/qsprintf.php
@@ -1,309 +1,313 @@
<?php
/*
* Copyright 2011 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.
*/
/**
* Format an SQL query. This function behaves like sprintf(), except that
* all the normal conversions (like %s) will be properly escaped, and
* additional conversions are supported:
*
* %nd, %ns, %nf
* "Nullable" versions of %d, %s and %f. Will produce 'NULL' if the
* argument is a strict null.
*
* %=d, %=s, %=f
* "Nullable Test" versions of %d, %s and %f. If you pass a value, you
* get "= 3"; if you pass null, you get "IS NULL". For instance, this
* will work properly if `hatID' is a nullable column and $hat is null.
*
* qsprintf($conn, 'WHERE hatID %=d', $hat);
*
* %Ld, %Ls, %Lf
* "List" versions of %d, %s and %f. These are appropriate for use in
* an "IN" clause. For example:
*
* qsprintf($conn, 'WHERE hatID IN(%Ld)', $list_of_hats);
*
* %T ("Table")
* Escapes a table name.
*
* %C, %LC
* Escapes a column name or a list of column names.
*
* %K ("Comment")
* Escapes a comment.
*
* %Q ("Query Fragment")
* Injects a raw query fragment. Extremely dangerous! Not escaped!
*
* %~ ("Substring")
* Escapes a substring query for a LIKE (or NOT LIKE) clause. For example:
*
* // Find all rows with $search as a substing of `name`.
* qsprintf($conn, 'WHERE name LIKE %~', $search);
*
* See also %> and %<.
*
* %> ("Prefix")
* Escapes a prefix query for a LIKE clause. For example:
*
* // Find all rows where `name` starts with $prefix.
* qsprintf($conn, 'WHERE name LIKE %>', $prefix);
*
* %< ("Suffix")
* Escapes a suffix query for a LIKE clause. For example:
*
* // Find all rows where `name` ends with $suffix.
* qsprintf($conn, 'WHERE name LIKE %<', $suffix);
*
* @group storage
*/
function qsprintf($conn, $pattern/*, ... */) {
$args = func_get_args();
array_shift($args);
return xsprintf('xsprintf_query', $conn, $args);
}
/**
* @group storage
*/
function vqsprintf($conn, $pattern, array $argv) {
array_unshift($argv, $pattern);
return xsprintf('xsprintf_query', $conn, $argv);
}
/**
* xsprintf() callback for encoding SQL queries. See qsprintf().
* @group storage
*/
function xsprintf_query($userdata, &$pattern, &$pos, &$value, &$length) {
$type = $pattern[$pos];
$conn = $userdata;
$next = (strlen($pattern) > $pos + 1) ? $pattern[$pos + 1] : null;
$nullable = false;
$done = false;
$prefix = '';
+ if (!($conn instanceof AphrontDatabaseConnection)) {
+ throw new Exception("Invalid database connection!");
+ }
+
switch ($type) {
case '=': // Nullable test
switch ($next) {
case 'd':
case 'f':
case 's':
$pattern = substr_replace($pattern, '', $pos, 1);
$length = strlen($pattern);
$type = 's';
if ($value === null) {
$value = 'IS NULL';
$done = true;
} else {
$prefix = '= ';
$type = $next;
}
break;
default:
throw new Exception('Unknown conversion, try %=d, %=s, or %=f.');
}
break;
case 'n': // Nullable...
switch ($next) {
case 'd': // ...integer.
case 'f': // ...float.
case 's': // ...string.
$pattern = substr_replace($pattern, '', $pos, 1);
$length = strlen($pattern);
$type = $next;
$nullable = true;
break;
default:
throw new Exception('Unknown conversion, try %nd or %ns.');
}
break;
case 'L': // List of..
_qsprintf_check_type($value, "L{$next}", $pattern);
$pattern = substr_replace($pattern, '', $pos, 1);
$length = strlen($pattern);
$type = 's';
$done = true;
switch ($next) {
case 'd': // ...integers.
$value = implode(', ', array_map('intval', $value));
break;
case 's': // ...strings.
foreach ($value as $k => $v) {
$value[$k] = "'".$conn->escapeString($v)."'";
}
$value = implode(', ', $value);
break;
case 'C': // ...columns.
foreach ($value as $k => $v) {
$value[$k] = $conn->escapeColumnName($v);
}
$value = implode(', ', $value);
break;
default:
throw new Exception("Unknown conversion %L{$next}.");
}
break;
}
if (!$done) {
_qsprintf_check_type($value, $type, $pattern);
switch ($type) {
case 's': // String
if ($nullable && $value === null) {
$value = 'NULL';
} else {
$value = "'".$conn->escapeString($value)."'";
}
$type = 's';
break;
case 'Q': // Query Fragment
$type = 's';
break;
case '~': // Like Substring
case '>': // Like Prefix
case '<': // Like Suffix
$value = $conn->escapeStringForLikeClause($value);
switch ($type) {
case '~': $value = "'%".$value."%'"; break;
case '>': $value = "'" .$value."%'"; break;
case '<': $value = "'%".$value. "'"; break;
}
$type = 's';
break;
case 'f': // Float
if ($nullable && $value === null) {
$value = 'NULL';
} else {
$value = (float)$value;
}
$type = 's';
break;
case 'd': // Integer
if ($nullable && $value === null) {
$value = 'NULL';
} else {
$value = (int)$value;
}
$type = 's';
break;
case 'T': // Table
case 'C': // Column
$value = $conn->escapeColumnName($value);
$type = 's';
break;
case 'K': // Komment
$value = $conn->escapeMultilineComment($value);
$type = 's';
break;
default:
throw new Exception("Unknown conversion '%{$type}'.");
}
}
if ($prefix) {
$value = $prefix.$value;
}
$pattern[$pos] = $type;
}
/**
* @group storage
*/
function _qsprintf_check_type($value, $type, $query) {
switch ($type) {
case 'Ld': case 'Ls': case 'LC': case 'LA': case 'LO':
if (!is_array($value)) {
throw new AphrontQueryParameterException(
$query,
"Expected array argument for %{$type} conversion.");
}
if (empty($value)) {
throw new AphrontQueryParameterException(
$query,
"Array for %{$type} conversion is empty.");
}
foreach ($value as $scalar) {
_qsprintf_check_scalar_type($scalar, $type, $query);
}
break;
default:
_qsprintf_check_scalar_type($value, $type, $query);
}
}
/**
* @group storage
*/
function _qsprintf_check_scalar_type($value, $type, $query) {
switch ($type) {
case 'Q': case 'LC': case 'T': case 'C':
if (!is_string($value)) {
throw new AphrontQueryParameterException(
$query,
"Expected a string for %{$type} conversion.");
}
break;
case 'Ld': case 'd': case 'f':
if (!is_null($value) && !is_numeric($value)) {
throw new AphrontQueryParameterException(
$query,
"Expected a numeric scalar or null for %{$type} conversion.");
}
break;
case 'Ls': case 's':
case '~': case '>': case '<': case 'K':
if (!is_null($value) && !is_scalar($value)) {
throw new AphrontQueryParameterException(
$query,
"Expected a scalar or null for %{$type} conversion.");
}
break;
case 'LA': case 'LO':
if (!is_null($value) && !is_scalar($value) &&
!(is_array($value) && !empty($value))) {
throw new AphrontQueryParameterException(
$query,
"Expected a scalar or null or non-empty array for ".
"%{$type} conversion.");
}
break;
default:
throw new Exception("Unknown conversion '{$type}'.");
}
}

File Metadata

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

Event Timeline