Allow projects to review revisions

Summary:
Ref T1279. No actual logical changes, but:

  - You can now add projects as reviewers from the revision view typeahead ("Add Reviewers" action).
  - You can now add projects as reviewers from the revision detail typeahead.
  - You can now add projects as reviewers from the CLI (`#yoloswag`).
  - Generated commit messages now list project reviewers (`Reviewers: #yoloswag`).

I'll separate projects from users in the "Reviewers" tables in the next revision.

Test Plan:
  - Added projects as reviewers using the web UI and CLI.
  - Used `arc amend --show --revision Dnnn` to generate commit messages.
  - Viewed revision with project reviewers in web UI.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1279

Differential Revision: https://secure.phabricator.com/D7230
This commit is contained in:
epriestley 2013-10-04 19:57:15 -07:00
parent 370c7635a7
commit cf4eb3109e
9 changed files with 114 additions and 29 deletions

View file

@ -779,6 +779,14 @@ abstract class DifferentialFieldSpecification {
return $this->parseCommitMessageObjectList($value, $mailables = false); return $this->parseCommitMessageObjectList($value, $mailables = false);
} }
protected function parseCommitMessageUserOrProjectList($value) {
return $this->parseCommitMessageObjectList(
$value,
$mailables = false,
$allow_partial = false,
$projects = true);
}
/** /**
* Parse a list of mailable objects into a canonical PHID list. * Parse a list of mailable objects into a canonical PHID list.
* *
@ -813,36 +821,66 @@ abstract class DifferentialFieldSpecification {
$object_map = array(); $object_map = array();
$users = id(new PhabricatorUser())->loadAllWhere( $project_names = array();
'(username IN (%Ls))', $other_names = array();
$value); foreach ($value as $item) {
if (preg_match('/^#/', $item)) {
$user_map = mpull($users, 'getPHID', 'getUsername'); $project_names[$item] = ltrim(phutil_utf8_strtolower($item), '#').'/';
foreach ($user_map as $username => $phid) { } else {
// Usernames may have uppercase letters in them. Put both names in the $other_names[] = $item;
// map so we can try the original case first, so that username *always* }
// works in weird edge cases where some other mailable object collides.
$object_map[$username] = $phid;
$object_map[strtolower($username)] = $phid;
} }
if ($include_mailables) { if ($project_names) {
$mailables = id(new PhabricatorMetaMTAMailingList())->loadAllWhere( // TODO: (T603) This should probably be policy-aware, although maybe not,
'(email IN (%Ls)) OR (name IN (%Ls))', // since we generally don't want to destroy data and it doesn't leak
$value, // anything?
$value); $projects = id(new PhabricatorProjectQuery())
$object_map += mpull($mailables, 'getPHID', 'getName'); ->setViewer(PhabricatorUser::getOmnipotentUser())
$object_map += mpull($mailables, 'getPHID', 'getEmail'); ->withPhrictionSlugs($project_names)
->execute();
$reverse_map = array_flip($project_names);
foreach ($projects as $project) {
$reverse_key = $project->getPhrictionSlug();
if (isset($reverse_map[$reverse_key])) {
$object_map[$reverse_map[$reverse_key]] = $project->getPHID();
}
}
}
if ($other_names) {
$users = id(new PhabricatorUser())->loadAllWhere(
'(username IN (%Ls))',
$other_names);
$user_map = mpull($users, 'getPHID', 'getUsername');
foreach ($user_map as $username => $phid) {
// Usernames may have uppercase letters in them. Put both names in the
// map so we can try the original case first, so that username *always*
// works in weird edge cases where some other mailable object collides.
$object_map[$username] = $phid;
$object_map[strtolower($username)] = $phid;
}
if ($include_mailables) {
$mailables = id(new PhabricatorMetaMTAMailingList())->loadAllWhere(
'(email IN (%Ls)) OR (name IN (%Ls))',
$other_names,
$other_names);
$object_map += mpull($mailables, 'getPHID', 'getName');
$object_map += mpull($mailables, 'getPHID', 'getEmail');
}
} }
$invalid = array(); $invalid = array();
$results = array(); $results = array();
foreach ($value as $name) { foreach ($value as $name) {
if (empty($object_map[$name])) { if (empty($object_map[$name])) {
if (empty($object_map[strtolower($name)])) { if (empty($object_map[phutil_utf8_strtolower($name)])) {
$invalid[] = $name; $invalid[] = $name;
} else { } else {
$results[] = $object_map[strtolower($name)]; $results[] = $object_map[phutil_utf8_strtolower($name)];
} }
} else { } else {
$results[] = $object_map[$name]; $results[] = $object_map[$name];

View file

@ -15,7 +15,7 @@ final class DifferentialReviewersFieldSpecification
} }
public function renderLabelForRevisionView() { public function renderLabelForRevisionView() {
return 'Reviewers:'; return pht('Reviewers');
} }
public function renderValueForRevisionView() { public function renderValueForRevisionView() {
@ -144,7 +144,7 @@ final class DifferentialReviewersFieldSpecification
->setLabel(pht('Reviewers')) ->setLabel(pht('Reviewers'))
->setName('reviewers') ->setName('reviewers')
->setUser($this->getUser()) ->setUser($this->getUser())
->setDatasource('/typeahead/common/users/') ->setDatasource('/typeahead/common/usersorprojects/')
->setValue($reviewer_map) ->setValue($reviewer_map)
->setError($this->error); ->setError($this->error);
} }
@ -179,9 +179,11 @@ final class DifferentialReviewersFieldSpecification
return null; return null;
} }
$project_type = PhabricatorProjectPHIDTypeProject::TYPECONST;
$names = array(); $names = array();
foreach ($this->reviewers as $phid) { foreach ($this->reviewers as $phid) {
$names[] = $this->getHandle($phid)->getName(); $names[] = $this->getHandle($phid)->getObjectName();
} }
return implode(', ', $names); return implode(', ', $names);
@ -195,7 +197,7 @@ final class DifferentialReviewersFieldSpecification
} }
public function parseValueFromCommitMessage($value) { public function parseValueFromCommitMessage($value) {
return $this->parseCommitMessageUserList($value); return $this->parseCommitMessageUserOrProjectList($value);
} }
public function shouldAppearOnRevisionList() { public function shouldAppearOnRevisionList() {
@ -242,7 +244,8 @@ final class DifferentialReviewersFieldSpecification
$handles = array_select_keys( $handles = array_select_keys(
$handles, $handles,
array($this->getRevision()->getPrimaryReviewer())) + $handles; array($this->getRevision()->getPrimaryReviewer())) + $handles;
$names = mpull($handles, 'getName');
$names = mpull($handles, 'getObjectName');
return 'Reviewers: '.implode(', ', $names); return 'Reviewers: '.implode(', ', $names);
} }

View file

@ -914,6 +914,7 @@ final class DifferentialRevisionQuery
$edges = id(new PhabricatorEdgeQuery()) $edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($revisions, 'getPHID')) ->withSourcePHIDs(mpull($revisions, 'getPHID'))
->withEdgeTypes(array($type_reviewer)) ->withEdgeTypes(array($type_reviewer))
->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST)
->execute(); ->execute();
foreach ($revisions as $revision) { foreach ($revisions as $revision) {
@ -923,6 +924,7 @@ final class DifferentialRevisionQuery
$data[] = array( $data[] = array(
'relation' => DifferentialRevision::RELATION_REVIEWER, 'relation' => DifferentialRevision::RELATION_REVIEWER,
'objectPHID' => $dst_phid, 'objectPHID' => $dst_phid,
'reasonPHID' => null,
); );
} }
@ -1024,11 +1026,11 @@ final class DifferentialRevisionQuery
->withSourcePHIDs(mpull($revisions, 'getPHID')) ->withSourcePHIDs(mpull($revisions, 'getPHID'))
->withEdgeTypes(array($edge_type)) ->withEdgeTypes(array($edge_type))
->needEdgeData(true) ->needEdgeData(true)
->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST)
->execute(); ->execute();
foreach ($revisions as $revision) { foreach ($revisions as $revision) {
$revision_edges = $edges[$revision->getPHID()][$edge_type]; $revision_edges = $edges[$revision->getPHID()][$edge_type];
$reviewers = array(); $reviewers = array();
foreach ($revision_edges as $user_phid => $edge) { foreach ($revision_edges as $user_phid => $edge) {
$data = $edge['data']; $data = $edge['data'];

View file

@ -237,11 +237,13 @@ final class DifferentialRevision extends DifferentialDAO
$reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(), $this->getPHID(),
PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER); PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER);
$reviewer_phids = array_reverse($reviewer_phids);
foreach ($reviewer_phids as $phid) { foreach ($reviewer_phids as $phid) {
$data[] = array( $data[] = array(
'relation' => self::RELATION_REVIEWER, 'relation' => self::RELATION_REVIEWER,
'objectPHID' => $phid, 'objectPHID' => $phid,
'reasonPHID' => null,
); );
} }

View file

@ -118,12 +118,12 @@ final class DifferentialAddCommentView extends AphrontView {
'add_reviewers' => 1, 'add_reviewers' => 1,
'resign' => 1, 'resign' => 1,
), ),
'src' => '/typeahead/common/users/', 'src' => '/typeahead/common/usersorprojects/',
'value' => $this->reviewers, 'value' => $this->reviewers,
'row' => 'add-reviewers', 'row' => 'add-reviewers',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'labels' => $add_reviewers_labels, 'labels' => $add_reviewers_labels,
'placeholder' => pht('Type a user name...'), 'placeholder' => pht('Type a user or project name...'),
), ),
'add-ccs-tokenizer' => array( 'add-ccs-tokenizer' => array(
'actions' => array('add_ccs' => 1), 'actions' => array('add_ccs' => 1),

View file

@ -107,4 +107,8 @@ final class PhabricatorNotificationQuery
return $this->formatWhereClause($where); return $this->formatWhereClause($where);
} }
protected function getPagingValue($item) {
return $item->getChronologicalKey();
}
} }

View file

@ -14,6 +14,19 @@ final class PhabricatorObjectHandle
private $status = PhabricatorObjectHandleStatus::STATUS_OPEN; private $status = PhabricatorObjectHandleStatus::STATUS_OPEN;
private $complete; private $complete;
private $disabled; private $disabled;
private $objectName;
public function setObjectName($object_name) {
$this->objectName = $object_name;
return $this;
}
public function getObjectName() {
if (!$this->objectName) {
return $this->getName();
}
return $this->objectName;
}
public function setURI($uri) { public function setURI($uri) {
$this->uri = $uri; $this->uri = $uri;

View file

@ -39,6 +39,7 @@ final class PhabricatorProjectPHIDTypeProject extends PhabricatorPHIDType {
$id = $project->getID(); $id = $project->getID();
$handle->setName($name); $handle->setName($name);
$handle->setObjectName('#'.rtrim($project->getPhrictionSlug(), '/'));
$handle->setURI("/project/view/{$id}/"); $handle->setURI("/project/view/{$id}/");
} }
} }

View file

@ -26,6 +26,10 @@ final class PhabricatorEdgeQuery extends PhabricatorQuery {
private $edgeTypes; private $edgeTypes;
private $resultSet; private $resultSet;
const ORDER_OLDEST_FIRST = 'order:oldest';
const ORDER_NEWEST_FIRST = 'order:newest';
private $order = self::ORDER_NEWEST_FIRST;
private $needEdgeData; private $needEdgeData;
@ -74,6 +78,20 @@ final class PhabricatorEdgeQuery extends PhabricatorQuery {
} }
/**
* Configure the order edge results are returned in.
*
* @param const Order constant.
* @return this
*
* @task config
*/
public function setOrder($order) {
$this->order = $order;
return $this;
}
/** /**
* When loading edges, also load edge data. * When loading edges, also load edge data.
* *
@ -303,7 +321,11 @@ final class PhabricatorEdgeQuery extends PhabricatorQuery {
* @task internal * @task internal
*/ */
private function buildOrderClause($conn_r) { private function buildOrderClause($conn_r) {
return 'ORDER BY edge.dateCreated DESC, edge.seq ASC'; if ($this->order == self::ORDER_NEWEST_FIRST) {
return 'ORDER BY edge.dateCreated DESC, edge.seq DESC';
} else {
return 'ORDER BY edge.dateCreated ASC, edge.seq ASC';
}
} }
} }