mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-13 18:32:41 +01:00
424ba2e588
Summary: Ref T2222. This enriches mail a little bit to get these rendering pretty much like they do now. Test Plan: {F118255} Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2222 Differential Revision: https://secure.phabricator.com/D8343
888 lines
29 KiB
PHP
888 lines
29 KiB
PHP
<?php
|
|
|
|
final class DifferentialTransactionEditor
|
|
extends PhabricatorApplicationTransactionEditor {
|
|
|
|
public function getTransactionTypes() {
|
|
$types = parent::getTransactionTypes();
|
|
|
|
$types[] = PhabricatorTransactions::TYPE_COMMENT;
|
|
$types[] = PhabricatorTransactions::TYPE_EDGE;
|
|
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
|
|
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
|
|
|
|
$types[] = DifferentialTransaction::TYPE_ACTION;
|
|
$types[] = DifferentialTransaction::TYPE_INLINE;
|
|
$types[] = DifferentialTransaction::TYPE_STATUS;
|
|
|
|
/*
|
|
|
|
$types[] = DifferentialTransaction::TYPE_UPDATE;
|
|
*/
|
|
|
|
return $types;
|
|
}
|
|
|
|
protected function getCustomTransactionOldValue(
|
|
PhabricatorLiskDAO $object,
|
|
PhabricatorApplicationTransaction $xaction) {
|
|
|
|
switch ($xaction->getTransactionType()) {
|
|
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
|
return $object->getViewPolicy();
|
|
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
|
return $object->getEditPolicy();
|
|
case DifferentialTransaction::TYPE_ACTION:
|
|
return null;
|
|
case DifferentialTransaction::TYPE_INLINE:
|
|
return null;
|
|
}
|
|
|
|
return parent::getCustomTransactionOldValue($object, $xaction);
|
|
}
|
|
|
|
protected function getCustomTransactionNewValue(
|
|
PhabricatorLiskDAO $object,
|
|
PhabricatorApplicationTransaction $xaction) {
|
|
|
|
switch ($xaction->getTransactionType()) {
|
|
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
|
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
|
case DifferentialTransaction::TYPE_ACTION:
|
|
return $xaction->getNewValue();
|
|
case DifferentialTransaction::TYPE_INLINE:
|
|
return null;
|
|
}
|
|
|
|
return parent::getCustomTransactionNewValue($object, $xaction);
|
|
}
|
|
|
|
protected function transactionHasEffect(
|
|
PhabricatorLiskDAO $object,
|
|
PhabricatorApplicationTransaction $xaction) {
|
|
|
|
switch ($xaction->getTransactionType()) {
|
|
case DifferentialTransaction::TYPE_INLINE:
|
|
return $xaction->hasComment();
|
|
case DifferentialTransaction::TYPE_ACTION:
|
|
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
|
|
$status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
|
|
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
|
|
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
|
|
|
|
$action_type = $xaction->getNewValue();
|
|
switch ($action_type) {
|
|
case DifferentialAction::ACTION_ACCEPT:
|
|
case DifferentialAction::ACTION_REJECT:
|
|
if ($action_type == DifferentialAction::ACTION_ACCEPT) {
|
|
$new_status = DifferentialReviewerStatus::STATUS_ACCEPTED;
|
|
} else {
|
|
$new_status = DifferentialReviewerStatus::STATUS_REJECTED;
|
|
}
|
|
|
|
$actor = $this->getActor();
|
|
$actor_phid = $actor->getPHID();
|
|
|
|
// These transactions can cause effects in to ways: by altering the
|
|
// status of an existing reviewer; or by adding the actor as a new
|
|
// reviewer.
|
|
|
|
$will_add_reviewer = true;
|
|
foreach ($object->getReviewerStatus() as $reviewer) {
|
|
if ($reviewer->hasAuthority($actor)) {
|
|
if ($reviewer->getStatus() != $new_status) {
|
|
return true;
|
|
}
|
|
}
|
|
if ($reviewer->getReviewerPHID() == $actor_phid) {
|
|
$will_add_reviwer = false;
|
|
}
|
|
}
|
|
|
|
return $will_add_reviewer;
|
|
case DifferentialAction::ACTION_CLOSE:
|
|
return ($object->getStatus() != $status_closed);
|
|
case DifferentialAction::ACTION_ABANDON:
|
|
return ($object->getStatus() != $status_abandoned);
|
|
case DifferentialAction::ACTION_RECLAIM:
|
|
return ($object->getStatus() == $status_abandoned);
|
|
case DifferentialAction::ACTION_REOPEN:
|
|
return ($object->getStatus() == $status_closed);
|
|
case DifferentialAction::ACTION_RETHINK:
|
|
return ($object->getStatus() != $status_revision);
|
|
case DifferentialAction::ACTION_REQUEST:
|
|
return ($object->getStatus() != $status_review);
|
|
case DifferentialAction::ACTION_RESIGN:
|
|
$actor_phid = $this->getActor()->getPHID();
|
|
foreach ($object->getReviewerStatus() as $reviewer) {
|
|
if ($reviewer->getReviewerPHID() == $actor_phid) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
case DifferentialAction::ACTION_CLAIM:
|
|
$actor_phid = $this->getActor()->getPHID();
|
|
return ($actor_phid != $object->getAuthorPHID());
|
|
}
|
|
}
|
|
|
|
return parent::transactionHasEffect($object, $xaction);
|
|
}
|
|
|
|
|
|
protected function applyCustomInternalTransaction(
|
|
PhabricatorLiskDAO $object,
|
|
PhabricatorApplicationTransaction $xaction) {
|
|
|
|
switch ($xaction->getTransactionType()) {
|
|
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
|
$object->setViewPolicy($xaction->getNewValue());
|
|
return;
|
|
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
|
$object->setEditPolicy($xaction->getNewValue());
|
|
return;
|
|
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
|
|
case PhabricatorTransactions::TYPE_COMMENT:
|
|
case DifferentialTransaction::TYPE_INLINE:
|
|
return;
|
|
case PhabricatorTransactions::TYPE_EDGE:
|
|
return;
|
|
case DifferentialTransaction::TYPE_ACTION:
|
|
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
|
|
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
|
|
|
|
switch ($xaction->getNewValue()) {
|
|
case DifferentialAction::ACTION_RESIGN:
|
|
case DifferentialAction::ACTION_ACCEPT:
|
|
case DifferentialAction::ACTION_REJECT:
|
|
// These have no direct effects, and affect review status only
|
|
// indirectly by altering reviewers with TYPE_EDGE transactions.
|
|
return;
|
|
case DifferentialAction::ACTION_ABANDON:
|
|
$object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED);
|
|
return;
|
|
case DifferentialAction::ACTION_RETHINK:
|
|
$object->setStatus($status_revision);
|
|
return;
|
|
case DifferentialAction::ACTION_RECLAIM:
|
|
$object->setStatus($status_review);
|
|
return;
|
|
case DifferentialAction::ACTION_REOPEN:
|
|
$object->setStatus($status_review);
|
|
return;
|
|
case DifferentialAction::ACTION_REQUEST:
|
|
$object->setStatus($status_review);
|
|
return;
|
|
case DifferentialAction::ACTION_CLOSE:
|
|
$object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED);
|
|
return;
|
|
case DifferentialAction::ACTION_CLAIM:
|
|
$object->setAuthorPHID($this->getActor()->getPHID());
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return parent::applyCustomInternalTransaction($object, $xaction);
|
|
}
|
|
|
|
protected function expandTransaction(
|
|
PhabricatorLiskDAO $object,
|
|
PhabricatorApplicationTransaction $xaction) {
|
|
|
|
$actor = $this->getActor();
|
|
$actor_phid = $actor->getPHID();
|
|
$type_edge = PhabricatorTransactions::TYPE_EDGE;
|
|
$edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
|
|
|
|
$results = parent::expandTransaction($object, $xaction);
|
|
switch ($xaction->getTransactionType()) {
|
|
case PhabricatorTransactions::TYPE_COMMENT:
|
|
// When a user leaves a comment, upgrade their reviewer status from
|
|
// "added" to "commented" if they're also a reviewer. We may further
|
|
// upgrade this based on other actions in the transaction group.
|
|
|
|
$status_added = DifferentialReviewerStatus::STATUS_ADDED;
|
|
$status_commented = DifferentialReviewerStatus::STATUS_COMMENTED;
|
|
|
|
$data = array(
|
|
'status' => $status_commented,
|
|
);
|
|
|
|
$edits = array();
|
|
foreach ($object->getReviewerStatus() as $reviewer) {
|
|
if ($reviewer->getReviewerPHID() == $actor_phid) {
|
|
if ($reviewer->getStatus() == $status_added) {
|
|
$edits[$actor_phid] = array(
|
|
'data' => $data,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($edits) {
|
|
$results[] = id(new DifferentialTransaction())
|
|
->setTransactionType($type_edge)
|
|
->setMetadataValue('edge:type', $edge_reviewer)
|
|
->setIgnoreOnNoEffect(true)
|
|
->setNewValue(array('+' => $edits));
|
|
}
|
|
break;
|
|
|
|
case DifferentialTransaction::TYPE_ACTION:
|
|
$action_type = $xaction->getNewValue();
|
|
|
|
switch ($action_type) {
|
|
case DifferentialAction::ACTION_ACCEPT:
|
|
case DifferentialAction::ACTION_REJECT:
|
|
if ($action_type == DifferentialAction::ACTION_ACCEPT) {
|
|
$data = array(
|
|
'status' => DifferentialReviewerStatus::STATUS_ACCEPTED,
|
|
);
|
|
} else {
|
|
$data = array(
|
|
'status' => DifferentialReviewerStatus::STATUS_REJECTED,
|
|
);
|
|
}
|
|
|
|
$edits = array();
|
|
|
|
foreach ($object->getReviewerStatus() as $reviewer) {
|
|
if ($reviewer->hasAuthority($actor)) {
|
|
$edits[$reviewer->getReviewerPHID()] = array(
|
|
'data' => $data,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Also either update or add the actor themselves as a reviewer.
|
|
$edits[$actor_phid] = array(
|
|
'data' => $data,
|
|
);
|
|
|
|
$results[] = id(new DifferentialTransaction())
|
|
->setTransactionType($type_edge)
|
|
->setMetadataValue('edge:type', $edge_reviewer)
|
|
->setIgnoreOnNoEffect(true)
|
|
->setNewValue(array('+' => $edits));
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_CLAIM:
|
|
// If the user is commandeering, add the previous owner as a
|
|
// reviewer and remove the actor.
|
|
|
|
$edits = array(
|
|
'-' => array(
|
|
$actor_phid => $actor_phid,
|
|
),
|
|
);
|
|
|
|
$owner_phid = $object->getAuthorPHID();
|
|
if ($owner_phid) {
|
|
$reviewer = new DifferentialReviewer(
|
|
$owner_phid,
|
|
array(
|
|
'status' => DifferentialReviewerStatus::STATUS_ADDED,
|
|
));
|
|
|
|
$edits['+'] = array(
|
|
$owner_phid => array(
|
|
'data' => $reviewer->getEdgeData(),
|
|
),
|
|
);
|
|
}
|
|
|
|
$results[] = id(new DifferentialTransaction())
|
|
->setTransactionType($type_edge)
|
|
->setMetadataValue('edge:type', $edge_reviewer)
|
|
->setIgnoreOnNoEffect(true)
|
|
->setNewValue($edits);
|
|
|
|
break;
|
|
case DifferentialAction::ACTION_RESIGN:
|
|
// If the user is resigning, add a separate reviewer edit
|
|
// transaction which removes them as a reviewer.
|
|
|
|
$results[] = id(new DifferentialTransaction())
|
|
->setTransactionType($type_edge)
|
|
->setMetadataValue('edge:type', $edge_reviewer)
|
|
->setIgnoreOnNoEffect(true)
|
|
->setNewValue(
|
|
array(
|
|
'-' => array(
|
|
$actor_phid => $actor_phid,
|
|
),
|
|
));
|
|
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
protected function applyCustomExternalTransaction(
|
|
PhabricatorLiskDAO $object,
|
|
PhabricatorApplicationTransaction $xaction) {
|
|
|
|
switch ($xaction->getTransactionType()) {
|
|
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
|
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
|
return;
|
|
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
|
|
case PhabricatorTransactions::TYPE_EDGE:
|
|
case PhabricatorTransactions::TYPE_COMMENT:
|
|
case DifferentialTransaction::TYPE_ACTION:
|
|
case DifferentialTransaction::TYPE_INLINE:
|
|
return;
|
|
}
|
|
|
|
return parent::applyCustomExternalTransaction($object, $xaction);
|
|
}
|
|
|
|
protected function mergeEdgeData($type, array $u, array $v) {
|
|
$result = parent::mergeEdgeData($type, $u, $v);
|
|
|
|
switch ($type) {
|
|
case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER:
|
|
// When the same reviewer has their status updated by multiple
|
|
// transactions, we want the strongest status to win. An example of
|
|
// this is when a user adds a comment and also accepts a revision which
|
|
// they are a reviewer on. The comment creates a "commented" status,
|
|
// while the accept creates an "accepted" status. Since accept is
|
|
// stronger, it should win and persist.
|
|
|
|
$u_status = idx($u, 'status');
|
|
$v_status = idx($v, 'status');
|
|
$u_str = DifferentialReviewerStatus::getStatusStrength($u_status);
|
|
$v_str = DifferentialReviewerStatus::getStatusStrength($v_status);
|
|
if ($u_str > $v_str) {
|
|
$result['status'] = $u_status;
|
|
} else {
|
|
$result['status'] = $v_status;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
|
|
protected function applyFinalEffects(
|
|
PhabricatorLiskDAO $object,
|
|
array $xactions) {
|
|
|
|
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
|
|
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
|
|
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
|
|
|
|
$old_status = $object->getStatus();
|
|
switch ($old_status) {
|
|
case $status_accepted:
|
|
case $status_revision:
|
|
case $status_review:
|
|
// Load the most up-to-date version of the revision and its reviewers,
|
|
// so we don't need to try to deduce the state of reviewers by examining
|
|
// all the changes made by the transactions.
|
|
$new_revision = id(new DifferentialRevisionQuery())
|
|
->setViewer($this->getActor())
|
|
->needReviewerStatus(true)
|
|
->withIDs(array($object->getID()))
|
|
->executeOne();
|
|
if (!$new_revision) {
|
|
throw new Exception(
|
|
pht('Failed to load revision from transaction finalization.'));
|
|
}
|
|
|
|
// Try to move a revision to "accepted". We look for:
|
|
//
|
|
// - at least one accepting reviewer who is a user; and
|
|
// - no rejects; and
|
|
// - no blocking reviewers.
|
|
|
|
$has_accepting_user = false;
|
|
$has_rejecting_reviewer = false;
|
|
$has_blocking_reviewer = false;
|
|
foreach ($new_revision->getReviewerStatus() as $reviewer) {
|
|
$reviewer_status = $reviewer->getStatus();
|
|
switch ($reviewer_status) {
|
|
case DifferentialReviewerStatus::STATUS_REJECTED:
|
|
$has_rejecting_reviewer = true;
|
|
break;
|
|
case DifferentialReviewerStatus::STATUS_BLOCKING:
|
|
$has_blocking_reviewer = true;
|
|
break;
|
|
case DifferentialReviewerStatus::STATUS_ACCEPTED:
|
|
if ($reviewer->isUser()) {
|
|
$has_accepting_user = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
$new_status = null;
|
|
if ($has_accepting_user &&
|
|
!$has_rejecting_reviewer &&
|
|
!$has_blocking_reviewer) {
|
|
$new_status = $status_accepted;
|
|
} else if ($has_rejecting_reviewer) {
|
|
// This isn't accepted, and there's at least one rejecting reviewer,
|
|
// so the revision needs changes. This usually happens after a
|
|
// "reject".
|
|
$new_status = $status_revision;
|
|
} else if ($old_status == $status_accepted) {
|
|
// This revision was accepted, but it no longer satisfies the
|
|
// conditions for acceptance. This usually happens after an accepting
|
|
// reviewer resigns or is removed.
|
|
$new_status = $status_review;
|
|
}
|
|
|
|
if ($new_status !== null && $new_status != $old_status) {
|
|
$xaction = id(new DifferentialTransaction())
|
|
->setTransactionType(DifferentialTransaction::TYPE_STATUS)
|
|
->setOldValue($old_status)
|
|
->setNewValue($new_status);
|
|
|
|
$xaction = $this->populateTransaction($object, $xaction)->save();
|
|
|
|
$xactions[] = $xaction;
|
|
|
|
$object->setStatus($new_status)->save();
|
|
}
|
|
break;
|
|
default:
|
|
// Revisions can't transition out of other statuses (like closed or
|
|
// abandoned) as a side effect of reviewer status changes.
|
|
break;
|
|
}
|
|
|
|
return $xactions;
|
|
}
|
|
|
|
|
|
protected function validateTransaction(
|
|
PhabricatorLiskDAO $object,
|
|
$type,
|
|
array $xactions) {
|
|
|
|
$errors = parent::validateTransaction($object, $type, $xactions);
|
|
|
|
foreach ($xactions as $xaction) {
|
|
switch ($type) {
|
|
case DifferentialTransaction::TYPE_ACTION:
|
|
$error = $this->validateDifferentialAction(
|
|
$object,
|
|
$type,
|
|
$xaction,
|
|
$xaction->getNewValue());
|
|
if ($error) {
|
|
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
|
$type,
|
|
pht('Invalid'),
|
|
$error,
|
|
$xaction);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
private function validateDifferentialAction(
|
|
DifferentialRevision $revision,
|
|
$type,
|
|
DifferentialTransaction $xaction,
|
|
$action) {
|
|
|
|
$author_phid = $revision->getAuthorPHID();
|
|
$actor_phid = $this->getActor()->getPHID();
|
|
$actor_is_author = ($author_phid == $actor_phid);
|
|
|
|
$config_close_key = 'differential.always-allow-close';
|
|
$always_allow_close = PhabricatorEnv::getEnvConfig($config_close_key);
|
|
|
|
$config_reopen_key = 'differential.allow-reopen';
|
|
$allow_reopen = PhabricatorEnv::getEnvConfig($config_reopen_key);
|
|
|
|
$config_self_accept_key = 'differential.allow-self-accept';
|
|
$allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
|
|
|
|
$revision_status = $revision->getStatus();
|
|
|
|
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
|
|
$status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
|
|
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
|
|
|
|
switch ($action) {
|
|
case DifferentialAction::ACTION_ACCEPT:
|
|
if ($actor_is_author && !$allow_self_accept) {
|
|
return pht(
|
|
'You can not accept this revision because you are the owner.');
|
|
}
|
|
|
|
if ($revision_status == $status_abandoned) {
|
|
return pht(
|
|
'You can not accept this revision because it has been '.
|
|
'abandoned.');
|
|
}
|
|
|
|
if ($revision_status == $status_closed) {
|
|
return pht(
|
|
'You can not accept this revision because it has already been '.
|
|
'closed.');
|
|
}
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_REJECT:
|
|
if ($actor_is_author) {
|
|
return pht(
|
|
'You can not request changes to your own revision.');
|
|
}
|
|
|
|
if ($revision_status == $status_abandoned) {
|
|
return pht(
|
|
'You can not request changes to this revision because it has been '.
|
|
'abandoned.');
|
|
}
|
|
|
|
if ($revision_status == $status_closed) {
|
|
return pht(
|
|
'You can not request changes to this revision because it has '.
|
|
'already been closed.');
|
|
}
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_RESIGN:
|
|
// You can always resign from a revision if you're a reviewer. If you
|
|
// aren't, this is a no-op rather than invalid.
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_CLAIM:
|
|
// You can claim a revision if you're not the owner. If you are, this
|
|
// is a no-op rather than invalid.
|
|
|
|
if ($revision_status == $status_closed) {
|
|
return pht(
|
|
"You can not commandeer this revision because it has already been ".
|
|
"closed.");
|
|
}
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_ABANDON:
|
|
if (!$actor_is_author) {
|
|
return pht(
|
|
"You can not abandon this revision because you do not own it. ".
|
|
"You can only abandon revisions you own.");
|
|
}
|
|
|
|
if ($revision_status == $status_closed) {
|
|
return pht(
|
|
"You can not abandon this revision because it has already been ".
|
|
"closed.");
|
|
}
|
|
|
|
// NOTE: Abandons of already-abandoned revisions are treated as no-op
|
|
// instead of invalid. Other abandons are OK.
|
|
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_RECLAIM:
|
|
if (!$actor_is_author) {
|
|
return pht(
|
|
"You can not reclaim this revision because you do not own ".
|
|
"it. You can only reclaim revisions you own.");
|
|
}
|
|
|
|
if ($revision_status == $status_closed) {
|
|
return pht(
|
|
"You can not reclaim this revision because it has already been ".
|
|
"closed.");
|
|
}
|
|
|
|
// NOTE: Reclaims of other non-abandoned revisions are treated as no-op
|
|
// instead of invalid.
|
|
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_REOPEN:
|
|
if (!$allow_reopen) {
|
|
return pht(
|
|
'The reopen action is not enabled on this Phabricator install. '.
|
|
'Adjust your configuration to enable it.');
|
|
}
|
|
|
|
// NOTE: If the revision is not closed, this is caught as a no-op
|
|
// instead of an invalid transaction.
|
|
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_RETHINK:
|
|
if (!$actor_is_author) {
|
|
return pht(
|
|
"You can not plan changes to this revision because you do not ".
|
|
"own it. To plan chagnes to a revision, you must be its owner.");
|
|
}
|
|
|
|
switch ($revision_status) {
|
|
case ArcanistDifferentialRevisionStatus::ACCEPTED:
|
|
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
|
|
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
|
|
// These are OK.
|
|
break;
|
|
case ArcanistDifferentialRevisionStatus::ABANDONED:
|
|
return pht(
|
|
"You can not plan changes to this revision because it has ".
|
|
"been abandoned.");
|
|
case ArcanistDifferentialRevisionStatus::CLOSED:
|
|
return pht(
|
|
"You can not plan changes to this revision because it has ".
|
|
"already been closed.");
|
|
default:
|
|
throw new Exception(
|
|
pht(
|
|
'Encountered unexpected revision status ("%s") when '.
|
|
'validating "%s" action.',
|
|
$revision_status,
|
|
$action));
|
|
}
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_REQUEST:
|
|
if (!$actor_is_author) {
|
|
return pht(
|
|
"You can not request review of this revision because you do ".
|
|
"not own it. To request review of a revision, you must be its ".
|
|
"owner.");
|
|
}
|
|
|
|
switch ($revision_status) {
|
|
case ArcanistDifferentialRevisionStatus::ACCEPTED:
|
|
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
|
|
// These are OK.
|
|
break;
|
|
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
|
|
// This will be caught as "no effect" later on.
|
|
break;
|
|
case ArcanistDifferentialRevisionStatus::ABANDONED:
|
|
return pht(
|
|
"You can not request review of this revision because it has ".
|
|
"been abandoned. Instead, reclaim it.");
|
|
case ArcanistDifferentialRevisionStatus::CLOSED:
|
|
return pht(
|
|
"You can not request review of this revision because it has ".
|
|
"already been closed.");
|
|
default:
|
|
throw new Exception(
|
|
pht(
|
|
'Encountered unexpected revision status ("%s") when '.
|
|
'validating "%s" action.',
|
|
$revision_status,
|
|
$action));
|
|
}
|
|
break;
|
|
|
|
case DifferentialAction::ACTION_CLOSE:
|
|
|
|
// TODO: Permit the daemons to take this action in all cases.
|
|
|
|
if (!$actor_is_author && !$always_allow_close) {
|
|
return pht(
|
|
"You can not close this revision because you do not own it. To ".
|
|
"close a revision, you must be its owner.");
|
|
}
|
|
|
|
if ($revision_status != $status_accepted) {
|
|
return pht(
|
|
"You can not close this revision because it has not been ".
|
|
"accepted. You can only close accepted revisions.");
|
|
}
|
|
break;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function sortTransactions(array $xactions) {
|
|
$head = array();
|
|
$tail = array();
|
|
|
|
// Move bare comments to the end, so the actions precede them.
|
|
foreach ($xactions as $xaction) {
|
|
$type = $xaction->getTransactionType();
|
|
if ($type == DifferentialTransaction::TYPE_INLINE) {
|
|
$tail[] = $xaction;
|
|
} else {
|
|
$head[] = $xaction;
|
|
}
|
|
}
|
|
|
|
return array_values(array_merge($head, $tail));
|
|
}
|
|
|
|
protected function requireCapabilities(
|
|
PhabricatorLiskDAO $object,
|
|
PhabricatorApplicationTransaction $xaction) {
|
|
|
|
switch ($xaction->getTransactionType()) {
|
|
}
|
|
|
|
return parent::requireCapabilities($object, $xaction);
|
|
}
|
|
|
|
protected function shouldSendMail(
|
|
PhabricatorLiskDAO $object,
|
|
array $xactions) {
|
|
return true;
|
|
}
|
|
|
|
protected function getMailTo(PhabricatorLiskDAO $object) {
|
|
$phids = array();
|
|
$phids[] = $object->getAuthorPHID();
|
|
foreach ($object->getReviewerStatus() as $reviewer) {
|
|
$phids[] = $reviewer->getReviewerPHID();
|
|
}
|
|
return $phids;
|
|
}
|
|
|
|
protected function getMailSubjectPrefix() {
|
|
return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
|
|
}
|
|
|
|
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
|
|
return id(new DifferentialReplyHandler())
|
|
->setMailReceiver($object);
|
|
}
|
|
|
|
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
|
|
$id = $object->getID();
|
|
$title = $object->getTitle();
|
|
|
|
$original_title = $object->getOriginalTitle();
|
|
|
|
$subject = "D{$id}: {$title}";
|
|
$thread_topic = "D{$id}: {$original_title}";
|
|
|
|
return id(new PhabricatorMetaMTAMail())
|
|
->setSubject($subject)
|
|
->addHeader('Thread-Topic', $thread_topic);
|
|
}
|
|
|
|
protected function buildMailBody(
|
|
PhabricatorLiskDAO $object,
|
|
array $xactions) {
|
|
|
|
$body = parent::buildMailBody($object, $xactions);
|
|
|
|
$type_inline = DifferentialTransaction::TYPE_INLINE;
|
|
|
|
$inlines = array();
|
|
foreach ($xactions as $xaction) {
|
|
if ($xaction->getTransactionType() == $type_inline) {
|
|
$inlines[] = $xaction;
|
|
}
|
|
}
|
|
|
|
if ($inlines) {
|
|
$body->addTextSection(
|
|
pht('INLINE COMMENTS'),
|
|
$this->renderInlineCommentsForMail($object, $inlines));
|
|
}
|
|
|
|
$body->addTextSection(
|
|
pht('REVISION DETAIL'),
|
|
PhabricatorEnv::getProductionURI('/D'.$object->getID()));
|
|
|
|
|
|
return $body;
|
|
}
|
|
|
|
protected function supportsSearch() {
|
|
return true;
|
|
}
|
|
|
|
protected function extractFilePHIDsFromCustomTransaction(
|
|
PhabricatorLiskDAO $object,
|
|
PhabricatorApplicationTransaction $xaction) {
|
|
|
|
switch ($xaction->getTransactionType()) {
|
|
}
|
|
|
|
return parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
|
|
}
|
|
|
|
private function renderInlineCommentsForMail(
|
|
PhabricatorLiskDAO $object,
|
|
array $inlines) {
|
|
|
|
$context_key = 'metamta.differential.unified-comment-context';
|
|
$show_context = PhabricatorEnv::getEnvConfig($context_key);
|
|
|
|
$changeset_ids = array();
|
|
foreach ($inlines as $inline) {
|
|
$id = $inline->getComment()->getChangesetID();
|
|
$changeset_ids[$id] = $id;
|
|
}
|
|
|
|
// TODO: We should write a proper Query class for this eventually.
|
|
$changesets = id(new DifferentialChangeset())->loadAllWhere(
|
|
'id IN (%Ld)',
|
|
$changeset_ids);
|
|
if ($show_context) {
|
|
$hunk_parser = new DifferentialHunkParser();
|
|
foreach ($changesets as $changeset) {
|
|
$changeset->attachHunks($changeset->loadHunks());
|
|
}
|
|
}
|
|
|
|
$inline_groups = DifferentialTransactionComment::sortAndGroupInlines(
|
|
$inlines,
|
|
$changesets);
|
|
|
|
$result = array();
|
|
foreach ($inline_groups as $changeset_id => $group) {
|
|
$changeset = idx($changesets, $changeset_id);
|
|
if (!$changeset) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($group as $inline) {
|
|
$comment = $inline->getComment();
|
|
$file = $changeset->getFilename();
|
|
$start = $comment->getLineNumber();
|
|
$len = $comment->getLineLength();
|
|
if ($len) {
|
|
$range = $start.'-'.($start + $len);
|
|
} else {
|
|
$range = $start;
|
|
}
|
|
|
|
$inline_content = $comment->getContent();
|
|
|
|
if (!$show_context) {
|
|
$result[] = "{$file}:{$range} {$inline_content}";
|
|
} else {
|
|
$result[] = "================";
|
|
$result[] = "Comment at: " . $file . ":" . $range;
|
|
$result[] = $hunk_parser->makeContextDiff(
|
|
$changeset->getHunks(),
|
|
$comment->getIsNewFile(),
|
|
$comment->getLineNumber(),
|
|
$comment->getLineLength(),
|
|
1);
|
|
$result[] = "----------------";
|
|
|
|
$result[] = $inline_content;
|
|
$result[] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
return implode("\n", $result);
|
|
}
|
|
|
|
|
|
|
|
}
|