Path: blob/master/src/applications/differential/editor/DifferentialTransactionEditor.php
12256 views
<?php12final class DifferentialTransactionEditor3extends PhabricatorApplicationTransactionEditor {45private $changedPriorToCommitURI;6private $isCloseByCommit;7private $repositoryPHIDOverride = false;8private $didExpandInlineState = false;9private $firstBroadcast = false;10private $wasBroadcasting;11private $isDraftDemotion;1213private $ownersDiff;14private $ownersChangesets;1516public function getEditorApplicationClass() {17return 'PhabricatorDifferentialApplication';18}1920public function getEditorObjectsDescription() {21return pht('Differential Revisions');22}2324public function getCreateObjectTitle($author, $object) {25return pht('%s created this revision.', $author);26}2728public function getCreateObjectTitleForFeed($author, $object) {29return pht('%s created %s.', $author, $object);30}3132public function isFirstBroadcast() {33return $this->firstBroadcast;34}3536public function getDiffUpdateTransaction(array $xactions) {37$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;3839foreach ($xactions as $xaction) {40if ($xaction->getTransactionType() == $type_update) {41return $xaction;42}43}4445return null;46}4748public function setIsCloseByCommit($is_close_by_commit) {49$this->isCloseByCommit = $is_close_by_commit;50return $this;51}5253public function getIsCloseByCommit() {54return $this->isCloseByCommit;55}5657public function setChangedPriorToCommitURI($uri) {58$this->changedPriorToCommitURI = $uri;59return $this;60}6162public function getChangedPriorToCommitURI() {63return $this->changedPriorToCommitURI;64}6566public function setRepositoryPHIDOverride($phid_or_null) {67$this->repositoryPHIDOverride = $phid_or_null;68return $this;69}7071public function getTransactionTypes() {72$types = parent::getTransactionTypes();7374$types[] = PhabricatorTransactions::TYPE_COMMENT;75$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;76$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;77$types[] = PhabricatorTransactions::TYPE_INLINESTATE;7879$types[] = DifferentialTransaction::TYPE_INLINE;8081return $types;82}8384protected function getCustomTransactionOldValue(85PhabricatorLiskDAO $object,86PhabricatorApplicationTransaction $xaction) {8788switch ($xaction->getTransactionType()) {89case DifferentialTransaction::TYPE_INLINE:90return null;91}9293return parent::getCustomTransactionOldValue($object, $xaction);94}9596protected function getCustomTransactionNewValue(97PhabricatorLiskDAO $object,98PhabricatorApplicationTransaction $xaction) {99100switch ($xaction->getTransactionType()) {101case DifferentialTransaction::TYPE_INLINE:102return null;103}104105return parent::getCustomTransactionNewValue($object, $xaction);106}107108protected function applyCustomInternalTransaction(109PhabricatorLiskDAO $object,110PhabricatorApplicationTransaction $xaction) {111112switch ($xaction->getTransactionType()) {113case DifferentialTransaction::TYPE_INLINE:114$comment = $xaction->getComment();115116$comment->setAttribute('editing', false);117118PhabricatorVersionedDraft::purgeDrafts(119$comment->getPHID(),120$this->getActingAsPHID());121return;122}123124return parent::applyCustomInternalTransaction($object, $xaction);125}126127protected function expandTransactions(128PhabricatorLiskDAO $object,129array $xactions) {130131foreach ($xactions as $xaction) {132switch ($xaction->getTransactionType()) {133case PhabricatorTransactions::TYPE_INLINESTATE:134// If we have an "Inline State" transaction already, the caller135// built it for us so we don't need to expand it again.136$this->didExpandInlineState = true;137break;138case DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE:139if ($xaction->getMetadataValue('draft.demote')) {140$this->isDraftDemotion = true;141}142break;143}144}145146$this->wasBroadcasting = $object->getShouldBroadcast();147148return parent::expandTransactions($object, $xactions);149}150151protected function expandTransaction(152PhabricatorLiskDAO $object,153PhabricatorApplicationTransaction $xaction) {154155$results = parent::expandTransaction($object, $xaction);156157$actor = $this->getActor();158$actor_phid = $this->getActingAsPHID();159$type_edge = PhabricatorTransactions::TYPE_EDGE;160161$edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;162163$want_downgrade = array();164$must_downgrade = array();165if ($this->getIsCloseByCommit()) {166// Never downgrade reviewers when we're closing a revision after a167// commit.168} else {169switch ($xaction->getTransactionType()) {170case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:171$want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;172$want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;173break;174case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE:175if (!$object->isChangePlanned()) {176// If the old state isn't "Changes Planned", downgrade the accepts177// even if they're sticky.178179// We don't downgrade for "Changes Planned" to allow an author to180// undo a "Plan Changes" by immediately following it up with a181// "Request Review".182$want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;183$must_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;184}185$want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;186break;187}188}189190if ($want_downgrade) {191$void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE;192193$results[] = id(new DifferentialTransaction())194->setTransactionType($void_type)195->setIgnoreOnNoEffect(true)196->setMetadataValue('void.force', $must_downgrade)197->setNewValue($want_downgrade);198}199200$new_author_phid = null;201switch ($xaction->getTransactionType()) {202case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:203if ($this->getIsCloseByCommit()) {204// Don't bother with any of this if this update is a side effect of205// commit detection.206break;207}208209// When a revision is updated and the diff comes from a branch named210// "T123" or similar, automatically associate the commit with the211// task that the branch names.212213$maniphest = 'PhabricatorManiphestApplication';214if (PhabricatorApplication::isClassInstalled($maniphest)) {215$diff = $this->requireDiff($xaction->getNewValue());216$branch = $diff->getBranch();217218// No "$", to allow for branches like T123_demo.219$match = null;220if ($branch !== null && preg_match('/^T(\d+)/i', $branch, $match)) {221$task_id = $match[1];222$tasks = id(new ManiphestTaskQuery())223->setViewer($this->getActor())224->withIDs(array($task_id))225->execute();226if ($tasks) {227$task = head($tasks);228$task_phid = $task->getPHID();229230$results[] = id(new DifferentialTransaction())231->setTransactionType($type_edge)232->setMetadataValue('edge:type', $edge_ref_task)233->setIgnoreOnNoEffect(true)234->setNewValue(array('+' => array($task_phid => $task_phid)));235}236}237}238break;239240case DifferentialRevisionCommandeerTransaction::TRANSACTIONTYPE:241$new_author_phid = $actor_phid;242break;243244case DifferentialRevisionAuthorTransaction::TRANSACTIONTYPE:245$new_author_phid = $xaction->getNewValue();246break;247248}249250if ($new_author_phid) {251$swap_xaction = $this->newSwapReviewersTransaction(252$object,253$new_author_phid);254if ($swap_xaction) {255$results[] = $swap_xaction;256}257}258259if (!$this->didExpandInlineState) {260switch ($xaction->getTransactionType()) {261case PhabricatorTransactions::TYPE_COMMENT:262case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:263case DifferentialTransaction::TYPE_INLINE:264$this->didExpandInlineState = true;265266$query_template = id(new DifferentialDiffInlineCommentQuery())267->withRevisionPHIDs(array($object->getPHID()));268269$state_xaction = $this->newInlineStateTransaction(270$object,271$query_template);272273if ($state_xaction) {274$results[] = $state_xaction;275}276break;277}278}279280return $results;281}282283protected function applyCustomExternalTransaction(284PhabricatorLiskDAO $object,285PhabricatorApplicationTransaction $xaction) {286287switch ($xaction->getTransactionType()) {288case DifferentialTransaction::TYPE_INLINE:289$reply = $xaction->getComment()->getReplyToComment();290if ($reply && !$reply->getHasReplies()) {291$reply->setHasReplies(1)->save();292}293return;294}295296return parent::applyCustomExternalTransaction($object, $xaction);297}298299protected function applyBuiltinExternalTransaction(300PhabricatorLiskDAO $object,301PhabricatorApplicationTransaction $xaction) {302303switch ($xaction->getTransactionType()) {304case PhabricatorTransactions::TYPE_INLINESTATE:305$table = new DifferentialTransactionComment();306$conn_w = $table->establishConnection('w');307foreach ($xaction->getNewValue() as $phid => $state) {308queryfx(309$conn_w,310'UPDATE %T SET fixedState = %s WHERE phid = %s',311$table->getTableName(),312$state,313$phid);314}315break;316}317318return parent::applyBuiltinExternalTransaction($object, $xaction);319}320321protected function applyFinalEffects(322PhabricatorLiskDAO $object,323array $xactions) {324325// Load the most up-to-date version of the revision and its reviewers,326// so we don't need to try to deduce the state of reviewers by examining327// all the changes made by the transactions. Then, update the reviewers328// on the object to make sure we're acting on the current reviewer set329// (and, for example, sending mail to the right people).330331$new_revision = id(new DifferentialRevisionQuery())332->setViewer($this->getActor())333->needReviewers(true)334->needActiveDiffs(true)335->withIDs(array($object->getID()))336->executeOne();337if (!$new_revision) {338throw new Exception(339pht('Failed to load revision from transaction finalization.'));340}341342$active_diff = $new_revision->getActiveDiff();343$new_diff_phid = $active_diff->getPHID();344345$object->attachReviewers($new_revision->getReviewers());346$object->attachActiveDiff($active_diff);347$object->attachRepository($new_revision->getRepository());348349$has_new_diff = false;350$should_index_paths = false;351$should_index_hashes = false;352$need_changesets = false;353354foreach ($xactions as $xaction) {355switch ($xaction->getTransactionType()) {356case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:357$need_changesets = true;358359$new_diff_phid = $xaction->getNewValue();360$has_new_diff = true;361362$should_index_paths = true;363$should_index_hashes = true;364break;365case DifferentialRevisionRepositoryTransaction::TRANSACTIONTYPE:366// The "AffectedPath" table denormalizes the repository, so we367// want to update the index if the repository changes.368369$need_changesets = true;370371$should_index_paths = true;372break;373}374}375376if ($need_changesets) {377$new_diff = $this->requireDiff($new_diff_phid, true);378379if ($should_index_paths) {380id(new DifferentialAffectedPathEngine())381->setRevision($object)382->setDiff($new_diff)383->updateAffectedPaths();384}385386if ($should_index_hashes) {387$this->updateRevisionHashTable($object, $new_diff);388}389390if ($has_new_diff) {391$this->ownersDiff = $new_diff;392$this->ownersChangesets = $new_diff->getChangesets();393}394}395396$xactions = $this->updateReviewStatus($object, $xactions);397$this->markReviewerComments($object, $xactions);398399return $xactions;400}401402private function updateReviewStatus(403DifferentialRevision $revision,404array $xactions) {405406$was_accepted = $revision->isAccepted();407$was_revision = $revision->isNeedsRevision();408$was_review = $revision->isNeedsReview();409if (!$was_accepted && !$was_revision && !$was_review) {410// Revisions can't transition out of other statuses (like closed or411// abandoned) as a side effect of reviewer status changes.412return $xactions;413}414415// Try to move a revision to "accepted". We look for:416//417// - at least one accepting reviewer who is a user; and418// - no rejects; and419// - no rejects of older diffs; and420// - no blocking reviewers.421422$has_accepting_user = false;423$has_rejecting_reviewer = false;424$has_rejecting_older_reviewer = false;425$has_blocking_reviewer = false;426427$active_diff = $revision->getActiveDiff();428foreach ($revision->getReviewers() as $reviewer) {429$reviewer_status = $reviewer->getReviewerStatus();430switch ($reviewer_status) {431case DifferentialReviewerStatus::STATUS_REJECTED:432$active_phid = $active_diff->getPHID();433if ($reviewer->isRejected($active_phid)) {434$has_rejecting_reviewer = true;435} else {436$has_rejecting_older_reviewer = true;437}438break;439case DifferentialReviewerStatus::STATUS_REJECTED_OLDER:440$has_rejecting_older_reviewer = true;441break;442case DifferentialReviewerStatus::STATUS_BLOCKING:443$has_blocking_reviewer = true;444break;445case DifferentialReviewerStatus::STATUS_ACCEPTED:446if ($reviewer->isUser()) {447$active_phid = $active_diff->getPHID();448if ($reviewer->isAccepted($active_phid)) {449$has_accepting_user = true;450}451}452break;453}454}455456$new_status = null;457if ($has_accepting_user &&458!$has_rejecting_reviewer &&459!$has_rejecting_older_reviewer &&460!$has_blocking_reviewer) {461$new_status = DifferentialRevisionStatus::ACCEPTED;462} else if ($has_rejecting_reviewer) {463// This isn't accepted, and there's at least one rejecting reviewer,464// so the revision needs changes. This usually happens after a465// "reject".466$new_status = DifferentialRevisionStatus::NEEDS_REVISION;467} else if ($was_accepted) {468// This revision was accepted, but it no longer satisfies the469// conditions for acceptance. This usually happens after an accepting470// reviewer resigns or is removed.471$new_status = DifferentialRevisionStatus::NEEDS_REVIEW;472} else if ($was_revision) {473// This revision was "Needs Revision", but no longer has any rejecting474// reviewers. This usually happens after the last rejecting reviewer475// resigns or is removed. Put the revision back in "Needs Review".476$new_status = DifferentialRevisionStatus::NEEDS_REVIEW;477}478479if ($new_status === null) {480return $xactions;481}482483$old_status = $revision->getModernRevisionStatus();484if ($new_status == $old_status) {485return $xactions;486}487488$xaction = id(new DifferentialTransaction())489->setTransactionType(490DifferentialRevisionStatusTransaction::TRANSACTIONTYPE)491->setOldValue($old_status)492->setNewValue($new_status);493494$xaction = $this->populateTransaction($revision, $xaction)495->save();496$xactions[] = $xaction;497498// Save the status adjustment we made earlier.499$revision500->setModernRevisionStatus($new_status)501->save();502503return $xactions;504}505506protected function sortTransactions(array $xactions) {507$xactions = parent::sortTransactions($xactions);508509$head = array();510$tail = array();511512foreach ($xactions as $xaction) {513$type = $xaction->getTransactionType();514if ($type == DifferentialTransaction::TYPE_INLINE) {515$tail[] = $xaction;516} else {517$head[] = $xaction;518}519}520521return array_values(array_merge($head, $tail));522}523524protected function shouldPublishFeedStory(525PhabricatorLiskDAO $object,526array $xactions) {527528if (!$object->getShouldBroadcast()) {529return false;530}531532return true;533}534535protected function shouldSendMail(536PhabricatorLiskDAO $object,537array $xactions) {538return true;539}540541protected function getMailTo(PhabricatorLiskDAO $object) {542if ($object->getShouldBroadcast()) {543$this->requireReviewers($object);544545$phids = array();546$phids[] = $object->getAuthorPHID();547foreach ($object->getReviewers() as $reviewer) {548if ($reviewer->isResigned()) {549continue;550}551552$phids[] = $reviewer->getReviewerPHID();553}554return $phids;555}556557// If we're demoting a draft after a build failure, just notify the author.558if ($this->isDraftDemotion) {559$author_phid = $object->getAuthorPHID();560return array(561$author_phid,562);563}564565return array();566}567568protected function getMailCC(PhabricatorLiskDAO $object) {569if (!$object->getShouldBroadcast()) {570return array();571}572573return parent::getMailCC($object);574}575576protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {577$this->requireReviewers($object);578579$phids = array();580581foreach ($object->getReviewers() as $reviewer) {582if ($reviewer->isResigned()) {583$phids[] = $reviewer->getReviewerPHID();584}585}586587return $phids;588}589590protected function getMailAction(591PhabricatorLiskDAO $object,592array $xactions) {593594$show_lines = false;595if ($this->isFirstBroadcast()) {596$action = pht('Request');597598$show_lines = true;599} else {600$action = parent::getMailAction($object, $xactions);601602$strongest = $this->getStrongestAction($object, $xactions);603$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;604if ($strongest->getTransactionType() == $type_update) {605$show_lines = true;606}607}608609if ($show_lines) {610$count = new PhutilNumber($object->getLineCount());611$action = pht('%s] [%s', $action, $object->getRevisionScaleGlyphs());612}613614return $action;615}616617protected function getMailSubjectPrefix() {618return pht('[Differential]');619}620621protected function getMailThreadID(PhabricatorLiskDAO $object) {622// This is nonstandard, but retains threading with older messages.623$phid = $object->getPHID();624return "differential-rev-{$phid}-req";625}626627protected function buildReplyHandler(PhabricatorLiskDAO $object) {628return id(new DifferentialReplyHandler())629->setMailReceiver($object);630}631632protected function buildMailTemplate(PhabricatorLiskDAO $object) {633$monogram = $object->getMonogram();634$title = $object->getTitle();635636return id(new PhabricatorMetaMTAMail())637->setSubject(pht('%s: %s', $monogram, $title))638->setMustEncryptSubject(pht('%s: Revision Updated', $monogram))639->setMustEncryptURI($object->getURI());640}641642protected function getTransactionsForMail(643PhabricatorLiskDAO $object,644array $xactions) {645// If this is the first time we're sending mail about this revision, we646// generate mail for all prior transactions, not just whatever is being647// applied now. This gets the "added reviewers" lines and other relevant648// information into the mail.649if ($this->isFirstBroadcast()) {650return $this->loadUnbroadcastTransactions($object);651}652653return $xactions;654}655656protected function getObjectLinkButtonLabelForMail(657PhabricatorLiskDAO $object) {658return pht('View Revision');659}660661protected function buildMailBody(662PhabricatorLiskDAO $object,663array $xactions) {664665$viewer = $this->requireActor();666667$body = id(new PhabricatorMetaMTAMailBody())668->setViewer($viewer);669670$revision_uri = $this->getObjectLinkButtonURIForMail($object);671$new_uri = $revision_uri.'/new/';672673$this->addHeadersAndCommentsToMailBody(674$body,675$xactions,676$this->getObjectLinkButtonLabelForMail($object),677$revision_uri);678679$type_inline = DifferentialTransaction::TYPE_INLINE;680681$inlines = array();682foreach ($xactions as $xaction) {683if ($xaction->getTransactionType() == $type_inline) {684$inlines[] = $xaction;685}686}687688if ($inlines) {689$this->appendInlineCommentsForMail($object, $inlines, $body);690}691692$update_xaction = null;693foreach ($xactions as $xaction) {694switch ($xaction->getTransactionType()) {695case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:696$update_xaction = $xaction;697break;698}699}700701if ($update_xaction) {702$diff = $this->requireDiff($update_xaction->getNewValue(), true);703} else {704$diff = null;705}706707$changed_uri = $this->getChangedPriorToCommitURI();708if ($changed_uri) {709$body->addLinkSection(710pht('CHANGED PRIOR TO COMMIT'),711$changed_uri);712}713714$this->addCustomFieldsToMailBody($body, $object, $xactions);715716if (!$this->isFirstBroadcast()) {717$body->addLinkSection(pht('CHANGES SINCE LAST ACTION'), $new_uri);718}719720$body->addLinkSection(721pht('REVISION DETAIL'),722$revision_uri);723724if ($update_xaction) {725$body->addTextSection(726pht('AFFECTED FILES'),727$this->renderAffectedFilesForMail($diff));728729$config_key_inline = 'metamta.differential.inline-patches';730$config_inline = PhabricatorEnv::getEnvConfig($config_key_inline);731732$config_key_attach = 'metamta.differential.attach-patches';733$config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);734735if ($config_inline || $config_attach) {736$body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');737738try {739$patch = $this->buildPatchForMail($diff, $body_limit);740} catch (ArcanistDiffByteSizeException $ex) {741$patch = null;742}743744if (($patch !== null) && $config_inline) {745$lines = substr_count($patch, "\n");746$bytes = strlen($patch);747748// Limit the patch size to the smaller of 256 bytes per line or749// the mail body limit. This prevents degenerate behavior for patches750// with one line that is 10MB long. See T11748.751$byte_limits = array();752$byte_limits[] = (256 * $config_inline);753$byte_limits[] = $body_limit;754$byte_limit = min($byte_limits);755756$lines_ok = ($lines <= $config_inline);757$bytes_ok = ($bytes <= $byte_limit);758759if ($lines_ok && $bytes_ok) {760$this->appendChangeDetailsForMail($object, $diff, $patch, $body);761} else {762// TODO: Provide a helpful message about the patch being too763// large or lengthy here.764}765}766767if (($patch !== null) && $config_attach) {768// See T12033, T11767, and PHI55. This is a crude fix to stop the769// major concrete problems that lackluster email size limits cause.770if (strlen($patch) < $body_limit) {771$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());772$mime_type = 'text/x-patch; charset=utf-8';773$body->addAttachment(774new PhabricatorMailAttachment($patch, $name, $mime_type));775}776}777}778}779780return $body;781}782783public function getMailTagsMap() {784return array(785DifferentialTransaction::MAILTAG_REVIEW_REQUEST =>786pht('A revision is created.'),787DifferentialTransaction::MAILTAG_UPDATED =>788pht('A revision is updated.'),789DifferentialTransaction::MAILTAG_COMMENT =>790pht('Someone comments on a revision.'),791DifferentialTransaction::MAILTAG_CLOSED =>792pht('A revision is closed.'),793DifferentialTransaction::MAILTAG_REVIEWERS =>794pht("A revision's reviewers change."),795DifferentialTransaction::MAILTAG_CC =>796pht("A revision's CCs change."),797DifferentialTransaction::MAILTAG_OTHER =>798pht('Other revision activity not listed above occurs.'),799);800}801802protected function supportsSearch() {803return true;804}805806protected function expandCustomRemarkupBlockTransactions(807PhabricatorLiskDAO $object,808array $xactions,809array $changes,810PhutilMarkupEngine $engine) {811812// For "Fixes ..." and "Depends on ...", we're only going to look at813// content blocks which are part of the revision itself (like "Summary"814// and "Test Plan"), not comments.815$content_parts = array();816foreach ($changes as $change) {817if ($change->getTransaction()->isCommentTransaction()) {818continue;819}820$content_parts[] = $change->getNewValue();821}822if (!$content_parts) {823return array();824}825$content_block = implode("\n\n", $content_parts);826$task_map = array();827$task_refs = id(new ManiphestCustomFieldStatusParser())828->parseCorpus($content_block);829foreach ($task_refs as $match) {830foreach ($match['monograms'] as $monogram) {831$task_id = (int)trim($monogram, 'tT');832$task_map[$task_id] = true;833}834}835836$rev_map = array();837$rev_refs = id(new DifferentialCustomFieldDependsOnParser())838->parseCorpus($content_block);839foreach ($rev_refs as $match) {840foreach ($match['monograms'] as $monogram) {841$rev_id = (int)trim($monogram, 'dD');842$rev_map[$rev_id] = true;843}844}845846$edges = array();847$task_phids = array();848$rev_phids = array();849850if ($task_map) {851$tasks = id(new ManiphestTaskQuery())852->setViewer($this->getActor())853->withIDs(array_keys($task_map))854->execute();855856if ($tasks) {857$task_phids = mpull($tasks, 'getPHID', 'getPHID');858$edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST;859$edges[$edge_related] = $task_phids;860}861}862863if ($rev_map) {864$revs = id(new DifferentialRevisionQuery())865->setViewer($this->getActor())866->withIDs(array_keys($rev_map))867->execute();868$rev_phids = mpull($revs, 'getPHID', 'getPHID');869870// NOTE: Skip any write attempts if a user cleverly implies a revision871// depends upon itself.872unset($rev_phids[$object->getPHID()]);873874if ($revs) {875$depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;876$edges[$depends] = $rev_phids;877}878}879880$revert_refs = id(new DifferentialCustomFieldRevertsParser())881->parseCorpus($content_block);882883$revert_monograms = array();884foreach ($revert_refs as $match) {885foreach ($match['monograms'] as $monogram) {886$revert_monograms[] = $monogram;887}888}889890if ($revert_monograms) {891$revert_objects = DiffusionCommitRevisionQuery::loadRevertedObjects(892$this->getActor(),893$object,894$revert_monograms,895null);896897$revert_phids = mpull($revert_objects, 'getPHID', 'getPHID');898899$revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST;900$edges[$revert_type] = $revert_phids;901} else {902$revert_phids = array();903}904905$this->addUnmentionablePHIDs($task_phids);906$this->addUnmentionablePHIDs($rev_phids);907$this->addUnmentionablePHIDs($revert_phids);908909$result = array();910foreach ($edges as $type => $specs) {911$result[] = id(new DifferentialTransaction())912->setTransactionType(PhabricatorTransactions::TYPE_EDGE)913->setMetadataValue('edge:type', $type)914->setNewValue(array('+' => $specs));915}916917return $result;918}919920private function appendInlineCommentsForMail(921PhabricatorLiskDAO $object,922array $inlines,923PhabricatorMetaMTAMailBody $body) {924925$limit = 100;926$limit_note = null;927if (count($inlines) > $limit) {928$limit_note = pht(929'(Showing first %s of %s inline comments.)',930new PhutilNumber($limit),931phutil_count($inlines));932933$inlines = array_slice($inlines, 0, $limit, true);934}935936$section = id(new DifferentialInlineCommentMailView())937->setViewer($this->getActor())938->setInlines($inlines)939->buildMailSection();940941$header = pht('INLINE COMMENTS');942943$section_text = "\n".$section->getPlaintext();944if ($limit_note) {945$section_text = $limit_note."\n".$section_text;946}947948$style = array(949'margin: 6px 0 12px 0;',950);951952$section_html = phutil_tag(953'div',954array(955'style' => implode(' ', $style),956),957$section->getHTML());958959if ($limit_note) {960$section_html = array(961phutil_tag(962'em',963array(),964$limit_note),965$section_html,966);967}968969$body->addPlaintextSection($header, $section_text, false);970$body->addHTMLSection($header, $section_html);971}972973private function appendChangeDetailsForMail(974PhabricatorLiskDAO $object,975DifferentialDiff $diff,976$patch,977PhabricatorMetaMTAMailBody $body) {978979$section = id(new DifferentialChangeDetailMailView())980->setViewer($this->getActor())981->setDiff($diff)982->setPatch($patch)983->buildMailSection();984985$header = pht('CHANGE DETAILS');986987$section_text = "\n".$section->getPlaintext();988989$style = array(990'margin: 6px 0 12px 0;',991);992993$section_html = phutil_tag(994'div',995array(996'style' => implode(' ', $style),997),998$section->getHTML());9991000$body->addPlaintextSection($header, $section_text, false);1001$body->addHTMLSection($header, $section_html);1002}10031004private function loadDiff($phid, $need_changesets = false) {1005$query = id(new DifferentialDiffQuery())1006->withPHIDs(array($phid))1007->setViewer($this->getActor());10081009if ($need_changesets) {1010$query->needChangesets(true);1011}10121013return $query->executeOne();1014}10151016public function requireDiff($phid, $need_changesets = false) {1017$diff = $this->loadDiff($phid, $need_changesets);1018if (!$diff) {1019throw new Exception(pht('Diff "%s" does not exist!', $phid));1020}10211022return $diff;1023}10241025/* -( Herald Integration )------------------------------------------------- */10261027protected function shouldApplyHeraldRules(1028PhabricatorLiskDAO $object,1029array $xactions) {1030return true;1031}10321033protected function didApplyHeraldRules(1034PhabricatorLiskDAO $object,1035HeraldAdapter $adapter,1036HeraldTranscript $transcript) {10371038$repository = $object->getRepository();1039if (!$repository) {1040return array();1041}10421043$diff = $this->ownersDiff;1044$changesets = $this->ownersChangesets;10451046$this->ownersDiff = null;1047$this->ownersChangesets = null;10481049if (!$changesets) {1050return array();1051}10521053$packages = PhabricatorOwnersPackage::loadAffectedPackagesForChangesets(1054$repository,1055$diff,1056$changesets);1057if (!$packages) {1058return array();1059}10601061// Identify the packages with "Non-Owner Author" review rules and remove1062// them if the author has authority over the package.10631064$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();1065$need_authority = array();1066foreach ($packages as $package) {1067$autoreview_setting = $package->getAutoReview();10681069$spec = idx($autoreview_map, $autoreview_setting);1070if (!$spec) {1071continue;1072}10731074if (idx($spec, 'authority')) {1075$need_authority[$package->getPHID()] = $package->getPHID();1076}1077}10781079if ($need_authority) {1080$authority = id(new PhabricatorOwnersPackageQuery())1081->setViewer(PhabricatorUser::getOmnipotentUser())1082->withPHIDs($need_authority)1083->withAuthorityPHIDs(array($object->getAuthorPHID()))1084->execute();1085$authority = mpull($authority, null, 'getPHID');10861087foreach ($packages as $key => $package) {1088$package_phid = $package->getPHID();1089if (isset($authority[$package_phid])) {1090unset($packages[$key]);1091continue;1092}1093}10941095if (!$packages) {1096return array();1097}1098}10991100$auto_subscribe = array();1101$auto_review = array();1102$auto_block = array();11031104foreach ($packages as $package) {1105switch ($package->getAutoReview()) {1106case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW:1107case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW_ALWAYS:1108$auto_review[] = $package;1109break;1110case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK:1111case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK_ALWAYS:1112$auto_block[] = $package;1113break;1114case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE:1115case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE_ALWAYS:1116$auto_subscribe[] = $package;1117break;1118case PhabricatorOwnersPackage::AUTOREVIEW_NONE:1119default:1120break;1121}1122}11231124$owners_phid = id(new PhabricatorOwnersApplication())1125->getPHID();11261127$xactions = array();1128if ($auto_subscribe) {1129$xactions[] = $object->getApplicationTransactionTemplate()1130->setAuthorPHID($owners_phid)1131->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)1132->setNewValue(1133array(1134'+' => mpull($auto_subscribe, 'getPHID'),1135));1136}11371138$specs = array(1139array($auto_review, false),1140array($auto_block, true),1141);11421143foreach ($specs as $spec) {1144list($reviewers, $blocking) = $spec;1145if (!$reviewers) {1146continue;1147}11481149$phids = mpull($reviewers, 'getPHID');1150$xaction = $this->newAutoReviewTransaction($object, $phids, $blocking);1151if ($xaction) {1152$xactions[] = $xaction;1153}1154}11551156return $xactions;1157}11581159private function newAutoReviewTransaction(1160PhabricatorLiskDAO $object,1161array $phids,1162$is_blocking) {11631164// TODO: This is substantially similar to DifferentialReviewersHeraldAction1165// and both are needlessly complex. This logic should live in the normal1166// transaction application pipeline. See T10967.11671168$reviewers = $object->getReviewers();1169$reviewers = mpull($reviewers, null, 'getReviewerPHID');11701171if ($is_blocking) {1172$new_status = DifferentialReviewerStatus::STATUS_BLOCKING;1173} else {1174$new_status = DifferentialReviewerStatus::STATUS_ADDED;1175}11761177$new_strength = DifferentialReviewerStatus::getStatusStrength(1178$new_status);11791180$current = array();1181foreach ($phids as $phid) {1182if (!isset($reviewers[$phid])) {1183continue;1184}11851186// If we're applying a stronger status (usually, upgrading a reviewer1187// into a blocking reviewer), skip this check so we apply the change.1188$old_strength = DifferentialReviewerStatus::getStatusStrength(1189$reviewers[$phid]->getReviewerStatus());1190if ($old_strength <= $new_strength) {1191continue;1192}11931194$current[] = $phid;1195}11961197$phids = array_diff($phids, $current);11981199if (!$phids) {1200return null;1201}12021203$phids = array_fuse($phids);12041205$value = array();1206foreach ($phids as $phid) {1207if ($is_blocking) {1208$value[] = 'blocking('.$phid.')';1209} else {1210$value[] = $phid;1211}1212}12131214$owners_phid = id(new PhabricatorOwnersApplication())1215->getPHID();12161217$reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;12181219return $object->getApplicationTransactionTemplate()1220->setAuthorPHID($owners_phid)1221->setTransactionType($reviewers_type)1222->setNewValue(1223array(1224'+' => $value,1225));1226}12271228protected function buildHeraldAdapter(1229PhabricatorLiskDAO $object,1230array $xactions) {12311232$revision = id(new DifferentialRevisionQuery())1233->setViewer($this->getActor())1234->withPHIDs(array($object->getPHID()))1235->needActiveDiffs(true)1236->needReviewers(true)1237->executeOne();1238if (!$revision) {1239throw new Exception(1240pht('Failed to load revision for Herald adapter construction!'));1241}12421243$adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(1244$revision,1245$revision->getActiveDiff());12461247// If the object is still a draft, prevent "Send me an email" and other1248// similar rules from acting yet.1249if (!$object->getShouldBroadcast()) {1250$adapter->setForbiddenAction(1251HeraldMailableState::STATECONST,1252DifferentialHeraldStateReasons::REASON_DRAFT);1253}12541255// If this edit didn't actually change the diff (for example, a user1256// edited the title or changed subscribers), prevent "Run build plan"1257// and other similar rules from acting yet, since the build results will1258// not (or, at least, should not) change unless the actual source changes.1259// We also don't run Differential builds if the update was caused by1260// discovering a commit, as the expectation is that Diffusion builds take1261// over once things land.1262$has_update = false;1263$has_commit = false;12641265$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;1266foreach ($xactions as $xaction) {1267if ($xaction->getTransactionType() != $type_update) {1268continue;1269}12701271if ($xaction->getMetadataValue('isCommitUpdate')) {1272$has_commit = true;1273} else {1274$has_update = true;1275}12761277break;1278}12791280if ($has_commit) {1281$adapter->setForbiddenAction(1282HeraldBuildableState::STATECONST,1283DifferentialHeraldStateReasons::REASON_LANDED);1284} else if (!$has_update) {1285$adapter->setForbiddenAction(1286HeraldBuildableState::STATECONST,1287DifferentialHeraldStateReasons::REASON_UNCHANGED);1288}12891290return $adapter;1291}12921293/**1294* Update the table connecting revisions to DVCS local hashes, so we can1295* identify revisions by commit/tree hashes.1296*/1297private function updateRevisionHashTable(1298DifferentialRevision $revision,1299DifferentialDiff $diff) {13001301$vcs = $diff->getSourceControlSystem();1302if ($vcs == DifferentialRevisionControlSystem::SVN) {1303// Subversion has no local commit or tree hash information, so we don't1304// have to do anything.1305return;1306}13071308$property = id(new DifferentialDiffProperty())->loadOneWhere(1309'diffID = %d AND name = %s',1310$diff->getID(),1311'local:commits');1312if (!$property) {1313return;1314}13151316$hashes = array();13171318$data = $property->getData();1319switch ($vcs) {1320case DifferentialRevisionControlSystem::GIT:1321foreach ($data as $commit) {1322$hashes[] = array(1323ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,1324$commit['commit'],1325);1326$hashes[] = array(1327ArcanistDifferentialRevisionHash::HASH_GIT_TREE,1328$commit['tree'],1329);1330}1331break;1332case DifferentialRevisionControlSystem::MERCURIAL:1333foreach ($data as $commit) {1334$hashes[] = array(1335ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,1336$commit['rev'],1337);1338}1339break;1340}13411342$conn_w = $revision->establishConnection('w');13431344$sql = array();1345foreach ($hashes as $info) {1346list($type, $hash) = $info;1347$sql[] = qsprintf(1348$conn_w,1349'(%d, %s, %s)',1350$revision->getID(),1351$type,1352$hash);1353}13541355queryfx(1356$conn_w,1357'DELETE FROM %T WHERE revisionID = %d',1358ArcanistDifferentialRevisionHash::TABLE_NAME,1359$revision->getID());13601361if ($sql) {1362queryfx(1363$conn_w,1364'INSERT INTO %T (revisionID, type, hash) VALUES %LQ',1365ArcanistDifferentialRevisionHash::TABLE_NAME,1366$sql);1367}1368}13691370private function renderAffectedFilesForMail(DifferentialDiff $diff) {1371$changesets = $diff->getChangesets();13721373$filenames = mpull($changesets, 'getDisplayFilename');1374sort($filenames);13751376$count = count($filenames);1377$max = 250;1378if ($count > $max) {1379$filenames = array_slice($filenames, 0, $max);1380$filenames[] = pht('(%d more files...)', ($count - $max));1381}13821383return implode("\n", $filenames);1384}13851386private function renderPatchHTMLForMail($patch) {1387return phutil_tag('pre',1388array('style' => 'font-family: monospace;'), $patch);1389}13901391private function buildPatchForMail(DifferentialDiff $diff, $byte_limit) {1392$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');13931394return id(new DifferentialRawDiffRenderer())1395->setViewer($this->getActor())1396->setFormat($format)1397->setChangesets($diff->getChangesets())1398->setByteLimit($byte_limit)1399->buildPatch();1400}14011402protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {1403// Reload to pick up the active diff and reviewer status.1404return id(new DifferentialRevisionQuery())1405->setViewer($this->getActor())1406->needReviewers(true)1407->needActiveDiffs(true)1408->withIDs(array($object->getID()))1409->executeOne();1410}14111412protected function getCustomWorkerState() {1413return array(1414'changedPriorToCommitURI' => $this->changedPriorToCommitURI,1415'firstBroadcast' => $this->firstBroadcast,1416'isDraftDemotion' => $this->isDraftDemotion,1417);1418}14191420protected function loadCustomWorkerState(array $state) {1421$this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI');1422$this->firstBroadcast = idx($state, 'firstBroadcast');1423$this->isDraftDemotion = idx($state, 'isDraftDemotion');1424return $this;1425}14261427private function newSwapReviewersTransaction(1428DifferentialRevision $revision,1429$new_author_phid) {14301431$old_author_phid = $revision->getAuthorPHID();14321433if ($old_author_phid === $new_author_phid) {1434return;1435}14361437// If the revision is changing authorship, add the previous author as a1438// reviewer and remove the new author.14391440$edits = array(1441'-' => array(1442$new_author_phid,1443),1444'+' => array(1445$old_author_phid,1446),1447);14481449// NOTE: We're setting setIsCommandeerSideEffect() on this because normally1450// you can't add a revision's author as a reviewer, but this action swaps1451// them after validation executes.14521453$xaction_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;14541455return id(new DifferentialTransaction())1456->setTransactionType($xaction_type)1457->setIgnoreOnNoEffect(true)1458->setIsCommandeerSideEffect(true)1459->setNewValue($edits);1460}146114621463public function getActiveDiff($object) {1464if ($this->getIsNewObject()) {1465return null;1466} else {1467return $object->getActiveDiff();1468}1469}14701471/**1472* When a reviewer makes a comment, mark the last revision they commented1473* on.1474*1475* This allows us to show a hint to help authors and other reviewers quickly1476* distinguish between reviewers who have participated in the discussion and1477* reviewers who haven't been part of it.1478*/1479private function markReviewerComments($object, array $xactions) {1480$acting_phid = $this->getActingAsPHID();1481if (!$acting_phid) {1482return;1483}14841485$diff = $this->getActiveDiff($object);1486if (!$diff) {1487return;1488}14891490$has_comment = false;1491foreach ($xactions as $xaction) {1492if ($xaction->hasComment()) {1493$has_comment = true;1494break;1495}1496}14971498if (!$has_comment) {1499return;1500}15011502$reviewer_table = new DifferentialReviewer();1503$conn = $reviewer_table->establishConnection('w');15041505queryfx(1506$conn,1507'UPDATE %T SET lastCommentDiffPHID = %s1508WHERE revisionPHID = %s1509AND reviewerPHID = %s',1510$reviewer_table->getTableName(),1511$diff->getPHID(),1512$object->getPHID(),1513$acting_phid);1514}15151516private function loadUnbroadcastTransactions($object) {1517$viewer = $this->requireActor();15181519$xactions = id(new DifferentialTransactionQuery())1520->setViewer($viewer)1521->withObjectPHIDs(array($object->getPHID()))1522->execute();15231524return array_reverse($xactions);1525}152615271528protected function didApplyTransactions($object, array $xactions) {1529// In a moment, we're going to try to publish draft revisions which have1530// completed all their builds. However, we only want to do that if the1531// actor is either the revision author or an omnipotent user (generally,1532// the Harbormaster application).15331534// If we let any actor publish the revision as a side effect of other1535// changes then an unlucky third party who innocently comments on the draft1536// can end up racing Harbormaster and promoting the revision. At best, this1537// is confusing. It can also run into validation problems with the "Request1538// Review" transaction. See PHI309 for some discussion.1539$author_phid = $object->getAuthorPHID();1540$viewer = $this->requireActor();1541$can_undraft =1542($this->getActingAsPHID() === $author_phid) ||1543($viewer->isOmnipotent());15441545// If a draft revision has no outstanding builds and we're automatically1546// making drafts public after builds finish, make the revision public.1547if ($can_undraft) {1548$auto_undraft = !$object->getHoldAsDraft();1549} else {1550$auto_undraft = false;1551}15521553$can_promote = false;1554$can_demote = false;15551556// "Draft" revisions can promote to "Review Requested" after builds pass,1557// or demote to "Changes Planned" after builds fail.1558if ($object->isDraft()) {1559$can_promote = true;1560$can_demote = true;1561}15621563// See PHI584. "Changes Planned" revisions which are not yet broadcasting1564// can promote to "Review Requested" if builds pass.15651566// This pass is presumably the result of someone restarting the builds and1567// having them work this time, perhaps because the builds are not perfectly1568// reliable or perhaps because someone fixed some issue with build hardware1569// or some other dependency.15701571// Currently, there's no legitimate way to end up in this state except1572// through automatic demotion, so this behavior should not generate an1573// undue level of confusion or ambiguity. Also note that these changes can1574// not demote again since they've already been demoted once.1575if ($object->isChangePlanned()) {1576if (!$object->getShouldBroadcast()) {1577$can_promote = true;1578}1579}15801581if (($can_promote || $can_demote) && $auto_undraft) {1582$status = $this->loadCompletedBuildableStatus($object);15831584$is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED);1585$is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED);15861587if ($is_passed && $can_promote) {1588// When Harbormaster moves a revision out of the draft state, we1589// attribute the action to the revision author since this is more1590// natural and more useful.15911592// Additionally, we change the acting PHID for the transaction set1593// to the author if it isn't already a user so that mail comes from1594// the natural author.1595$acting_phid = $this->getActingAsPHID();1596$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;1597if (phid_get_type($acting_phid) != $user_type) {1598$this->setActingAsPHID($author_phid);1599}16001601$xaction = $object->getApplicationTransactionTemplate()1602->setAuthorPHID($author_phid)1603->setTransactionType(1604DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE)1605->setNewValue(true);16061607// If we're creating this revision and immediately moving it out of1608// the draft state, mark this as a create transaction so it gets1609// hidden in the timeline and mail, since it isn't interesting: it1610// is as though the draft phase never happened.1611if ($this->getIsNewObject()) {1612$xaction->setIsCreateTransaction(true);1613}16141615// Queue this transaction and apply it separately after the current1616// batch of transactions finishes so that Herald can fire on the new1617// revision state. See T13027 for discussion.1618$this->queueTransaction($xaction);1619} else if ($is_failed && $can_demote) {1620// When demoting a revision, we act as "Harbormaster" instead of1621// the author since this feels a little more natural.1622$harbormaster_phid = id(new PhabricatorHarbormasterApplication())1623->getPHID();16241625$xaction = $object->getApplicationTransactionTemplate()1626->setAuthorPHID($harbormaster_phid)1627->setMetadataValue('draft.demote', true)1628->setTransactionType(1629DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE)1630->setNewValue(true);16311632$this->queueTransaction($xaction);1633}1634}16351636// If the revision is new or was a draft, and is no longer a draft, we1637// might be sending the first email about it.16381639// This might mean it was created directly into a non-draft state, or1640// it just automatically undrafted after builds finished, or a user1641// explicitly promoted it out of the draft state with an action like1642// "Request Review".16431644// If we haven't sent any email about it yet, mark this email as the first1645// email so the mail gets enriched with "SUMMARY" and "TEST PLAN".16461647$is_new = $this->getIsNewObject();1648$was_broadcasting = $this->wasBroadcasting;16491650if ($object->getShouldBroadcast()) {1651if (!$was_broadcasting || $is_new) {1652// Mark this as the first broadcast we're sending about the revision1653// so mail can generate specially.1654$this->firstBroadcast = true;1655}1656}16571658return $xactions;1659}16601661private function loadCompletedBuildableStatus(1662DifferentialRevision $revision) {1663$viewer = $this->requireActor();1664$builds = $revision->loadImpactfulBuilds($viewer);1665return $revision->newBuildableStatusForBuilds($builds);1666}16671668private function requireReviewers(DifferentialRevision $revision) {1669if ($revision->hasAttachedReviewers()) {1670return;1671}16721673$with_reviewers = id(new DifferentialRevisionQuery())1674->setViewer($this->getActor())1675->needReviewers(true)1676->withPHIDs(array($revision->getPHID()))1677->executeOne();1678if (!$with_reviewers) {1679throw new Exception(1680pht(1681'Failed to reload revision ("%s").',1682$revision->getPHID()));1683}16841685$revision->attachReviewers($with_reviewers->getReviewers());1686}168716881689}169016911692