diff --git a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php index 73bf1999ac..2f012dba41 100644 --- a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php +++ b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php @@ -1,572 +1,559 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Handle major edit operations to DifferentialRevision -- adding and removing * reviewers, diffs, and CCs. Unlike simple edits, these changes trigger * complicated email workflows. */ class DifferentialRevisionEditor { protected $revision; protected $actorPHID; protected $cc = null; protected $reviewers = null; protected $diff; protected $comments; protected $silentUpdate; public function __construct(DifferentialRevision $revision, $actor_phid) { $this->revision = $revision; $this->actorPHID = $actor_phid; } public static function newRevisionFromConduitWithDiff( array $fields, DifferentialDiff $diff, $user_phid) { $revision = new DifferentialRevision(); $revision->setPHID($revision->generatePHID()); $revision->setAuthorPHID($user_phid); $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW); $editor = new DifferentialRevisionEditor($revision, $user_phid); $editor->copyFieldsFromConduit($fields); $editor->addDiff($diff, null); $editor->save(); return $revision; } public function copyFieldsFromConduit(array $fields) { $revision = $this->revision; $revision->setTitle((string)$fields['title']); $revision->setSummary((string)$fields['summary']); $revision->setTestPlan((string)$fields['testPlan']); $revision->setBlameRevision((string)$fields['blameRevision']); $revision->setRevertPlan((string)$fields['revertPlan']); $this->setReviewers($fields['reviewerPHIDs']); $this->setCCPHIDs($fields['ccPHIDs']); } public function getRevision() { return $this->revision; } public function setReviewers(array $reviewers) { $this->reviewers = $reviewers; return $this; } public function setCCPHIDs(array $cc) { $this->cc = $cc; return $this; } public function addDiff(DifferentialDiff $diff, $comments) { if ($diff->getRevisionID() && $diff->getRevisionID() != $this->getRevision()->getID()) { $diff_id = (int)$diff->getID(); $targ_id = (int)$this->getRevision()->getID(); $real_id = (int)$diff->getRevisionID(); throw new Exception( "Can not attach diff #{$diff_id} to Revision D{$targ_id}, it is ". "already attached to D{$real_id}."); } $this->diff = $diff; $this->comments = $comments; return $this; } protected function getDiff() { return $this->diff; } protected function getComments() { return $this->comments; } protected function getActorPHID() { return $this->actorPHID; } public function isNewRevision() { return !$this->getRevision()->getID(); } /** * A silent update does not trigger Herald rules or send emails. This is used * for auto-amends at commit time. */ public function setSilentUpdate($silent) { $this->silentUpdate = $silent; return $this; } public function save() { $revision = $this->getRevision(); // TODO // $revision->openTransaction(); $is_new = $this->isNewRevision(); if ($is_new) { // These fields aren't nullable; set them to sensible defaults if they // haven't been configured. We're just doing this so we can generate an // ID for the revision if we don't have one already. $revision->setLineCount(0); if ($revision->getStatus() === null) { $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW); } if ($revision->getTitle() === null) { $revision->setTitle('Untitled Revision'); } if ($revision->getAuthorPHID() === null) { $revision->setAuthorPHID($this->getActorPHID()); } $revision->save(); } $revision->loadRelationships(); if ($this->reviewers === null) { $this->reviewers = $revision->getReviewers(); } if ($this->cc === null) { $this->cc = $revision->getCCPHIDs(); } // We're going to build up three dictionaries: $add, $rem, and $stable. The // $add dictionary has added reviewers/CCs. The $rem dictionary has // reviewers/CCs who have been removed, and the $stable array is // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs // a different ("welcome") email than we send stable reviewers/CCs. $old = array( 'rev' => array_fill_keys($revision->getReviewers(), true), 'ccs' => array_fill_keys($revision->getCCPHIDs(), true), ); $diff = $this->getDiff(); $xscript_header = null; $xscript_uri = null; $new = array( 'rev' => array_fill_keys($this->reviewers, true), 'ccs' => array_fill_keys($this->cc, true), ); $rem_ccs = array(); if ($diff) { $diff->setRevisionID($revision->getID()); $revision->setLineCount($diff->getLineCount()); -// TODO! -// $revision->setRepositoryID($diff->getRepositoryID()); - -/* - $iface = new DifferentialRevisionHeraldable($revision); - $iface->setExplicitCCs($new['ccs']); - $iface->setExplicitReviewers($new['rev']); - $iface->setForbiddenCCs($revision->getForbiddenCCPHIDs()); - $iface->setForbiddenReviewers($revision->getForbiddenReviewers()); - $iface->setDiff($diff); - - $xscript = HeraldEngine::loadAndApplyRules($iface); - $xscript_uri = $xscript->getURI(); + $adapter = new HeraldDifferentialRevisionAdapter( + $revision, + $diff); + $adapter->setExplicitCCs($new['ccs']); + $adapter->setExplicitReviewers($new['rev']); + $adapter->setForbiddenCCs($revision->getUnsubscribed()); + + $xscript = HeraldEngine::loadAndApplyRules($adapter); + $xscript_uri = PhabricatorEnv::getProductionURI( + '/herald/transcript/'.$xscript->getID().'/'); $xscript_phid = $xscript->getPHID(); $xscript_header = $xscript->getXHeraldRulesHeader(); - - $sub = array( - 'rev' => array(), - 'ccs' => $iface->getCCsAddedByHerald(), - ); - $rem_ccs = $iface->getCCsRemovedByHerald(); -*/ - // TODO! $sub = array( 'rev' => array(), - 'ccs' => array(), + 'ccs' => $adapter->getCCsAddedByHerald(), ); - - + $rem_ccs = $adapter->getCCsRemovedByHerald(); } else { $sub = array( 'rev' => array(), 'ccs' => array(), ); } // Remove any CCs which are prevented by Herald rules. $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs); $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs); $add = array(); $rem = array(); $stable = array(); foreach (array('rev', 'ccs') as $key) { $add[$key] = array(); if ($new[$key] !== null) { $add[$key] += array_diff_key($new[$key], $old[$key]); } $add[$key] += array_diff_key($sub[$key], $old[$key]); $combined = $sub[$key]; if ($new[$key] !== null) { $combined += $new[$key]; } $rem[$key] = array_diff_key($old[$key], $combined); $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]); } self::alterReviewers( $revision, $this->reviewers, array_keys($rem['rev']), array_keys($add['rev']), $this->actorPHID); /* // TODO: When Herald is brought over, run through this stuff to figure // out which adds are Herald's fault. + // TODO: Still need to do this. + if ($add['ccs'] || $rem['ccs']) { foreach (array_keys($add['ccs']) as $id) { if (empty($new['ccs'][$id])) { $reason_phid = 'TODO';//$xscript_phid; } else { $reason_phid = $this->getActorPHID(); } } foreach (array_keys($rem['ccs']) as $id) { if (empty($new['ccs'][$id])) { $reason_phid = $this->getActorPHID(); } else { $reason_phid = 'TODO';//$xscript_phid; } } } */ self::alterCCs( $revision, $this->cc, array_keys($rem['ccs']), array_keys($add['ccs']), $this->actorPHID); // Add the author to the relevant set of users so they get a copy of the // email. if (!$this->silentUpdate) { if ($is_new) { $add['rev'][$this->getActorPHID()] = true; } else { $stable['rev'][$this->getActorPHID()] = true; } } $mail = array(); $phids = array($this->getActorPHID()); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $actor_handle = $handles[$this->getActorPHID()]; $changesets = null; $comment = null; if ($diff) { $changesets = $diff->loadChangesets(); // TODO: This should probably be in DifferentialFeedbackEditor? if (!$is_new) { $comment = $this->createComment(); } if ($comment) { $mail[] = id(new DifferentialNewDiffMail( $revision, $actor_handle, $changesets)) ->setIsFirstMailAboutRevision($is_new) ->setIsFirstMailToRecipients($is_new) ->setComments($this->getComments()) ->setToPHIDs(array_keys($stable['rev'])) ->setCCPHIDs(array_keys($stable['ccs'])); } // Save the changes we made above. $diff->setDescription(substr($this->getComments(), 0, 80)); $diff->save(); // An updated diff should require review, as long as it's not committed // or accepted. The "accepted" status is "sticky" to encourage courtesy // re-diffs after someone accepts with minor changes/suggestions. $status = $revision->getStatus(); if ($status != DifferentialRevisionStatus::COMMITTED && $status != DifferentialRevisionStatus::ACCEPTED) { $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW); } } else { $diff = $revision->loadActiveDiff(); if ($diff) { $changesets = $diff->loadChangesets(); } else { $changesets = array(); } } $revision->save(); // TODO // $revision->saveTransaction(); $event = array( 'revision_id' => $revision->getID(), 'PHID' => $revision->getPHID(), 'action' => $is_new ? 'create' : 'update', 'actor' => $this->getActorPHID(), ); -// TODO: When timelines get implemented, move indexing to them. +// TODO: Move this into a worker task thing. PhabricatorSearchDifferentialIndexer::indexRevision($revision); -// TODO -// id(new ToolsTimelineEvent('difx', fb_json_encode($event)))->record(); if ($this->silentUpdate) { return; } $revision->loadRelationships(); if ($add['rev']) { $message = id(new DifferentialNewDiffMail( $revision, $actor_handle, $changesets)) ->setIsFirstMailAboutRevision($is_new) ->setIsFirstMailToRecipients(true) ->setToPHIDs(array_keys($add['rev'])); if ($is_new) { // The first time we send an email about a revision, put the CCs in // the "CC:" field of the same "Review Requested" email that reviewers // get, so you don't get two initial emails if you're on a list that // is CC'd. $message->setCCPHIDs(array_keys($add['ccs'])); } $mail[] = $message; } // If you were added as a reviewer and a CC, just give you the reviewer // email. We could go to greater lengths to prevent this, but there's // bunch of stuff with list subscriptions anyway. You can still get two // emails, but only if a revision is updated and you are added as a reviewer // at the same time a list you are on is added as a CC, which is rare and // reasonable. $add['ccs'] = array_diff_key($add['ccs'], $add['rev']); if (!$is_new && $add['ccs']) { $mail[] = id(new DifferentialCCWelcomeMail( $revision, $actor_handle, $changesets)) ->setIsFirstMailToRecipients(true) ->setToPHIDs(array_keys($add['ccs'])); } foreach ($mail as $message) { -// TODO -// $message->setHeraldTranscriptURI($xscript_uri); -// $message->setXHeraldRulesHeader($xscript_header); + $message->setHeraldTranscriptURI($xscript_uri); + $message->setXHeraldRulesHeader($xscript_header); $message->send(); } } public static function addCC( DifferentialRevision $revision, $phid, $reason) { return self::alterCCs( $revision, $revision->getCCPHIDs(), $rem = array(), $add = array($phid), $reason); } public static function removeCC( DifferentialRevision $revision, $phid, $reason) { return self::alterCCs( $revision, $revision->getCCPHIDs(), $rem = array($phid), $add = array(), $reason); } protected static function alterCCs( DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid) { return self::alterRelationships( $revision, $stable_phids, $rem_phids, $add_phids, $reason_phid, DifferentialRevision::RELATION_SUBSCRIBED); } public static function alterReviewers( DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid) { return self::alterRelationships( $revision, $stable_phids, $rem_phids, $add_phids, $reason_phid, DifferentialRevision::RELATION_REVIEWER); } private static function alterRelationships( DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid, $relation_type) { $rem_map = array_fill_keys($rem_phids, true); $add_map = array_fill_keys($add_phids, true); $seq_map = array_values($stable_phids); $seq_map = array_flip($seq_map); foreach ($rem_map as $phid => $ignored) { if (!isset($seq_map[$phid])) { $seq_map[$phid] = count($seq_map); } } foreach ($add_map as $phid => $ignored) { if (!isset($seq_map[$phid])) { $seq_map[$phid] = count($seq_map); } } $raw = $revision->getRawRelations($relation_type); $raw = ipull($raw, null, 'objectPHID'); $sequence = count($seq_map); foreach ($raw as $phid => $ignored) { if (isset($seq_map[$phid])) { $raw[$phid]['sequence'] = $seq_map[$phid]; } else { $raw[$phid]['sequence'] = $sequence++; } } $raw = isort($raw, 'sequence'); foreach ($raw as $phid => $ignored) { if (isset($rem_map[$phid])) { unset($raw[$phid]); } } foreach ($add_phids as $add) { $raw[$add] = array( 'objectPHID' => $add, 'sequence' => idx($seq_map, $add, $sequence++), 'reasonPHID' => $reason_phid, ); } $conn_w = $revision->establishConnection('w'); $sql = array(); foreach ($raw as $relation) { $sql[] = qsprintf( $conn_w, '(%d, %s, %s, %d, %s)', $revision->getID(), $relation_type, $relation['objectPHID'], $relation['sequence'], $relation['reasonPHID']); } $conn_w->openTransaction(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d AND relation = %s', DifferentialRevision::RELATIONSHIP_TABLE, $revision->getID(), $relation_type); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (revisionID, relation, objectPHID, sequence, reasonPHID) VALUES %Q', DifferentialRevision::RELATIONSHIP_TABLE, implode(', ', $sql)); } $conn_w->saveTransaction(); } private function createComment() { $revision_id = $this->revision->getID(); $comment = id(new DifferentialComment()) ->setAuthorPHID($this->getActorPHID()) ->setRevisionID($revision_id) ->setContent($this->getComments()) ->setAction('update'); $comment->save(); return $comment; } } diff --git a/src/applications/differential/editor/revision/__init__.php b/src/applications/differential/editor/revision/__init__.php index 314d676c81..ad53cc8f87 100644 --- a/src/applications/differential/editor/revision/__init__.php +++ b/src/applications/differential/editor/revision/__init__.php @@ -1,22 +1,25 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus'); phutil_require_module('phabricator', 'applications/differential/mail/ccwelcome'); phutil_require_module('phabricator', 'applications/differential/mail/newdiff'); phutil_require_module('phabricator', 'applications/differential/storage/comment'); phutil_require_module('phabricator', 'applications/differential/storage/revision'); +phutil_require_module('phabricator', 'applications/herald/adapter/differential'); +phutil_require_module('phabricator', 'applications/herald/engine/engine'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'applications/search/index/indexer/differential'); +phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'storage/qsprintf'); phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phutil', 'utils'); phutil_require_source('DifferentialRevisionEditor.php'); diff --git a/src/applications/herald/adapter/differential/HeraldDifferentialRevisionAdapter.php b/src/applications/herald/adapter/differential/HeraldDifferentialRevisionAdapter.php index 318c9a5dbd..b4025a656f 100644 --- a/src/applications/herald/adapter/differential/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/herald/adapter/differential/HeraldDifferentialRevisionAdapter.php @@ -1,304 +1,298 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class HeraldDifferentialRevisionAdapter extends HeraldObjectAdapter { protected $revision; protected $diff; protected $explicitCCs; protected $explicitReviewers; protected $forbiddenCCs; - protected $forbiddenReviewers; protected $newCCs = array(); protected $remCCs = array(); protected $repository; protected $affectedPackages; protected $changesets; public function __construct( DifferentialRevision $revision, DifferentialDiff $diff) { $revision->loadRelationships(); $this->revision = $revision; $this->diff = $diff; } public function setExplicitCCs($explicit_ccs) { $this->explicitCCs = $explicit_ccs; return $this; } public function setExplicitReviewers($explicit_reviewers) { $this->explicitReviewers = $explicit_reviewers; return $this; } public function setForbiddenCCs($forbidden_ccs) { $this->forbiddenCCs = $forbidden_ccs; return $this; } - public function setForbiddenReviewers($forbidden_reviewers) { - $this->forbiddenReviewers = $forbidden_reviewers; - return $this; - } - public function getCCsAddedByHerald() { return array_diff_key($this->newCCs, $this->remCCs); } public function getCCsRemovedByHerald() { return $this->remCCs; } public function getPHID() { return $this->revision->getPHID(); } public function getHeraldName() { return $this->revision->getTitle(); } public function getHeraldTypeName() { return HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL; } public function loadRepository() { if ($this->repository === null) { $diff = $this->diff; $repository = false; if ($diff->getRepositoryUUID()) { $repository = id(new PhabricatorRepository())->loadOneWhere( 'uuid = %s', $diff->getRepositoryUUID()); } if (!$repository && $diff->getArcanistProjectPHID()) { $project = id(new PhabricatorRepositoryArcanistProject())->loadOneWhere( 'phid = %s', $diff->getArcanistProjectPHID()); if ($project && $project->getRepositoryID()) { $repository = id(new PhabricatorRepository())->load( $project->getRepositoryID()); } } $this->repository = $repository; } return $this->repository; } protected function loadChangesets() { if ($this->changesets === null) { $this->changesets = $this->diff->loadChangesets(); } return $this->changesets; } protected function loadAffectedPaths() { $changesets = $this->loadChangesets(); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $this->getAbsoluteRepositoryPathForChangeset($changeset); } return $paths; } protected function getAbsoluteRepositoryPathForChangeset( DifferentialChangeset $changeset) { $repository = $this->loadRepository(); if (!$repository) { return '/'.ltrim($changeset->getFilename(), '/'); } $diff = $this->diff; return $changeset->getAbsoluteRepositoryPath($diff, $repository); } protected function loadContentDictionary() { $changesets = $this->loadChangesets(); $hunks = array(); if ($changesets) { $hunks = id(new DifferentialHunk())->loadAllWhere( 'changesetID in (%Ld)', mpull($changesets, 'getID')); } $dict = array(); $hunks = mgroup($hunks, 'getChangesetID'); $changesets = mpull($changesets, null, 'getID'); foreach ($changesets as $id => $changeset) { $path = $this->getAbsoluteRepositoryPathForChangeset($changeset); $content = array(); foreach (idx($hunks, $id, array()) as $hunk) { $content[] = $hunk->makeChanges(); } $dict[$path] = implode("\n", $content); } return $dict; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $this->affectedPackages = array(); $repository = $this->loadRepository(); if ($repository) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $repository, $this->loadAffectedPaths()); $this->affectedPackages = $packages; } } return $this->affectedPackages; } public function getHeraldField($field) { switch ($field) { case HeraldFieldConfig::FIELD_TITLE: return $this->revision->getTitle(); break; case HeraldFieldConfig::FIELD_BODY: return $this->revision->getSummary()."\n". $this->revision->getTestPlan(); break; case HeraldFieldConfig::FIELD_AUTHOR: return $this->revision->getAuthorPHID(); break; case HeraldFieldConfig::FIELD_DIFF_FILE: return $this->loadAffectedPaths(); case HeraldFieldConfig::FIELD_CC: if (isset($this->explicitCCs)) { return array_keys($this->explicitCCs); } else { return $this->revision->getCCPHIDs(); } case HeraldFieldConfig::FIELD_REVIEWERS: if (isset($this->explicitReviewers)) { return array_keys($this->explicitReviewers); } else { return $this->revision->getReviewers(); } case HeraldFieldConfig::FIELD_REPOSITORY: $repository = $this->loadRepository(); if (!$repository) { return null; } return $repository->getPHID(); case HeraldFieldConfig::FIELD_DIFF_CONTENT: return $this->loadContentDictionary(); case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE: $packages = $this->loadAffectedPackages(); return mpull($packages, 'getPHID'); case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE_OWNER: $packages = $this->loadAffectedPackages(); $owners = PhabricatorOwnersOwner::loadAllForPackages($packages); return mpull($owners, 'getUserPHID'); default: throw new Exception("Invalid field '{$field}'."); } } public function applyHeraldEffects(array $effects) { $result = array(); if ($this->explicitCCs) { $effect = new HeraldEffect(); $effect->setAction(HeraldActionConfig::ACTION_ADD_CC); $effect->setTarget(array_keys($this->explicitCCs)); $effect->setReason( 'CCs provided explicitly by revision author or carried over from a '. 'previous version of the revision.'); $result[] = new HeraldApplyTranscript( $effect, true, 'Added addresses to CC list.'); } $forbidden_ccs = array_fill_keys( nonempty($this->forbiddenCCs, array()), true); foreach ($effects as $effect) { $action = $effect->getAction(); switch ($action) { case HeraldActionConfig::ACTION_NOTHING: $result[] = new HeraldApplyTranscript( $effect, true, 'OK, did nothing.'); break; case HeraldActionConfig::ACTION_ADD_CC: $base_target = $effect->getTarget(); $forbidden = array(); foreach ($base_target as $key => $fbid) { if (isset($forbidden_ccs[$fbid])) { $forbidden[] = $fbid; unset($base_target[$key]); } else { $this->newCCs[$fbid] = true; } } if ($forbidden) { $failed = clone $effect; $failed->setTarget($forbidden); if ($base_target) { $effect->setTarget($base_target); $result[] = new HeraldApplyTranscript( $effect, true, 'Added these addresses to CC list. Others could not be added.'); } $result[] = new HeraldApplyTranscript( $failed, false, 'CC forbidden, these addresses have unsubscribed.'); } else { $result[] = new HeraldApplyTranscript( $effect, true, 'Added addresses to CC list.'); } break; case HeraldActionConfig::ACTION_REMOVE_CC: foreach ($effect->getTarget() as $fbid) { $this->remCCs[$fbid] = true; } $result[] = new HeraldApplyTranscript( $effect, true, 'Removed addresses from CC list.'); break; default: throw new Exception("No rules to handle action '{$action}'."); } } return $result; } }