Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2891638
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
46 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment