Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index 063df6c602..a092bf2693 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1,1765 +1,1766 @@
<?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 = DifferentialTransaction::TYPE_UPDATE;
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;
$types[] = DifferentialTransaction::TYPE_UPDATE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return null;
case DifferentialTransaction::TYPE_UPDATE:
if ($this->getIsNewObject()) {
return null;
} else {
return $object->getActiveDiff()->getPHID();
}
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
return $xaction->getNewValue();
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;
case DifferentialTransaction::TYPE_UPDATE:
if (!$this->getIsCloseByCommit()) {
if ($object->isNeedsRevision() ||
$object->isChangePlanned() ||
$object->isAbandoned()) {
$object->setModernRevisionStatus(
DifferentialRevisionStatus::NEEDS_REVIEW);
}
}
$diff = $this->requireDiff($xaction->getNewValue());
$this->updateRevisionLineCounts($object, $diff);
if ($this->repositoryPHIDOverride !== false) {
$object->setRepositoryPHID($this->repositoryPHIDOverride);
} else {
$object->setRepositoryPHID($diff->getRepositoryPHID());
}
$object->attachActiveDiff($diff);
$object->setActiveDiffPHID($diff->getPHID());
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;
if ($this->getIsCloseByCommit()) {
// Never downgrade reviewers when we're closing a revision after a
// commit.
} else {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$downgrade_rejects = true;
if (!$is_sticky_accept) {
// If "sticky accept" is disabled, also downgrade the accepts.
$downgrade_accepts = true;
}
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;
}
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) {
$void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE;
$results[] = id(new DifferentialTransaction())
->setTransactionType($void_type)
->setIgnoreOnNoEffect(true)
->setNewValue($downgrade);
}
$is_commandeer = false;
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
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 DifferentialTransaction::TYPE_UPDATE:
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;
case DifferentialTransaction::TYPE_UPDATE:
// Now that we're inside the transaction, do a final check.
$diff = $this->requireDiff($xaction->getNewValue());
// TODO: It would be slightly cleaner to just revalidate this
// transaction somehow using the same validation code, but that's
// not easy to do at the moment.
$revision_id = $diff->getRevisionID();
if ($revision_id && ($revision_id != $object->getID())) {
throw new Exception(
pht(
'Diff is already attached to another revision. You lost '.
'a race?'));
}
// TODO: This can race with diff updates, particularly those from
// Harbormaster. See discussion in T8650.
$diff->setRevisionID($object->getID());
$diff->save();
- // Update Harbormaster to set the containerPHID correctly for any
- // existing buildables. We may otherwise have buildables stuck with
- // the old (`null`) container.
-
- // TODO: This is a bit iffy, maybe we can find a cleaner approach?
- // In particular, this could (rarely) be overwritten by Harbormaster
- // workers.
- $table = new HarbormasterBuildable();
- $conn_w = $table->establishConnection('w');
- queryfx(
- $conn_w,
- 'UPDATE %T SET containerPHID = %s WHERE buildablePHID = %s',
- $table->getTableName(),
- $object->getPHID(),
- $diff->getPHID());
+ // If there are any outstanding buildables for this diff, tell
+ // Harbormaster that their containers need to be updated. This is
+ // common, because `arc` creates buildables so it can upload lint
+ // and unit results.
+
+ $buildables = id(new HarbormasterBuildableQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withManualBuildables(false)
+ ->withBuildablePHIDs(array($diff->getPHID()))
+ ->execute();
+ foreach ($buildables as $buildable) {
+ $buildable->sendMessage(
+ $this->getActor(),
+ HarbormasterMessageType::BUILDABLE_CONTAINER,
+ true);
+ }
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 DifferentialTransaction::TYPE_UPDATE:
$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 validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
$config_self_accept_key = 'differential.allow-self-accept';
$allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
foreach ($xactions as $xaction) {
switch ($type) {
case DifferentialTransaction::TYPE_UPDATE:
$diff = $this->loadDiff($xaction->getNewValue());
if (!$diff) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('The specified diff does not exist.'),
$xaction);
} else if (($diff->getRevisionID()) &&
($diff->getRevisionID() != $object->getID())) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can not update this revision to the specified diff, '.
'because the diff is already attached to another revision.'),
$xaction);
}
break;
}
}
return $errors;
}
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 = DifferentialTransaction::TYPE_UPDATE;
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 DifferentialTransaction::TYPE_UPDATE:
$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();
}
private 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();
}
// Remove packages that the revision author is an owner of. If you own
// code, you don't need another owner to review it.
$authority = id(new PhabricatorOwnersPackageQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(mpull($packages, 'getPHID'))
->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_SUBSCRIBE:
$auto_subscribe[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW:
$auto_review[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK:
$auto_block[] = $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 = DifferentialTransaction::TYPE_UPDATE;
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 updateRevisionLineCounts(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$revision->setLineCount($diff->getLineCount());
$conn = $revision->establishConnection('r');
$row = queryfx_one(
$conn,
'SELECT SUM(addLines) A, SUM(delLines) D FROM %T
WHERE diffID = %d',
id(new DifferentialChangeset())->getTableName(),
$diff->getID());
if ($row) {
$revision->setAddedLineCount((int)$row['A']);
$revision->setRemovedLineCount((int)$row['D']);
}
}
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/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
index e8fdf7e514..6813c124d7 100644
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -1,1065 +1,1066 @@
<?php
final class DifferentialRevision extends DifferentialDAO
implements
PhabricatorTokenReceiverInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorFlaggableInterface,
PhrequentTrackableInterface,
HarbormasterBuildableInterface,
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorMentionableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorDraftInterface {
protected $title = '';
protected $status;
protected $summary = '';
protected $testPlan = '';
protected $authorPHID;
protected $lastReviewerPHID;
protected $lineCount = 0;
protected $attached = array();
protected $mailKey;
protected $branchName;
protected $repositoryPHID;
protected $activeDiffPHID;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $properties = array();
private $commits = self::ATTACHABLE;
private $activeDiff = self::ATTACHABLE;
private $diffIDs = self::ATTACHABLE;
private $hashes = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $reviewerStatus = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $drafts = array();
private $flags = array();
private $forceMap = array();
const TABLE_COMMIT = 'differential_commit';
const RELATION_REVIEWER = 'revw';
const RELATION_SUBSCRIBED = 'subd';
const PROPERTY_CLOSED_FROM_ACCEPTED = 'wasAcceptedBeforeClose';
const PROPERTY_DRAFT_HOLD = 'draft.hold';
const PROPERTY_HAS_BROADCAST = 'draft.broadcast';
const PROPERTY_LINES_ADDED = 'lines.added';
const PROPERTY_LINES_REMOVED = 'lines.removed';
public static function initializeNewRevision(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
$initial_state = DifferentialRevisionStatus::DRAFT;
} else {
$initial_state = DifferentialRevisionStatus::NEEDS_REVIEW;
}
return id(new DifferentialRevision())
->setViewPolicy($view_policy)
->setAuthorPHID($actor->getPHID())
->attachRepository(null)
->attachActiveDiff(null)
->attachReviewers(array())
->setModernRevisionStatus($initial_state);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attached' => self::SERIALIZATION_JSON,
'unsubscribed' => self::SERIALIZATION_JSON,
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'status' => 'text32',
'summary' => 'text',
'testPlan' => 'text',
'authorPHID' => 'phid?',
'lastReviewerPHID' => 'phid?',
'lineCount' => 'uint32?',
'mailKey' => 'bytes40',
'branchName' => 'text255?',
'repositoryPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'repositoryPHID' => array(
'columns' => array('repositoryPHID'),
),
// If you (or a project you are a member of) is reviewing a significant
// fraction of the revisions on an install, the result set of open
// revisions may be smaller than the result set of revisions where you
// are a reviewer. In these cases, this key is better than keys on the
// edge table.
'key_status' => array(
'columns' => array('status', 'phid'),
),
),
) + parent::getConfiguration();
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function hasRevisionProperty($key) {
return array_key_exists($key, $this->properties);
}
public function getMonogram() {
$id = $this->getID();
return "D{$id}";
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function loadIDsByCommitPHIDs($phids) {
if (!$phids) {
return array();
}
$revision_ids = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE commitPHID IN (%Ls)',
self::TABLE_COMMIT,
$phids);
return ipull($revision_ids, 'revisionID', 'commitPHID');
}
public function loadCommitPHIDs() {
if (!$this->getID()) {
return ($this->commits = array());
}
$commits = queryfx_all(
$this->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$commits = ipull($commits, 'commitPHID');
return ($this->commits = $commits);
}
public function getCommitPHIDs() {
return $this->assertAttached($this->commits);
}
public function getActiveDiff() {
// TODO: Because it's currently technically possible to create a revision
// without an associated diff, we allow an attached-but-null active diff.
// It would be good to get rid of this once we make diff-attaching
// transactional.
return $this->assertAttached($this->activeDiff);
}
public function attachActiveDiff($diff) {
$this->activeDiff = $diff;
return $this;
}
public function getDiffIDs() {
return $this->assertAttached($this->diffIDs);
}
public function attachDiffIDs(array $ids) {
rsort($ids);
$this->diffIDs = array_values($ids);
return $this;
}
public function attachCommitPHIDs(array $phids) {
$this->commits = array_values($phids);
return $this;
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function setAttachedPHIDs($type, array $phids) {
$this->attached[$type] = array_fill_keys($phids, array());
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialRevisionPHIDType::TYPECONST);
}
public function loadActiveDiff() {
return id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->mailKey = Filesystem::readRandomCharacters(40);
}
return parent::save();
}
public function getHashes() {
return $this->assertAttached($this->hashes);
}
public function attachHashes(array $hashes) {
$this->hashes = $hashes;
return $this;
}
public function canReviewerForceAccept(
PhabricatorUser $viewer,
DifferentialReviewer $reviewer) {
if (!$reviewer->isPackage()) {
return false;
}
$map = $this->getReviewerForceAcceptMap($viewer);
if (!$map) {
return false;
}
if (isset($map[$reviewer->getReviewerPHID()])) {
return true;
}
return false;
}
private function getReviewerForceAcceptMap(PhabricatorUser $viewer) {
$fragment = $viewer->getCacheFragment();
if (!array_key_exists($fragment, $this->forceMap)) {
$map = $this->newReviewerForceAcceptMap($viewer);
$this->forceMap[$fragment] = $map;
}
return $this->forceMap[$fragment];
}
private function newReviewerForceAcceptMap(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
if (!$diff) {
return null;
}
$repository_phid = $diff->getRepositoryPHID();
if (!$repository_phid) {
return null;
}
$paths = array();
try {
$changesets = $diff->getChangesets();
} catch (Exception $ex) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withDiffs(array($diff))
->execute();
}
foreach ($changesets as $changeset) {
$paths[] = $changeset->getOwnersFilename();
}
if (!$paths) {
return null;
}
$reviewer_phids = array();
foreach ($this->getReviewers() as $reviewer) {
if (!$reviewer->isPackage()) {
continue;
}
$reviewer_phids[] = $reviewer->getReviewerPHID();
}
if (!$reviewer_phids) {
return null;
}
// Load all the reviewing packages which have control over some of the
// paths in the change. These are packages which the actor may be able
// to force-accept on behalf of.
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withPHIDs($reviewer_phids)
->withControl($repository_phid, $paths);
$control_packages = $control_query->execute();
if (!$control_packages) {
return null;
}
// Load all the packages which have potential control over some of the
// paths in the change and are owned by the actor. These are packages
// which the actor may be able to use their authority over to gain the
// ability to force-accept for other packages. This query doesn't apply
// dominion rules yet, and we'll bypass those rules later on.
$authority_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->withControl($repository_phid, $paths);
$authority_packages = $authority_query->execute();
if (!$authority_packages) {
return null;
}
$authority_packages = mpull($authority_packages, null, 'getPHID');
// Build a map from each path in the revision to the reviewer packages
// which control it.
$control_map = array();
foreach ($paths as $path) {
$control_packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$path);
// Remove packages which the viewer has authority over. We don't need
// to check these for force-accept because they can just accept them
// normally.
$control_packages = mpull($control_packages, null, 'getPHID');
foreach ($control_packages as $phid => $control_package) {
if (isset($authority_packages[$phid])) {
unset($control_packages[$phid]);
}
}
if (!$control_packages) {
continue;
}
$control_map[$path] = $control_packages;
}
if (!$control_map) {
return null;
}
// From here on out, we only care about paths which we have at least one
// controlling package for.
$paths = array_keys($control_map);
// Now, build a map from each path to the packages which would control it
// if there were no dominion rules.
$authority_map = array();
foreach ($paths as $path) {
$authority_packages = $authority_query->getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = true);
$authority_map[$path] = mpull($authority_packages, null, 'getPHID');
}
// For each path, find the most general package that the viewer has
// authority over. For example, we'll prefer a package that owns "/" to a
// package that owns "/src/".
$force_map = array();
foreach ($authority_map as $path => $package_map) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
// Find the package that we have authority over which has the most
// general match for this path.
$best_match = null;
$best_package = null;
foreach ($package_map as $package_phid => $package) {
$package_paths = $package->getPathsForRepository($repository_phid);
foreach ($package_paths as $package_path) {
// NOTE: A strength of 0 means "no match". A strength of 1 means
// that we matched "/", so we can not possibly find another stronger
// match.
$strength = $package_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength < $best_match || !$best_package) {
$best_match = $strength;
$best_package = $package;
if ($strength == 1) {
break 2;
}
}
}
}
if ($best_package) {
$force_map[$path] = array(
'strength' => $best_match,
'package' => $best_package,
);
}
}
// For each path which the viewer owns a package for, find other packages
// which that authority can be used to force-accept. Once we find a way to
// force-accept a package, we don't need to keep looking.
$has_control = array();
foreach ($force_map as $path => $spec) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
$authority_strength = $spec['strength'];
$control_packages = $control_map[$path];
foreach ($control_packages as $control_phid => $control_package) {
if (isset($has_control[$control_phid])) {
continue;
}
$control_paths = $control_package->getPathsForRepository(
$repository_phid);
foreach ($control_paths as $control_path) {
$strength = $control_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength > $authority_strength) {
$authority = $spec['package'];
$has_control[$control_phid] = array(
'authority' => $authority,
'phid' => $authority->getPHID(),
);
break;
}
}
}
}
// Return a map from packages which may be force accepted to the packages
// which permit that forced acceptance.
return ipull($has_control, 'phid');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A revision's author (which effectively means "owner" after we added
// commandeering) can always view and edit it.
$author_phid = $this->getAuthorPHID();
if ($author_phid) {
if ($user->getPHID() == $author_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
$description = array(
pht('The owner of a revision can always view and edit it.'),
);
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$description[] = pht(
'If a revision belongs to a repository, other users must be able '.
'to view the repository in order to view the revision.');
break;
}
return $description;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$repository_phid = $this->getRepositoryPHID();
$repository = $this->getRepository();
// Try to use the object if we have it, since it will save us some
// data fetching later on. In some cases, we might not have it.
$repository_ref = nonempty($repository, $repository_phid);
if ($repository_ref) {
$extended[] = array(
$repository_ref,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
public function getReviewers() {
return $this->assertAttached($this->reviewerStatus);
}
public function attachReviewers(array $reviewers) {
assert_instances_of($reviewers, 'DifferentialReviewer');
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
$this->reviewerStatus = $reviewers;
return $this;
}
public function hasAttachedReviewers() {
return ($this->reviewerStatus !== self::ATTACHABLE);
}
public function getReviewerPHIDs() {
$reviewers = $this->getReviewers();
return mpull($reviewers, 'getReviewerPHID');
}
public function getReviewerPHIDsForEdit() {
$reviewers = $this->getReviewers();
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
$value = array();
foreach ($reviewers as $reviewer) {
$phid = $reviewer->getReviewerPHID();
if ($reviewer->getReviewerStatus() == $status_blocking) {
$value[] = 'blocking('.$phid.')';
} else {
$value[] = $phid;
}
}
return $value;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachRepository(PhabricatorRepository $repository = null) {
$this->repository = $repository;
return $this;
}
public function setModernRevisionStatus($status) {
return $this->setStatus($status);
}
public function getModernRevisionStatus() {
return $this->getStatus();
}
public function getLegacyRevisionStatus() {
return $this->getStatusObject()->getLegacyKey();
}
public function isClosed() {
return $this->getStatusObject()->isClosedStatus();
}
public function isAbandoned() {
return $this->getStatusObject()->isAbandoned();
}
public function isAccepted() {
return $this->getStatusObject()->isAccepted();
}
public function isNeedsReview() {
return $this->getStatusObject()->isNeedsReview();
}
public function isNeedsRevision() {
return $this->getStatusObject()->isNeedsRevision();
}
public function isChangePlanned() {
return $this->getStatusObject()->isChangePlanned();
}
public function isPublished() {
return $this->getStatusObject()->isPublished();
}
public function isDraft() {
return $this->getStatusObject()->isDraft();
}
public function getStatusIcon() {
return $this->getStatusObject()->getIcon();
}
public function getStatusDisplayName() {
return $this->getStatusObject()->getDisplayName();
}
public function getStatusIconColor() {
return $this->getStatusObject()->getIconColor();
}
public function getStatusObject() {
$status = $this->getStatus();
return DifferentialRevisionStatus::newForStatus($status);
}
public function getFlag(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->flags, $viewer->getPHID());
}
public function attachFlag(
PhabricatorUser $viewer,
PhabricatorFlag $flag = null) {
$this->flags[$viewer->getPHID()] = $flag;
return $this;
}
public function getHasDraft(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment());
}
public function attachHasDraft(PhabricatorUser $viewer, $has_draft) {
$this->drafts[$viewer->getCacheFragment()] = $has_draft;
return $this;
}
public function shouldBroadcast() {
if (!$this->isDraft()) {
return true;
}
return false;
}
public function getHoldAsDraft() {
return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false);
}
public function setHoldAsDraft($hold) {
return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold);
}
public function getHasBroadcast() {
return $this->getProperty(self::PROPERTY_HAS_BROADCAST, false);
}
public function setHasBroadcast($has_broadcast) {
return $this->setProperty(self::PROPERTY_HAS_BROADCAST, $has_broadcast);
}
public function setAddedLineCount($count) {
return $this->setProperty(self::PROPERTY_LINES_ADDED, $count);
}
public function getAddedLineCount() {
return $this->getProperty(self::PROPERTY_LINES_ADDED);
}
public function setRemovedLineCount($count) {
return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count);
}
public function getRemovedLineCount() {
return $this->getProperty(self::PROPERTY_LINES_REMOVED);
}
public function loadActiveBuilds(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
+ // NOTE: We can't use `withContainerPHIDs()` here because the container
+ // update in Harbormaster is not synchronous.
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
- ->withContainerPHIDs(array($this->getPHID()))
->withBuildablePHIDs(array($diff->getPHID()))
->withManualBuildables(false)
->execute();
if (!$buildables) {
return array();
}
return id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs(mpull($buildables, 'getPHID'))
->withAutobuilds(false)
->withBuildStatuses(
array(
HarbormasterBuildStatus::STATUS_INACTIVE,
HarbormasterBuildStatus::STATUS_PENDING,
HarbormasterBuildStatus::STATUS_BUILDING,
HarbormasterBuildStatus::STATUS_FAILED,
HarbormasterBuildStatus::STATUS_ABORTED,
HarbormasterBuildStatus::STATUS_ERROR,
HarbormasterBuildStatus::STATUS_PAUSED,
HarbormasterBuildStatus::STATUS_DEADLOCKED,
))
->execute();
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
return $this->getHarbormasterContainerPHID();
}
public function getHarbormasterBuildablePHID() {
return $this->loadActiveDiff()->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getPHID();
}
public function getHarbormasterPublishablePHID() {
return $this->getPHID();
}
public function getBuildVariables() {
return array();
}
public function getAvailableBuildVariables() {
return array();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
if ($phid == $this->getAuthorPHID()) {
return true;
}
// TODO: This only happens when adding or removing CCs, and is safe from a
// policy perspective, but the subscription pathway should have some
// opportunity to load this data properly. For now, this is the only case
// where implicit subscription is not an intrinsic property of the object.
if ($this->reviewerStatus == self::ATTACHABLE) {
$reviewers = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needReviewers(true)
->executeOne()
->getReviewers();
} else {
$reviewers = $this->getReviewers();
}
foreach ($reviewers as $reviewer) {
if ($reviewer->getReviewerPHID() !== $phid) {
continue;
}
if ($reviewer->isResigned()) {
continue;
}
return true;
}
return false;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('differential.fields');
}
public function getCustomFieldBaseClass() {
return 'DifferentialCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DifferentialTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
$viewer = $request->getViewer();
$render_data = $timeline->getRenderData();
$left = $request->getInt('left', idx($render_data, 'left'));
$right = $request->getInt('right', idx($render_data, 'right'));
$diffs = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withIDs(array($left, $right))
->execute();
$diffs = mpull($diffs, null, 'getID');
$left_diff = $diffs[$left];
$right_diff = $diffs[$right];
$old_ids = $request->getStr('old', idx($render_data, 'old'));
$new_ids = $request->getStr('new', idx($render_data, 'new'));
$old_ids = array_filter(explode(',', $old_ids));
$new_ids = array_filter(explode(',', $new_ids));
$type_inline = DifferentialTransaction::TYPE_INLINE;
$changeset_ids = array_merge($old_ids, $new_ids);
$inlines = array();
foreach ($timeline->getTransactions() as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction->getComment();
$changeset_ids[] = $xaction->getComment()->getChangesetID();
}
}
if ($changeset_ids) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($request->getUser())
->withIDs($changeset_ids)
->execute();
$changesets = mpull($changesets, null, 'getID');
} else {
$changesets = array();
}
foreach ($inlines as $key => $inline) {
$inlines[$key] = DifferentialInlineComment::newFromModernComment(
$inline);
}
$query = id(new DifferentialInlineCommentQuery())
->needHidden(true)
->setViewer($viewer);
// NOTE: This is a bit sketchy: this method adjusts the inlines as a
// side effect, which means it will ultimately adjust the transaction
// comments and affect timeline rendering.
$query->adjustInlinesForChangesets(
$inlines,
array_select_keys($changesets, $old_ids),
array_select_keys($changesets, $new_ids),
$this);
return $timeline
->setChangesets($changesets)
->setRevision($this)
->setLeftDiff($left_diff)
->setRightDiff($right_diff);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$diffs = id(new DifferentialDiffQuery())
->setViewer($engine->getViewer())
->withRevisionIDs(array($this->getID()))
->execute();
foreach ($diffs as $diff) {
$engine->destroyObject($diff);
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
// we have to do paths a little differently as they do not have
// an id or phid column for delete() to act on
$dummy_path = new DifferentialAffectedPath();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$dummy_path->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new DifferentialRevisionFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new DifferentialRevisionFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The revision title.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Revision author PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Information about revision status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid?')
->setDescription(pht('Revision repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('diffPHID')
->setType('phid')
->setDescription(pht('Active diff PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('summary')
->setType('string')
->setDescription(pht('Revision summary.')),
);
}
public function getFieldValuesForConduit() {
$status = $this->getStatusObject();
$status_info = array(
'value' => $status->getKey(),
'name' => $status->getDisplayName(),
'closed' => $status->isClosedStatus(),
'color.ansi' => $status->getANSIColor(),
);
return array(
'title' => $this->getTitle(),
'authorPHID' => $this->getAuthorPHID(),
'status' => $status_info,
'repositoryPHID' => $this->getRepositoryPHID(),
'diffPHID' => $this->getActiveDiffPHID(),
'summary' => $this->getSummary(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new DifferentialReviewersSearchEngineAttachment())
->setAttachmentKey('reviewers'),
);
}
/* -( PhabricatorDraftInterface )------------------------------------------ */
public function newDraftEngine() {
return new DifferentialRevisionDraftEngine();
}
}
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
index ea20c64538..454fec7f37 100644
--- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
+++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
@@ -1,623 +1,640 @@
<?php
/**
* Moves a build forward by queuing build tasks, canceling or restarting the
* build, or failing it in response to task failures.
*/
final class HarbormasterBuildEngine extends Phobject {
private $build;
private $viewer;
private $newBuildTargets = array();
private $artifactReleaseQueue = array();
private $forceBuildableUpdate;
public function setForceBuildableUpdate($force_buildable_update) {
$this->forceBuildableUpdate = $force_buildable_update;
return $this;
}
public function shouldForceBuildableUpdate() {
return $this->forceBuildableUpdate;
}
public function queueNewBuildTarget(HarbormasterBuildTarget $target) {
$this->newBuildTargets[] = $target;
return $this;
}
public function getNewBuildTargets() {
return $this->newBuildTargets;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setBuild(HarbormasterBuild $build) {
$this->build = $build;
return $this;
}
public function getBuild() {
return $this->build;
}
public function continueBuild() {
$build = $this->getBuild();
$lock_key = 'harbormaster.build:'.$build->getID();
$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);
$build->reload();
$old_status = $build->getBuildStatus();
try {
$this->updateBuild($build);
} catch (Exception $ex) {
// If any exception is raised, the build is marked as a failure and the
// exception is re-thrown (this ensures we don't leave builds in an
// inconsistent state).
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ERROR);
$build->save();
$lock->unlock();
$this->releaseAllArtifacts($build);
throw $ex;
}
$lock->unlock();
// NOTE: We queue new targets after releasing the lock so that in-process
// execution via `bin/harbormaster` does not reenter the locked region.
foreach ($this->getNewBuildTargets() as $target) {
$task = PhabricatorWorker::scheduleTask(
'HarbormasterTargetWorker',
array(
'targetID' => $target->getID(),
),
array(
'objectPHID' => $target->getPHID(),
));
}
// If the build changed status, we might need to update the overall status
// on the buildable.
$new_status = $build->getBuildStatus();
if ($new_status != $old_status || $this->shouldForceBuildableUpdate()) {
$this->updateBuildable($build->getBuildable());
}
$this->releaseQueuedArtifacts();
// If we are no longer building for any reason, release all artifacts.
if (!$build->isBuilding()) {
$this->releaseAllArtifacts($build);
}
}
private function updateBuild(HarbormasterBuild $build) {
if ($build->isAborting()) {
$this->releaseAllArtifacts($build);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED);
$build->save();
}
if (($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_PENDING) ||
($build->isRestarting())) {
$this->restartBuild($build);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
$build->save();
}
if ($build->isResuming()) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
$build->save();
}
if ($build->isPausing() && !$build->isComplete()) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED);
$build->save();
}
$build->deleteUnprocessedCommands();
if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) {
$this->updateBuildSteps($build);
}
}
private function restartBuild(HarbormasterBuild $build) {
// We're restarting the build, so release all previous artifacts.
$this->releaseAllArtifacts($build);
// Increment the build generation counter on the build.
$build->setBuildGeneration($build->getBuildGeneration() + 1);
// Currently running targets should periodically check their build
// generation (which won't have changed) against the build's generation.
// If it is different, they will automatically stop what they're doing
// and abort.
// Previously we used to delete targets, logs and artifacts here. Instead
// leave them around so users can view previous generations of this build.
}
private function updateBuildSteps(HarbormasterBuild $build) {
$all_targets = id(new HarbormasterBuildTargetQuery())
->setViewer($this->getViewer())
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($build->getBuildGeneration()))
->execute();
$this->updateWaitingTargets($all_targets);
$targets = mgroup($all_targets, 'getBuildStepPHID');
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($this->getViewer())
->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID()))
->execute();
$steps = mpull($steps, null, 'getPHID');
// Identify steps which are in various states.
$queued = array();
$underway = array();
$waiting = array();
$complete = array();
$failed = array();
foreach ($steps as $step) {
$step_targets = idx($targets, $step->getPHID(), array());
if ($step_targets) {
$is_queued = false;
$is_underway = false;
foreach ($step_targets as $target) {
if ($target->isUnderway()) {
$is_underway = true;
break;
}
}
$is_waiting = false;
foreach ($step_targets as $target) {
if ($target->isWaiting()) {
$is_waiting = true;
break;
}
}
$is_complete = true;
foreach ($step_targets as $target) {
if (!$target->isComplete()) {
$is_complete = false;
break;
}
}
$is_failed = false;
foreach ($step_targets as $target) {
if ($target->isFailed()) {
$is_failed = true;
break;
}
}
} else {
$is_queued = true;
$is_underway = false;
$is_waiting = false;
$is_complete = false;
$is_failed = false;
}
if ($is_queued) {
$queued[$step->getPHID()] = true;
}
if ($is_underway) {
$underway[$step->getPHID()] = true;
}
if ($is_waiting) {
$waiting[$step->getPHID()] = true;
}
if ($is_complete) {
$complete[$step->getPHID()] = true;
}
if ($is_failed) {
$failed[$step->getPHID()] = true;
}
}
// If any step failed, fail the whole build, then bail.
if (count($failed)) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_FAILED);
$build->save();
return;
}
// If every step is complete, we're done with this build. Mark it passed
// and bail.
if (count($complete) == count($steps)) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PASSED);
$build->save();
return;
}
// Release any artifacts which are not inputs to any remaining build
// step. We're done with these, so something else is free to use them.
$ongoing_phids = array_keys($queued + $waiting + $underway);
$ongoing_steps = array_select_keys($steps, $ongoing_phids);
$this->releaseUnusedArtifacts($all_targets, $ongoing_steps);
// Identify all the steps which are ready to run (because all their
// dependencies are complete).
$runnable = array();
foreach ($steps as $step) {
$dependencies = $step->getStepImplementation()->getDependencies($step);
if (isset($queued[$step->getPHID()])) {
$can_run = true;
foreach ($dependencies as $dependency) {
if (empty($complete[$dependency])) {
$can_run = false;
break;
}
}
if ($can_run) {
$runnable[] = $step;
}
}
}
if (!$runnable && !$waiting && !$underway) {
// This means the build is deadlocked, and the user has configured
// circular dependencies.
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_DEADLOCKED);
$build->save();
return;
}
foreach ($runnable as $runnable_step) {
$target = HarbormasterBuildTarget::initializeNewBuildTarget(
$build,
$runnable_step,
$build->retrieveVariablesFromBuild());
$target->save();
$this->queueNewBuildTarget($target);
}
}
/**
* Release any artifacts which aren't used by any running or waiting steps.
*
* This releases artifacts as soon as they're no longer used. This can be
* particularly relevant when a build uses multiple hosts since it returns
* hosts to the pool more quickly.
*
* @param list<HarbormasterBuildTarget> Targets in the build.
* @param list<HarbormasterBuildStep> List of running and waiting steps.
* @return void
*/
private function releaseUnusedArtifacts(array $targets, array $steps) {
assert_instances_of($targets, 'HarbormasterBuildTarget');
assert_instances_of($steps, 'HarbormasterBuildStep');
if (!$targets || !$steps) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($this->getViewer())
->withBuildTargetPHIDs($target_phids)
->withIsReleased(false)
->execute();
if (!$artifacts) {
return;
}
// Collect all the artifacts that remaining build steps accept as inputs.
$must_keep = array();
foreach ($steps as $step) {
$inputs = $step->getStepImplementation()->getArtifactInputs();
foreach ($inputs as $input) {
$artifact_key = $input['key'];
$must_keep[$artifact_key] = true;
}
}
// Queue unreleased artifacts which no remaining step uses for immediate
// release.
foreach ($artifacts as $artifact) {
$key = $artifact->getArtifactKey();
if (isset($must_keep[$key])) {
continue;
}
$this->artifactReleaseQueue[] = $artifact;
}
}
/**
* Process messages which were sent to these targets, kicking applicable
* targets out of "Waiting" and into either "Passed" or "Failed".
*
* @param list<HarbormasterBuildTarget> List of targets to process.
* @return void
*/
private function updateWaitingTargets(array $targets) {
assert_instances_of($targets, 'HarbormasterBuildTarget');
// We only care about messages for targets which are actually in a waiting
// state.
$waiting_targets = array();
foreach ($targets as $target) {
if ($target->isWaiting()) {
$waiting_targets[$target->getPHID()] = $target;
}
}
if (!$waiting_targets) {
return;
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($this->getViewer())
->withReceiverPHIDs(array_keys($waiting_targets))
->withConsumed(false)
->execute();
foreach ($messages as $message) {
$target = $waiting_targets[$message->getReceiverPHID()];
switch ($message->getType()) {
case HarbormasterMessageType::MESSAGE_PASS:
$new_status = HarbormasterBuildTarget::STATUS_PASSED;
break;
case HarbormasterMessageType::MESSAGE_FAIL:
$new_status = HarbormasterBuildTarget::STATUS_FAILED;
break;
case HarbormasterMessageType::MESSAGE_WORK:
default:
$new_status = null;
break;
}
if ($new_status !== null) {
$message->setIsConsumed(true);
$message->save();
$target->setTargetStatus($new_status);
if ($target->isComplete()) {
$target->setDateCompleted(PhabricatorTime::getNow());
}
$target->save();
}
}
}
/**
* Update the overall status of the buildable this build is attached to.
*
* After a build changes state (for example, passes or fails) it may affect
* the overall state of the associated buildable. Compute the new aggregate
* state and save it on the buildable.
*
* @param HarbormasterBuild The buildable to update.
* @return void
*/
public function updateBuildable(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$lock_key = 'harbormaster.buildable:'.$buildable->getID();
$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($buildable->getID()))
->needBuilds(true)
->executeOne();
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(array($buildable->getPHID()))
->withConsumed(false)
->execute();
$done_preparing = false;
+ $update_container = false;
foreach ($messages as $message) {
switch ($message->getType()) {
case HarbormasterMessageType::BUILDABLE_BUILD:
$done_preparing = true;
break;
+ case HarbormasterMessageType::BUILDABLE_CONTAINER:
+ $update_container = true;
+ break;
default:
break;
}
$message
->setIsConsumed(true)
->save();
}
// If we received a "build" command, all builds are scheduled and we can
// move out of "preparing" into "building".
-
if ($done_preparing) {
if ($buildable->isPreparing()) {
$buildable
->setBuildableStatus(HarbormasterBuildableStatus::STATUS_BUILDING)
->save();
}
}
+ // If we've been informed that the container for the buildable has
+ // changed, update it.
+ if ($update_container) {
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($buildable->getBuildablePHID()))
+ ->executeOne();
+ if ($object) {
+ $buildable
+ ->setContainerPHID($object->getHarbormasterContainerPHID())
+ ->save();
+ }
+ }
+
// Don't update the buildable status if we're still preparing builds: more
// builds may still be scheduled shortly, so even if every build we know
// about so far has passed, that doesn't mean the buildable has actually
// passed everything it needs to.
if (!$buildable->isPreparing()) {
$all_pass = true;
$any_fail = false;
foreach ($buildable->getBuilds() as $build) {
if (!$build->isPassed()) {
$all_pass = false;
}
if ($build->isComplete() && !$build->isPassed()) {
$any_fail = true;
}
}
if ($any_fail) {
$new_status = HarbormasterBuildableStatus::STATUS_FAILED;
} else if ($all_pass) {
$new_status = HarbormasterBuildableStatus::STATUS_PASSED;
} else {
$new_status = HarbormasterBuildableStatus::STATUS_BUILDING;
}
$old_status = $buildable->getBuildableStatus();
$did_update = ($old_status != $new_status);
if ($did_update) {
$buildable->setBuildableStatus($new_status);
$buildable->save();
}
}
$lock->unlock();
// Don't publish anything if we're still preparing builds.
if ($buildable->isPreparing()) {
return;
}
// If we changed the buildable status, try to post a transaction to the
// object about it. We can safely do this outside of the locked region.
// NOTE: We only post transactions for automatic buildables, not for
// manual ones: manual builds are test builds, whoever is doing tests
// can look at the results themselves, and other users generally don't
// care about the outcome.
$should_publish =
($did_update) &&
($new_status != HarbormasterBuildableStatus::STATUS_BUILDING) &&
(!$buildable->getIsManualBuildable());
if (!$should_publish) {
return;
}
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($buildable->getBuildablePHID()))
->executeOne();
if (!$object) {
return;
}
$publish_phid = $object->getHarbormasterPublishablePHID();
if (!$publish_phid) {
return;
}
if ($publish_phid === $object->getPHID()) {
$publish = $object;
} else {
$publish = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($publish_phid))
->executeOne();
if (!$publish) {
return;
}
}
if (!($publish instanceof PhabricatorApplicationTransactionInterface)) {
return;
}
$template = $publish->getApplicationTransactionTemplate();
if (!$template) {
return;
}
$template
->setTransactionType(PhabricatorTransactions::TYPE_BUILDABLE)
->setMetadataValue(
'harbormaster:buildablePHID',
$buildable->getPHID())
->setOldValue($old_status)
->setNewValue($new_status);
$harbormaster_phid = id(new PhabricatorHarbormasterApplication())
->getPHID();
$daemon_source = PhabricatorContentSource::newForSource(
PhabricatorDaemonContentSource::SOURCECONST);
$editor = $publish->getApplicationTransactionEditor()
->setActor($viewer)
->setActingAsPHID($harbormaster_phid)
->setContentSource($daemon_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$editor->applyTransactions(
$publish->getApplicationTransactionObject(),
array($template));
}
private function releaseAllArtifacts(HarbormasterBuild $build) {
$targets = id(new HarbormasterBuildTargetQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($build->getBuildGeneration()))
->execute();
if (count($targets) === 0) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildTargetPHIDs($target_phids)
->withIsReleased(false)
->execute();
foreach ($artifacts as $artifact) {
$artifact->releaseArtifact();
}
}
private function releaseQueuedArtifacts() {
foreach ($this->artifactReleaseQueue as $key => $artifact) {
$artifact->releaseArtifact();
unset($this->artifactReleaseQueue[$key]);
}
}
}
diff --git a/src/applications/harbormaster/engine/HarbormasterMessageType.php b/src/applications/harbormaster/engine/HarbormasterMessageType.php
index 8b3ebe2e6f..135fb23ffc 100644
--- a/src/applications/harbormaster/engine/HarbormasterMessageType.php
+++ b/src/applications/harbormaster/engine/HarbormasterMessageType.php
@@ -1,46 +1,47 @@
<?php
final class HarbormasterMessageType extends Phobject {
const MESSAGE_PASS = 'pass';
const MESSAGE_FAIL = 'fail';
const MESSAGE_WORK = 'work';
const BUILDABLE_BUILD = 'build';
+ const BUILDABLE_CONTAINER = 'container';
public static function getAllMessages() {
return array_keys(self::getMessageSpecifications());
}
public static function getMessageDescription($message) {
$spec = self::getMessageSpecification($message);
if (!$spec) {
return null;
}
return idx($spec, 'description');
}
private static function getMessageSpecification($message) {
$specs = self::getMessageSpecifications();
return idx($specs, $message);
}
private static function getMessageSpecifications() {
return array(
self::MESSAGE_PASS => array(
'description' => pht(
'Report that the target is complete, and the target has passed.'),
),
self::MESSAGE_FAIL => array(
'description' => pht(
'Report that the target is complete, and the target has failed.'),
),
self::MESSAGE_WORK => array(
'description' => pht(
'Report that work on the target is ongoing. This message can be '.
'used to report partial results during a build.'),
),
);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Mar 14, 11:59 PM (20 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
72080
Default Alt Text
(107 KB)

Event Timeline