Page MenuHomestyx hydra

No OneTemporary

diff --git a/resources/sql/autopatches/20180312.reviewers.01.options.sql b/resources/sql/autopatches/20180312.reviewers.01.options.sql
new file mode 100644
index 0000000000..159426614d
--- /dev/null
+++ b/resources/sql/autopatches/20180312.reviewers.01.options.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_differential.differential_reviewer
+ ADD options LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql b/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql
new file mode 100644
index 0000000000..d509011f73
--- /dev/null
+++ b/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_differential.differential_reviewer
+ SET options = '{}' WHERE options = '';
diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index d04b0d684a..9882246608 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1,1657 +1,1639 @@
<?php
final class DifferentialTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $changedPriorToCommitURI;
private $isCloseByCommit;
private $repositoryPHIDOverride = false;
private $didExpandInlineState = false;
private $hasReviewTransaction = false;
private $affectedPaths;
private $firstBroadcast = false;
private $wasDraft = false;
public function getEditorApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getEditorObjectsDescription() {
return pht('Differential Revisions');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this revision.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function isFirstBroadcast() {
return $this->firstBroadcast;
}
public function getDiffUpdateTransaction(array $xactions) {
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_update) {
return $xaction;
}
}
return null;
}
public function setIsCloseByCommit($is_close_by_commit) {
$this->isCloseByCommit = $is_close_by_commit;
return $this;
}
public function getIsCloseByCommit() {
return $this->isCloseByCommit;
}
public function setChangedPriorToCommitURI($uri) {
$this->changedPriorToCommitURI = $uri;
return $this;
}
public function getChangedPriorToCommitURI() {
return $this->changedPriorToCommitURI;
}
public function setRepositoryPHIDOverride($phid_or_null) {
$this->repositoryPHIDOverride = $phid_or_null;
return $this;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_INLINESTATE;
$types[] = DifferentialTransaction::TYPE_INLINE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return null;
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
// If we have an "Inline State" transaction already, the caller
// built it for us so we don't need to expand it again.
$this->didExpandInlineState = true;
break;
case DifferentialRevisionAcceptTransaction::TRANSACTIONTYPE:
case DifferentialRevisionRejectTransaction::TRANSACTIONTYPE:
case DifferentialRevisionResignTransaction::TRANSACTIONTYPE:
// If we have a review transaction, we'll skip marking the user
// as "Commented" later. This should get cleaner after T10967.
$this->hasReviewTransaction = true;
break;
}
}
$this->wasDraft = $object->isDraft();
return parent::expandTransactions($object, $xactions);
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$actor = $this->getActor();
$actor_phid = $this->getActingAsPHID();
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;
- $is_sticky_accept = PhabricatorEnv::getEnvConfig(
- 'differential.sticky-accept');
-
- $downgrade_rejects = false;
- $downgrade_accepts = false;
+ $want_downgrade = array();
+ $must_downgrade = array();
if ($this->getIsCloseByCommit()) {
// Never downgrade reviewers when we're closing a revision after a
// commit.
} else {
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
- $downgrade_rejects = true;
- if (!$is_sticky_accept) {
- // If "sticky accept" is disabled, also downgrade the accepts.
- $downgrade_accepts = true;
- }
+ $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
+ $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
break;
case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE:
- $downgrade_rejects = true;
- if ((!$is_sticky_accept) ||
- (!$object->isChangePlanned())) {
- // If the old state isn't "changes planned", downgrade the accepts.
- // This exception allows an accepted revision to go through
- // "Plan Changes" -> "Request Review" and return to "accepted" if
- // the author didn't update the revision, essentially undoing the
- // "Plan Changes".
- $downgrade_accepts = true;
+ if (!$object->isChangePlanned()) {
+ // If the old state isn't "Changes Planned", downgrade the accepts
+ // even if they're sticky.
+
+ // We don't downgrade for "Changes Planned" to allow an author to
+ // undo a "Plan Changes" by immediately following it up with a
+ // "Request Review".
+ $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
+ $must_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
}
+ $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
break;
}
}
- $new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED;
- $new_reject = DifferentialReviewerStatus::STATUS_REJECTED;
- $old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER;
- $old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER;
-
- $downgrade = array();
- if ($downgrade_accepts) {
- $downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
- }
-
- if ($downgrade_rejects) {
- $downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
- }
-
- if ($downgrade) {
+ if ($want_downgrade) {
$void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE;
$results[] = id(new DifferentialTransaction())
->setTransactionType($void_type)
->setIgnoreOnNoEffect(true)
- ->setNewValue($downgrade);
+ ->setMetadataValue('void.force', $must_downgrade)
+ ->setNewValue($want_downgrade);
}
$is_commandeer = false;
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
if ($this->getIsCloseByCommit()) {
// Don't bother with any of this if this update is a side effect of
// commit detection.
break;
}
// When a revision is updated and the diff comes from a branch named
// "T123" or similar, automatically associate the commit with the
// task that the branch names.
$maniphest = 'PhabricatorManiphestApplication';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$diff = $this->requireDiff($xaction->getNewValue());
$branch = $diff->getBranch();
// No "$", to allow for branches like T123_demo.
$match = null;
if (preg_match('/^T(\d+)/i', $branch, $match)) {
$task_id = $match[1];
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array($task_id))
->execute();
if ($tasks) {
$task = head($tasks);
$task_phid = $task->getPHID();
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_ref_task)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => array($task_phid => $task_phid)));
}
}
}
break;
case DifferentialRevisionCommandeerTransaction::TRANSACTIONTYPE:
$is_commandeer = true;
break;
}
if ($is_commandeer) {
$results[] = $this->newCommandeerReviewerTransaction($object);
}
if (!$this->didExpandInlineState) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
case DifferentialTransaction::TYPE_INLINE:
$this->didExpandInlineState = true;
$actor_phid = $this->getActingAsPHID();
$actor_is_author = ($object->getAuthorPHID() == $actor_phid);
if (!$actor_is_author) {
break;
}
$state_map = PhabricatorTransactions::getInlineStateMap();
$inlines = id(new DifferentialDiffInlineCommentQuery())
->setViewer($this->getActor())
->withRevisionPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map))
->execute();
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$results[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}
return $results;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
$reply = $xaction->getComment()->getReplyToComment();
if ($reply && !$reply->getHasReplies()) {
$reply->setHasReplies(1)->save();
}
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$table = new DifferentialTransactionComment();
$conn_w = $table->establishConnection('w');
foreach ($xaction->getNewValue() as $phid => $state) {
queryfx(
$conn_w,
'UPDATE %T SET fixedState = %s WHERE phid = %s',
$table->getTableName(),
$state,
$phid);
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// 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. Then, update the reviewers
// on the object to make sure we're acting on the current reviewer set
// (and, for example, sending mail to the right people).
$new_revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->needActiveDiffs(true)
->withIDs(array($object->getID()))
->executeOne();
if (!$new_revision) {
throw new Exception(
pht('Failed to load revision from transaction finalization.'));
}
$object->attachReviewers($new_revision->getReviewers());
$object->attachActiveDiff($new_revision->getActiveDiff());
$object->attachRepository($new_revision->getRepository());
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$diff = $this->requireDiff($xaction->getNewValue(), true);
// Update these denormalized index tables when we attach a new
// diff to a revision.
$this->updateRevisionHashTable($object, $diff);
$this->updateAffectedPathTable($object, $diff);
break;
}
}
$xactions = $this->updateReviewStatus($object, $xactions);
$this->markReviewerComments($object, $xactions);
return $xactions;
}
private function updateReviewStatus(
DifferentialRevision $revision,
array $xactions) {
$was_accepted = $revision->isAccepted();
$was_revision = $revision->isNeedsRevision();
$was_review = $revision->isNeedsReview();
if (!$was_accepted && !$was_revision && !$was_review) {
// Revisions can't transition out of other statuses (like closed or
// abandoned) as a side effect of reviewer status changes.
return $xactions;
}
// Try to move a revision to "accepted". We look for:
//
// - at least one accepting reviewer who is a user; and
// - no rejects; and
// - no rejects of older diffs; and
// - no blocking reviewers.
$has_accepting_user = false;
$has_rejecting_reviewer = false;
$has_rejecting_older_reviewer = false;
$has_blocking_reviewer = false;
$active_diff = $revision->getActiveDiff();
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_status = $reviewer->getReviewerStatus();
switch ($reviewer_status) {
case DifferentialReviewerStatus::STATUS_REJECTED:
$active_phid = $active_diff->getPHID();
if ($reviewer->isRejected($active_phid)) {
$has_rejecting_reviewer = true;
} else {
$has_rejecting_older_reviewer = true;
}
break;
case DifferentialReviewerStatus::STATUS_REJECTED_OLDER:
$has_rejecting_older_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_BLOCKING:
$has_blocking_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_ACCEPTED:
if ($reviewer->isUser()) {
$active_phid = $active_diff->getPHID();
if ($reviewer->isAccepted($active_phid)) {
$has_accepting_user = true;
}
}
break;
}
}
$new_status = null;
if ($has_accepting_user &&
!$has_rejecting_reviewer &&
!$has_rejecting_older_reviewer &&
!$has_blocking_reviewer) {
$new_status = DifferentialRevisionStatus::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 = DifferentialRevisionStatus::NEEDS_REVISION;
} else if ($was_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 = DifferentialRevisionStatus::NEEDS_REVIEW;
}
if ($new_status === null) {
return $xactions;
}
$old_status = $revision->getModernRevisionStatus();
if ($new_status == $old_status) {
return $xactions;
}
$xaction = id(new DifferentialTransaction())
->setTransactionType(
DifferentialRevisionStatusTransaction::TRANSACTIONTYPE)
->setOldValue($old_status)
->setNewValue($new_status);
$xaction = $this->populateTransaction($revision, $xaction)
->save();
$xactions[] = $xaction;
// Save the status adjustment we made earlier.
$revision
->setModernRevisionStatus($new_status)
->save();
return $xactions;
}
protected function sortTransactions(array $xactions) {
$xactions = parent::sortTransactions($xactions);
$head = array();
$tail = array();
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 shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$object->shouldBroadcast()) {
return false;
}
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$object->shouldBroadcast()) {
return false;
}
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$this->requireReviewers($object);
$phids = array();
$phids[] = $object->getAuthorPHID();
foreach ($object->getReviewers() as $reviewer) {
if ($reviewer->isResigned()) {
continue;
}
$phids[] = $reviewer->getReviewerPHID();
}
return $phids;
}
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
$this->requireReviewers($object);
$phids = array();
foreach ($object->getReviewers() as $reviewer) {
if ($reviewer->isResigned()) {
$phids[] = $reviewer->getReviewerPHID();
}
}
return $phids;
}
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
$show_lines = false;
if ($this->isFirstBroadcast()) {
$action = pht('Request');
$show_lines = true;
} else {
$action = parent::getMailAction($object, $xactions);
$strongest = $this->getStrongestAction($object, $xactions);
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
if ($strongest->getTransactionType() == $type_update) {
$show_lines = true;
}
}
if ($show_lines) {
$count = new PhutilNumber($object->getLineCount());
$action = pht('%s, %s line(s)', $action, $count);
}
return $action;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
// This is nonstandard, but retains threading with older messages.
$phid = $object->getPHID();
return "differential-rev-{$phid}-req";
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new DifferentialReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
$subject = "D{$id}: {$title}";
return id(new PhabricatorMetaMTAMail())
->setSubject($subject);
}
protected function getTransactionsForMail(
PhabricatorLiskDAO $object,
array $xactions) {
// If this is the first time we're sending mail about this revision, we
// generate mail for all prior transactions, not just whatever is being
// applied now. This gets the "added reviewers" lines and other relevant
// information into the mail.
if ($this->isFirstBroadcast()) {
return $this->loadUnbroadcastTransactions($object);
}
return $xactions;
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$viewer = $this->requireActor();
$body = new PhabricatorMetaMTAMailBody();
$body->setViewer($this->requireActor());
$revision_uri = PhabricatorEnv::getProductionURI('/D'.$object->getID());
$this->addHeadersAndCommentsToMailBody(
$body,
$xactions,
pht('View Revision'),
$revision_uri);
$type_inline = DifferentialTransaction::TYPE_INLINE;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
}
if ($inlines) {
$this->appendInlineCommentsForMail($object, $inlines, $body);
}
$changed_uri = $this->getChangedPriorToCommitURI();
if ($changed_uri) {
$body->addLinkSection(
pht('CHANGED PRIOR TO COMMIT'),
$changed_uri);
}
$this->addCustomFieldsToMailBody($body, $object, $xactions);
$body->addLinkSection(
pht('REVISION DETAIL'),
$revision_uri);
$update_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$update_xaction = $xaction;
break;
}
}
if ($update_xaction) {
$diff = $this->requireDiff($update_xaction->getNewValue(), true);
$body->addTextSection(
pht('AFFECTED FILES'),
$this->renderAffectedFilesForMail($diff));
$config_key_inline = 'metamta.differential.inline-patches';
$config_inline = PhabricatorEnv::getEnvConfig($config_key_inline);
$config_key_attach = 'metamta.differential.attach-patches';
$config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);
if ($config_inline || $config_attach) {
$body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
$patch = $this->buildPatchForMail($diff);
if ($config_inline) {
$lines = substr_count($patch, "\n");
$bytes = strlen($patch);
// Limit the patch size to the smaller of 256 bytes per line or
// the mail body limit. This prevents degenerate behavior for patches
// with one line that is 10MB long. See T11748.
$byte_limits = array();
$byte_limits[] = (256 * $config_inline);
$byte_limits[] = $body_limit;
$byte_limit = min($byte_limits);
$lines_ok = ($lines <= $config_inline);
$bytes_ok = ($bytes <= $byte_limit);
if ($lines_ok && $bytes_ok) {
$this->appendChangeDetailsForMail($object, $diff, $patch, $body);
} else {
// TODO: Provide a helpful message about the patch being too
// large or lengthy here.
}
}
if ($config_attach) {
// See T12033, T11767, and PHI55. This is a crude fix to stop the
// major concrete problems that lackluster email size limits cause.
if (strlen($patch) < $body_limit) {
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
$mime_type = 'text/x-patch; charset=utf-8';
$body->addAttachment(
new PhabricatorMetaMTAAttachment($patch, $name, $mime_type));
}
}
}
}
return $body;
}
public function getMailTagsMap() {
return array(
DifferentialTransaction::MAILTAG_REVIEW_REQUEST =>
pht('A revision is created.'),
DifferentialTransaction::MAILTAG_UPDATED =>
pht('A revision is updated.'),
DifferentialTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a revision.'),
DifferentialTransaction::MAILTAG_CLOSED =>
pht('A revision is closed.'),
DifferentialTransaction::MAILTAG_REVIEWERS =>
pht("A revision's reviewers change."),
DifferentialTransaction::MAILTAG_CC =>
pht("A revision's CCs change."),
DifferentialTransaction::MAILTAG_OTHER =>
pht('Other revision activity not listed above occurs.'),
);
}
protected function supportsSearch() {
return true;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
// For "Fixes ..." and "Depends on ...", we're only going to look at
// content blocks which are part of the revision itself (like "Summary"
// and "Test Plan"), not comments.
$content_parts = array();
foreach ($changes as $change) {
if ($change->getTransaction()->isCommentTransaction()) {
continue;
}
$content_parts[] = $change->getNewValue();
}
if (!$content_parts) {
return array();
}
$content_block = implode("\n\n", $content_parts);
$task_map = array();
$task_refs = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($content_block);
foreach ($task_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$task_id = (int)trim($monogram, 'tT');
$task_map[$task_id] = true;
}
}
$rev_map = array();
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($content_block);
foreach ($rev_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$rev_id = (int)trim($monogram, 'dD');
$rev_map[$rev_id] = true;
}
}
$edges = array();
$task_phids = array();
$rev_phids = array();
if ($task_map) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array_keys($task_map))
->execute();
if ($tasks) {
$task_phids = mpull($tasks, 'getPHID', 'getPHID');
$edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$edges[$edge_related] = $task_phids;
}
}
if ($rev_map) {
$revs = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withIDs(array_keys($rev_map))
->execute();
$rev_phids = mpull($revs, 'getPHID', 'getPHID');
// NOTE: Skip any write attempts if a user cleverly implies a revision
// depends upon itself.
unset($rev_phids[$object->getPHID()]);
if ($revs) {
$depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
$edges[$depends] = $rev_phids;
}
}
$revert_refs = id(new DifferentialCustomFieldRevertsParser())
->parseCorpus($content_block);
$revert_monograms = array();
foreach ($revert_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$revert_monograms[] = $monogram;
}
}
if ($revert_monograms) {
$revert_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withNames($revert_monograms)
->withTypes(
array(
DifferentialRevisionPHIDType::TYPECONST,
PhabricatorRepositoryCommitPHIDType::TYPECONST,
))
->execute();
$revert_phids = mpull($revert_objects, 'getPHID', 'getPHID');
// Don't let an object revert itself, although other silly stuff like
// cycles of objects reverting each other is not prevented.
unset($revert_phids[$object->getPHID()]);
$revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST;
$edges[$revert_type] = $revert_phids;
} else {
$revert_phids = array();
}
$this->setUnmentionablePHIDMap(
array_merge(
$task_phids,
$rev_phids,
$revert_phids));
$result = array();
foreach ($edges as $type => $specs) {
$result[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $type)
->setNewValue(array('+' => $specs));
}
return $result;
}
private function appendInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inlines,
PhabricatorMetaMTAMailBody $body) {
$section = id(new DifferentialInlineCommentMailView())
->setViewer($this->getActor())
->setInlines($inlines)
->buildMailSection();
$header = pht('INLINE COMMENTS');
$section_text = "\n".$section->getPlaintext();
$style = array(
'margin: 6px 0 12px 0;',
);
$section_html = phutil_tag(
'div',
array(
'style' => implode(' ', $style),
),
$section->getHTML());
$body->addPlaintextSection($header, $section_text, false);
$body->addHTMLSection($header, $section_html);
}
private function appendChangeDetailsForMail(
PhabricatorLiskDAO $object,
DifferentialDiff $diff,
$patch,
PhabricatorMetaMTAMailBody $body) {
$section = id(new DifferentialChangeDetailMailView())
->setViewer($this->getActor())
->setDiff($diff)
->setPatch($patch)
->buildMailSection();
$header = pht('CHANGE DETAILS');
$section_text = "\n".$section->getPlaintext();
$style = array(
'margin: 6px 0 12px 0;',
);
$section_html = phutil_tag(
'div',
array(
'style' => implode(' ', $style),
),
$section->getHTML());
$body->addPlaintextSection($header, $section_text, false);
$body->addHTMLSection($header, $section_html);
}
private function loadDiff($phid, $need_changesets = false) {
$query = id(new DifferentialDiffQuery())
->withPHIDs(array($phid))
->setViewer($this->getActor());
if ($need_changesets) {
$query->needChangesets(true);
}
return $query->executeOne();
}
public function requireDiff($phid, $need_changesets = false) {
$diff = $this->loadDiff($phid, $need_changesets);
if (!$diff) {
throw new Exception(pht('Diff "%s" does not exist!', $phid));
}
return $diff;
}
/* -( Herald Integration )------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$repository = $object->getRepository();
if (!$repository) {
return array();
}
if (!$this->affectedPaths) {
return array();
}
$packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$this->affectedPaths);
if (!$packages) {
return array();
}
// Identify the packages with "Non-Owner Author" review rules and remove
// them if the author has authority over the package.
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$need_authority = array();
foreach ($packages as $package) {
$autoreview_setting = $package->getAutoReview();
$spec = idx($autoreview_map, $autoreview_setting);
if (!$spec) {
continue;
}
if (idx($spec, 'authority')) {
$need_authority[$package->getPHID()] = $package->getPHID();
}
}
if ($need_authority) {
$authority = id(new PhabricatorOwnersPackageQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($need_authority)
->withAuthorityPHIDs(array($object->getAuthorPHID()))
->execute();
$authority = mpull($authority, null, 'getPHID');
foreach ($packages as $key => $package) {
$package_phid = $package->getPHID();
if (isset($authority[$package_phid])) {
unset($packages[$key]);
continue;
}
}
if (!$packages) {
return array();
}
}
$auto_subscribe = array();
$auto_review = array();
$auto_block = array();
foreach ($packages as $package) {
switch ($package->getAutoReview()) {
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW:
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW_ALWAYS:
$auto_review[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK:
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK_ALWAYS:
$auto_block[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE:
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE_ALWAYS:
$auto_subscribe[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_NONE:
default:
break;
}
}
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
$xactions = array();
if ($auto_subscribe) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setAuthorPHID($owners_phid)
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => mpull($auto_subscribe, 'getPHID'),
));
}
$specs = array(
array($auto_review, false),
array($auto_block, true),
);
foreach ($specs as $spec) {
list($reviewers, $blocking) = $spec;
if (!$reviewers) {
continue;
}
$phids = mpull($reviewers, 'getPHID');
$xaction = $this->newAutoReviewTransaction($object, $phids, $blocking);
if ($xaction) {
$xactions[] = $xaction;
}
}
return $xactions;
}
private function newAutoReviewTransaction(
PhabricatorLiskDAO $object,
array $phids,
$is_blocking) {
// TODO: This is substantially similar to DifferentialReviewersHeraldAction
// and both are needlessly complex. This logic should live in the normal
// transaction application pipeline. See T10967.
$reviewers = $object->getReviewers();
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
if ($is_blocking) {
$new_status = DifferentialReviewerStatus::STATUS_BLOCKING;
} else {
$new_status = DifferentialReviewerStatus::STATUS_ADDED;
}
$new_strength = DifferentialReviewerStatus::getStatusStrength(
$new_status);
$current = array();
foreach ($phids as $phid) {
if (!isset($reviewers[$phid])) {
continue;
}
// If we're applying a stronger status (usually, upgrading a reviewer
// into a blocking reviewer), skip this check so we apply the change.
$old_strength = DifferentialReviewerStatus::getStatusStrength(
$reviewers[$phid]->getReviewerStatus());
if ($old_strength <= $new_strength) {
continue;
}
$current[] = $phid;
}
$phids = array_diff($phids, $current);
if (!$phids) {
return null;
}
$phids = array_fuse($phids);
$value = array();
foreach ($phids as $phid) {
if ($is_blocking) {
$value[] = 'blocking('.$phid.')';
} else {
$value[] = $phid;
}
}
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
$reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
return $object->getApplicationTransactionTemplate()
->setAuthorPHID($owners_phid)
->setTransactionType($reviewers_type)
->setNewValue(
array(
'+' => $value,
));
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withPHIDs(array($object->getPHID()))
->needActiveDiffs(true)
->needReviewers(true)
->executeOne();
if (!$revision) {
throw new Exception(
pht('Failed to load revision for Herald adapter construction!'));
}
$adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
$revision,
$revision->getActiveDiff());
// If the object is still a draft, prevent "Send me an email" and other
// similar rules from acting yet.
if (!$object->shouldBroadcast()) {
$adapter->setForbiddenAction(
HeraldMailableState::STATECONST,
DifferentialHeraldStateReasons::REASON_DRAFT);
}
// If this edit didn't actually change the diff (for example, a user
// edited the title or changed subscribers), prevent "Run build plan"
// and other similar rules from acting yet, since the build results will
// not (or, at least, should not) change unless the actual source changes.
// We also don't run Differential builds if the update was caused by
// discovering a commit, as the expectation is that Diffusion builds take
// over once things land.
$has_update = false;
$has_commit = false;
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() != $type_update) {
continue;
}
if ($xaction->getMetadataValue('isCommitUpdate')) {
$has_commit = true;
} else {
$has_update = true;
}
break;
}
if ($has_commit) {
$adapter->setForbiddenAction(
HeraldBuildableState::STATECONST,
DifferentialHeraldStateReasons::REASON_LANDED);
} else if (!$has_update) {
$adapter->setForbiddenAction(
HeraldBuildableState::STATECONST,
DifferentialHeraldStateReasons::REASON_UNCHANGED);
}
return $adapter;
}
/**
* Update the table which links Differential revisions to paths they affect,
* so Diffusion can efficiently find pending revisions for a given file.
*/
private function updateAffectedPathTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$repository = $revision->getRepository();
if (!$repository) {
// The repository where the code lives is untracked.
return;
}
$path_prefix = null;
$local_root = $diff->getSourceControlPath();
if ($local_root) {
// We're in a working copy which supports subdirectory checkouts (e.g.,
// SVN) so we need to figure out what prefix we should add to each path
// (e.g., trunk/projects/example/) to get the absolute path from the
// root of the repository. DVCS systems like Git and Mercurial are not
// affected.
// Normalize both paths and check if the repository root is a prefix of
// the local root. If so, throw it away. Note that this correctly handles
// the case where the remote path is "/".
$local_root = id(new PhutilURI($local_root))->getPath();
$local_root = rtrim($local_root, '/');
$repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
$repo_root = rtrim($repo_root, '/');
if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
$path_prefix = substr($local_root, strlen($repo_root));
}
}
$changesets = $diff->getChangesets();
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $path_prefix.'/'.$changeset->getFilename();
}
// Save the affected paths; we'll use them later to query Owners. This
// uses the un-expanded paths.
$this->affectedPaths = $paths;
// Mark this as also touching all parent paths, so you can see all pending
// changes to any file within a directory.
$all_paths = array();
foreach ($paths as $local) {
foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
$all_paths[$path] = true;
}
}
$all_paths = array_keys($all_paths);
$path_ids =
PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
$all_paths);
$table = new DifferentialAffectedPath();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($path_ids as $path_id) {
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %d)',
$repository->getID(),
$path_id,
time(),
$revision->getID());
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$table->getTableName(),
$revision->getID());
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
$table->getTableName(),
implode(', ', $chunk));
}
}
/**
* Update the table connecting revisions to DVCS local hashes, so we can
* identify revisions by commit/tree hashes.
*/
private function updateRevisionHashTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$vcs = $diff->getSourceControlSystem();
if ($vcs == DifferentialRevisionControlSystem::SVN) {
// Subversion has no local commit or tree hash information, so we don't
// have to do anything.
return;
}
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff->getID(),
'local:commits');
if (!$property) {
return;
}
$hashes = array();
$data = $property->getData();
switch ($vcs) {
case DifferentialRevisionControlSystem::GIT:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
$commit['commit'],
);
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
$commit['tree'],
);
}
break;
case DifferentialRevisionControlSystem::MERCURIAL:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
$commit['rev'],
);
}
break;
}
$conn_w = $revision->establishConnection('w');
$sql = array();
foreach ($hashes as $info) {
list($type, $hash) = $info;
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s)',
$revision->getID(),
$type,
$hash);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
ArcanistDifferentialRevisionHash::TABLE_NAME,
$revision->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
ArcanistDifferentialRevisionHash::TABLE_NAME,
implode(', ', $sql));
}
}
private function renderAffectedFilesForMail(DifferentialDiff $diff) {
$changesets = $diff->getChangesets();
$filenames = mpull($changesets, 'getDisplayFilename');
sort($filenames);
$count = count($filenames);
$max = 250;
if ($count > $max) {
$filenames = array_slice($filenames, 0, $max);
$filenames[] = pht('(%d more files...)', ($count - $max));
}
return implode("\n", $filenames);
}
private function renderPatchHTMLForMail($patch) {
return phutil_tag('pre',
array('style' => 'font-family: monospace;'), $patch);
}
private function buildPatchForMail(DifferentialDiff $diff) {
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
return id(new DifferentialRawDiffRenderer())
->setViewer($this->getActor())
->setFormat($format)
->setChangesets($diff->getChangesets())
->buildPatch();
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// Reload to pick up the active diff and reviewer status.
return id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->needActiveDiffs(true)
->withIDs(array($object->getID()))
->executeOne();
}
protected function getCustomWorkerState() {
return array(
'changedPriorToCommitURI' => $this->changedPriorToCommitURI,
'firstBroadcast' => $this->firstBroadcast,
);
}
protected function loadCustomWorkerState(array $state) {
$this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI');
$this->firstBroadcast = idx($state, 'firstBroadcast');
return $this;
}
private function newCommandeerReviewerTransaction(
DifferentialRevision $revision) {
$actor_phid = $this->getActingAsPHID();
$owner_phid = $revision->getAuthorPHID();
// If the user is commandeering, add the previous owner as a
// reviewer and remove the actor.
$edits = array(
'-' => array(
$actor_phid,
),
'+' => array(
$owner_phid,
),
);
// NOTE: We're setting setIsCommandeerSideEffect() on this because normally
// you can't add a revision's author as a reviewer, but this action swaps
// them after validation executes.
$xaction_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
return id(new DifferentialTransaction())
->setTransactionType($xaction_type)
->setIgnoreOnNoEffect(true)
->setIsCommandeerSideEffect(true)
->setNewValue($edits);
}
public function getActiveDiff($object) {
if ($this->getIsNewObject()) {
return null;
} else {
return $object->getActiveDiff();
}
}
/**
* When a reviewer makes a comment, mark the last revision they commented
* on.
*
* This allows us to show a hint to help authors and other reviewers quickly
* distinguish between reviewers who have participated in the discussion and
* reviewers who haven't been part of it.
*/
private function markReviewerComments($object, array $xactions) {
$acting_phid = $this->getActingAsPHID();
if (!$acting_phid) {
return;
}
$diff = $this->getActiveDiff($object);
if (!$diff) {
return;
}
$has_comment = false;
foreach ($xactions as $xaction) {
if ($xaction->hasComment()) {
$has_comment = true;
break;
}
}
if (!$has_comment) {
return;
}
$reviewer_table = new DifferentialReviewer();
$conn = $reviewer_table->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET lastCommentDiffPHID = %s
WHERE revisionPHID = %s
AND reviewerPHID = %s',
$reviewer_table->getTableName(),
$diff->getPHID(),
$object->getPHID(),
$acting_phid);
}
private function loadUnbroadcastTransactions($object) {
$viewer = $this->requireActor();
$xactions = id(new DifferentialTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->execute();
return array_reverse($xactions);
}
protected function didApplyTransactions($object, array $xactions) {
// In a moment, we're going to try to publish draft revisions which have
// completed all their builds. However, we only want to do that if the
// actor is either the revision author or an omnipotent user (generally,
// the Harbormaster application).
// If we let any actor publish the revision as a side effect of other
// changes then an unlucky third party who innocently comments on the draft
// can end up racing Harbormaster and promoting the revision. At best, this
// is confusing. It can also run into validation problems with the "Request
// Review" transaction. See PHI309 for some discussion.
$author_phid = $object->getAuthorPHID();
$viewer = $this->requireActor();
$can_undraft =
($this->getActingAsPHID() === $author_phid) ||
($viewer->isOmnipotent());
// If a draft revision has no outstanding builds and we're automatically
// making drafts public after builds finish, make the revision public.
if ($can_undraft) {
$auto_undraft = !$object->getHoldAsDraft();
} else {
$auto_undraft = false;
}
if ($object->isDraft() && $auto_undraft) {
$active_builds = $this->hasActiveBuilds($object);
if (!$active_builds) {
// When Harbormaster moves a revision out of the draft state, we
// attribute the action to the revision author since this is more
// natural and more useful.
// Additionally, we change the acting PHID for the transaction set
// to the author if it isn't already a user so that mail comes from
// the natural author.
$acting_phid = $this->getActingAsPHID();
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
if (phid_get_type($acting_phid) != $user_type) {
$this->setActingAsPHID($author_phid);
}
$xaction = $object->getApplicationTransactionTemplate()
->setAuthorPHID($author_phid)
->setTransactionType(
DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE)
->setNewValue(true);
// If we're creating this revision and immediately moving it out of
// the draft state, mark this as a create transaction so it gets
// hidden in the timeline and mail, since it isn't interesting: it
// is as though the draft phase never happened.
if ($this->getIsNewObject()) {
$xaction->setIsCreateTransaction(true);
}
// Queue this transaction and apply it separately after the current
// batch of transactions finishes so that Herald can fire on the new
// revision state. See T13027 for discussion.
$this->queueTransaction($xaction);
}
}
// If the revision is new or was a draft, and is no longer a draft, we
// might be sending the first email about it.
// This might mean it was created directly into a non-draft state, or
// it just automatically undrafted after builds finished, or a user
// explicitly promoted it out of the draft state with an action like
// "Request Review".
// If we haven't sent any email about it yet, mark this email as the first
// email so the mail gets enriched with "SUMMARY" and "TEST PLAN".
$is_new = $this->getIsNewObject();
$was_draft = $this->wasDraft;
if (!$object->isDraft() && ($was_draft || $is_new)) {
if (!$object->getHasBroadcast()) {
// Mark this as the first broadcast we're sending about the revision
// so mail can generate specially.
$this->firstBroadcast = true;
$object
->setHasBroadcast(true)
->save();
}
}
return $xactions;
}
private function hasActiveBuilds($object) {
$viewer = $this->requireActor();
$builds = $object->loadActiveBuilds($viewer);
if (!$builds) {
return false;
}
return true;
}
private function requireReviewers(DifferentialRevision $revision) {
if ($revision->hasAttachedReviewers()) {
return;
}
$with_reviewers = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->withPHIDs(array($revision->getPHID()))
->executeOne();
if (!$with_reviewers) {
throw new Exception(
pht(
'Failed to reload revision ("%s").',
$revision->getPHID()));
}
$revision->attachReviewers($with_reviewers->getReviewers());
}
}
diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php
index e3f9bdaf8d..823047f218 100644
--- a/src/applications/differential/storage/DifferentialReviewer.php
+++ b/src/applications/differential/storage/DifferentialReviewer.php
@@ -1,142 +1,155 @@
<?php
final class DifferentialReviewer
extends DifferentialDAO {
protected $revisionPHID;
protected $reviewerPHID;
protected $reviewerStatus;
protected $lastActionDiffPHID;
protected $lastCommentDiffPHID;
protected $lastActorPHID;
protected $voidedPHID;
+ protected $options = array();
private $authority = array();
private $changesets = self::ATTACHABLE;
protected function getConfiguration() {
return array(
+ self::CONFIG_SERIALIZATION => array(
+ 'options' => self::SERIALIZATION_JSON,
+ ),
self::CONFIG_COLUMN_SCHEMA => array(
'reviewerStatus' => 'text64',
'lastActionDiffPHID' => 'phid?',
'lastCommentDiffPHID' => 'phid?',
'lastActorPHID' => 'phid?',
'voidedPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_revision' => array(
'columns' => array('revisionPHID', 'reviewerPHID'),
'unique' => true,
),
'key_reviewer' => array(
'columns' => array('reviewerPHID', 'revisionPHID'),
),
),
) + parent::getConfiguration();
}
public function isUser() {
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
return (phid_get_type($this->getReviewerPHID()) == $user_type);
}
public function isPackage() {
$package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
return (phid_get_type($this->getReviewerPHID()) == $package_type);
}
public function attachAuthority(PhabricatorUser $user, $has_authority) {
$this->authority[$user->getCacheFragment()] = $has_authority;
return $this;
}
public function hasAuthority(PhabricatorUser $viewer) {
$cache_fragment = $viewer->getCacheFragment();
return $this->assertAttachedKey($this->authority, $cache_fragment);
}
public function attachChangesets(array $changesets) {
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->assertAttached($this->changesets);
}
+ public function setOption($key, $value) {
+ $this->options[$key] = $value;
+ return $this;
+ }
+
+ public function getOption($key, $default = null) {
+ return idx($this->options, $key, $default);
+ }
+
public function isResigned() {
$status_resigned = DifferentialReviewerStatus::STATUS_RESIGNED;
return ($this->getReviewerStatus() == $status_resigned);
}
public function isBlocking() {
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
return ($this->getReviewerStatus() == $status_blocking);
}
public function isRejected($diff_phid) {
$status_rejected = DifferentialReviewerStatus::STATUS_REJECTED;
if ($this->getReviewerStatus() != $status_rejected) {
return false;
}
if ($this->getVoidedPHID()) {
return false;
}
if ($this->isCurrentAction($diff_phid)) {
return true;
}
return false;
}
public function isAccepted($diff_phid) {
$status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED;
if ($this->getReviewerStatus() != $status_accepted) {
return false;
}
// If this accept has been voided (for example, but a reviewer using
// "Request Review"), don't count it as a real "Accept" even if it is
// against the current diff PHID.
if ($this->getVoidedPHID()) {
return false;
}
if ($this->isCurrentAction($diff_phid)) {
return true;
}
$sticky_key = 'differential.sticky-accept';
$is_sticky = PhabricatorEnv::getEnvConfig($sticky_key);
if ($is_sticky) {
return true;
}
return false;
}
private function isCurrentAction($diff_phid) {
if (!$diff_phid) {
return true;
}
$action_phid = $this->getLastActionDiffPHID();
if (!$action_phid) {
return true;
}
if ($action_phid == $diff_phid) {
return true;
}
return false;
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php
index b112eb638f..8aec75d2fb 100644
--- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php
@@ -1,266 +1,275 @@
<?php
abstract class DifferentialRevisionReviewTransaction
extends DifferentialRevisionActionTransaction {
protected function getRevisionActionGroupKey() {
return DifferentialRevisionEditEngine::ACTIONGROUP_REVIEW;
}
public function generateNewValue($object, $value) {
if (!is_array($value)) {
return true;
}
// If the list of options is the same as the default list, just treat this
// as a "take the default action" transaction.
$viewer = $this->getActor();
list($options, $default) = $this->getActionOptions($viewer, $object);
// Remove reviewers which aren't actionable. In the case of "Accept", we
// may allow the transaction to proceed with some reviewers who have
// already accepted, to avoid race conditions where two reviewers fill
// out the form at the same time and accept on behalf of the same package.
// It's okay for these reviewers to survive validation, but they should
// not survive beyond this point.
$value = array_fuse($value);
$value = array_intersect($value, array_keys($options));
$value = array_values($value);
sort($default);
sort($value);
if ($default === $value) {
return true;
}
return $value;
}
protected function isViewerAnyReviewer(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
return ($this->getViewerReviewerStatus($revision, $viewer) !== null);
}
protected function isViewerAnyAuthority(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
$reviewers = $revision->getReviewers();
foreach ($revision->getReviewers() as $reviewer) {
if ($reviewer->hasAuthority($viewer)) {
return true;
}
}
return false;
}
protected function isViewerFullyAccepted(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
return $this->isViewerReviewerStatusFully(
$revision,
$viewer,
DifferentialReviewerStatus::STATUS_ACCEPTED);
}
protected function isViewerFullyRejected(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
return $this->isViewerReviewerStatusFully(
$revision,
$viewer,
DifferentialReviewerStatus::STATUS_REJECTED);
}
protected function getViewerReviewerStatus(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
if (!$viewer->getPHID()) {
return null;
}
foreach ($revision->getReviewers() as $reviewer) {
if ($reviewer->getReviewerPHID() != $viewer->getPHID()) {
continue;
}
return $reviewer->getReviewerStatus();
}
return null;
}
private function isViewerReviewerStatusFully(
DifferentialRevision $revision,
PhabricatorUser $viewer,
$require_status) {
// If the user themselves is not a reviewer, the reviews they have
// authority over can not all be in any set of states since their own
// personal review has no state.
$status = $this->getViewerReviewerStatus($revision, $viewer);
if ($status === null) {
return false;
}
$active_phid = $this->getActiveDiffPHID($revision);
$status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED;
$status_rejected = DifferentialReviewerStatus::STATUS_REJECTED;
$is_accepted = ($require_status == $status_accepted);
$is_rejected = ($require_status == $status_rejected);
// Otherwise, check that all reviews they have authority over are in
// the desired set of states.
foreach ($revision->getReviewers() as $reviewer) {
if (!$reviewer->hasAuthority($viewer)) {
$can_force = false;
if ($is_accepted) {
if ($revision->canReviewerForceAccept($viewer, $reviewer)) {
$can_force = true;
}
}
if (!$can_force) {
continue;
}
}
$status = $reviewer->getReviewerStatus();
if ($status != $require_status) {
return false;
}
// Here, we're primarily testing if we can remove a void on the review.
if ($is_accepted) {
if (!$reviewer->isAccepted($active_phid)) {
return false;
}
}
if ($is_rejected) {
if (!$reviewer->isRejected($active_phid)) {
return false;
}
}
// This is a broader check to see if we can update the diff where the
// last action occurred.
if ($reviewer->getLastActionDiffPHID() != $active_phid) {
return false;
}
}
return true;
}
protected function applyReviewerEffect(
DifferentialRevision $revision,
PhabricatorUser $viewer,
$value,
- $status) {
+ $status,
+ array $reviewer_options = array()) {
+
+ PhutilTypeSpec::checkMap(
+ $reviewer_options,
+ array(
+ 'sticky' => 'optional bool',
+ ));
$map = array();
// When you accept or reject, you may accept or reject on behalf of all
// reviewers you have authority for. When you resign, you only affect
// yourself.
$with_authority = ($status != DifferentialReviewerStatus::STATUS_RESIGNED);
$with_force = ($status == DifferentialReviewerStatus::STATUS_ACCEPTED);
if ($with_authority) {
foreach ($revision->getReviewers() as $reviewer) {
if (!$reviewer->hasAuthority($viewer)) {
if (!$with_force) {
continue;
}
if (!$revision->canReviewerForceAccept($viewer, $reviewer)) {
continue;
}
}
$map[$reviewer->getReviewerPHID()] = $status;
}
}
// In all cases, you affect yourself.
$map[$viewer->getPHID()] = $status;
// If we're applying an "accept the defaults" transaction, and this
// transaction type uses checkboxes, replace the value with the list of
// defaults.
if (!is_array($value)) {
list($options, $default) = $this->getActionOptions($viewer, $revision);
if ($options) {
$value = $default;
}
}
// If we have a specific list of reviewers to act on, usually because the
// user has submitted a specific list of reviewers to act as by
// unchecking some checkboxes under "Accept", only affect those reviewers.
if (is_array($value)) {
$map = array_select_keys($map, $value);
}
// Now, do the new write.
if ($map) {
$diff = $this->getEditor()->getActiveDiff($revision);
if ($diff) {
$diff_phid = $diff->getPHID();
} else {
$diff_phid = null;
}
$table = new DifferentialReviewer();
$src_phid = $revision->getPHID();
$reviewers = $table->loadAllWhere(
'revisionPHID = %s AND reviewerPHID IN (%Ls)',
$src_phid,
array_keys($map));
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
foreach (array_keys($map) as $dst_phid) {
$reviewer = idx($reviewers, $dst_phid);
if (!$reviewer) {
$reviewer = id(new DifferentialReviewer())
->setRevisionPHID($src_phid)
->setReviewerPHID($dst_phid);
}
$old_status = $reviewer->getReviewerStatus();
$reviewer->setReviewerStatus($status);
if ($diff_phid) {
$reviewer->setLastActionDiffPHID($diff_phid);
}
if ($old_status !== $status) {
$reviewer->setLastActorPHID($this->getActingAsPHID());
}
// Clear any outstanding void on this reviewer. A void may be placed
// by the author using "Request Review" when a reviewer has already
// accepted.
$reviewer->setVoidedPHID(null);
+ $reviewer->setOption('sticky', idx($reviewer_options, 'sticky'));
+
try {
$reviewer->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// At least for now, just ignore it if we lost a race.
}
}
}
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php
index 5073a9c464..331c637aad 100644
--- a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php
@@ -1,70 +1,92 @@
<?php
/**
* This is an internal transaction type used to void reviews.
*
* For example, "Request Review" voids any open accepts, so they no longer
* act as current accepts.
*/
final class DifferentialRevisionVoidTransaction
extends DifferentialRevisionTransactionType {
const TRANSACTIONTYPE = 'differential.revision.void';
public function generateOldValue($object) {
return false;
}
public function generateNewValue($object, $value) {
- $table = new DifferentialReviewer();
- $table_name = $table->getTableName();
- $conn = $table->establishConnection('w');
-
- $rows = queryfx_all(
- $conn,
- 'SELECT reviewerPHID FROM %T
- WHERE revisionPHID = %s
- AND voidedPHID IS NULL
- AND reviewerStatus IN (%Ls)',
- $table_name,
+ $reviewers = id(new DifferentialReviewer())->loadAllWhere(
+ 'revisionPHID = %s
+ AND voidedPHID IS NULL
+ AND reviewerStatus IN (%Ls)',
$object->getPHID(),
$value);
- return ipull($rows, 'reviewerPHID');
+ $must_downgrade = $this->getMetadataValue('void.force', array());
+ $must_downgrade = array_fuse($must_downgrade);
+
+ $default = PhabricatorEnv::getEnvConfig('differential.sticky-accept');
+ foreach ($reviewers as $key => $reviewer) {
+ $status = $reviewer->getReviewerStatus();
+
+ // If this void is forced, always downgrade. For example, this happens
+ // when an author chooses "Request Review": existing reviews are always
+ // voided, even if they're sticky.
+ if (isset($must_downgrade[$status])) {
+ continue;
+ }
+
+ // Otherwise, if this is a sticky accept, don't void it. Accepts may be
+ // explicitly sticky or unsticky, or they'll use the default value if
+ // no value is specified.
+ $is_sticky = $reviewer->getOption('sticky');
+ $is_sticky = coalesce($is_sticky, $default);
+
+ if ($status === DifferentialReviewerStatus::STATUS_ACCEPTED) {
+ if ($is_sticky) {
+ unset($reviewers[$key]);
+ continue;
+ }
+ }
+ }
+
+
+ return mpull($reviewers, 'getReviewerPHID');
}
public function getTransactionHasEffect($object, $old, $new) {
return (bool)$new;
}
public function applyExternalEffects($object, $value) {
$table = new DifferentialReviewer();
$table_name = $table->getTableName();
$conn = $table->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET voidedPHID = %s
WHERE revisionPHID = %s
AND voidedPHID IS NULL
AND reviewerPHID IN (%Ls)',
$table_name,
$this->getActingAsPHID(),
$object->getPHID(),
$value);
}
public function shouldHide() {
// This is an internal transaction, so don't show it in feeds or
// transaction logs.
return true;
}
private function getVoidableStatuses() {
return array(
DifferentialReviewerStatus::STATUS_ACCEPTED,
DifferentialReviewerStatus::STATUS_REJECTED,
);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Jul 3, 3:16 PM (10 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
165944
Default Alt Text
(68 KB)

Event Timeline