diff --git a/src/applications/differential/editor/DifferentialRevisionEditor.php b/src/applications/differential/editor/DifferentialRevisionEditor.php index e128699645..5c156d04be 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditor.php +++ b/src/applications/differential/editor/DifferentialRevisionEditor.php @@ -1,950 +1,910 @@ aphrontRequestForEventDispatch = $request; return $this; } public function getAphrontRequestForEventDispatch() { return $this->aphrontRequestForEventDispatch; } public function __construct(DifferentialRevision $revision) { $this->revision = $revision; $this->isCreate = !($revision->getID()); } public static function newRevisionFromConduitWithDiff( array $fields, DifferentialDiff $diff, PhabricatorUser $actor) { $revision = DifferentialRevision::initializeNewRevision($actor); $revision->setPHID($revision->generatePHID()); $editor = new DifferentialRevisionEditor($revision); $editor->setActor($actor); $editor->addDiff($diff, null); $editor->copyFieldsFromConduit($fields); $editor->save(); return $revision; } public function copyFieldsFromConduit(array $fields) { $actor = $this->getActor(); $revision = $this->revision; $revision->loadRelationships(); $all_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); $aux_fields = array(); foreach ($all_fields as $aux_field) { $aux_field->setRevision($revision); $aux_field->setDiff($this->diff); $aux_field->setUser($actor); if ($aux_field->shouldAppearOnCommitMessage()) { $aux_fields[$aux_field->getCommitMessageKey()] = $aux_field; } } foreach ($fields as $field => $value) { if (empty($aux_fields[$field])) { throw new Exception( "Parsed commit message contains unrecognized field '{$field}'."); } $aux_fields[$field]->setValueFromParsedCommitMessage($value); } foreach ($aux_fields as $aux_field) { $aux_field->validateField(); } $this->setAuxiliaryFields($all_fields); } public function setAuxiliaryFields(array $auxiliary_fields) { assert_instances_of($auxiliary_fields, 'DifferentialFieldSpecification'); $this->auxiliaryFields = $auxiliary_fields; return $this; } 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 setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; 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; $repository = id(new DifferentialRepositoryLookup()) ->setViewer($this->getActor()) ->setDiff($diff) ->lookupRepository(); if ($repository) { $this->getRevision()->setRepositoryPHID($repository->getPHID()); } return $this; } protected function getDiff() { return $this->diff; } protected function getComments() { return $this->comments; } protected function getActorPHID() { return $this->getActor()->getPHID(); } public function isNewRevision() { return !$this->getRevision()->getID(); } public function save() { $revision = $this->getRevision(); $is_new = $this->isNewRevision(); $revision->loadRelationships(); - $this->willWriteRevision(); - if ($this->reviewers === null) { $this->reviewers = $revision->getReviewers(); } if ($this->cc === null) { $this->cc = $revision->getCCPHIDs(); } if ($is_new) { $content_blocks = array(); foreach ($this->auxiliaryFields as $field) { if ($field->shouldExtractMentions()) { $content_blocks[] = $field->renderValueForCommitMessage(false); } } $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( $content_blocks); $this->cc = array_unique(array_merge($this->cc, $phids)); } $diff = $this->getDiff(); if ($diff) { $revision->setLineCount($diff->getLineCount()); } // Save the revision, to generate its ID and PHID if it is new. We need // the ID/PHID in order to record them in Herald transcripts, but don't // want to hold a transaction open while running Herald because it is // potentially somewhat slow. The downside is that we may end up with a // saved revision/diff pair without appropriate CCs. We could be better // about this -- for example: // // - Herald can't affect reviewers, so we could compute them before // opening the transaction and then save them in the transaction. // - Herald doesn't *really* need PHIDs to compute its effects, we could // run it before saving these objects and then hand over the PHIDs later. // // But this should address the problem of orphaned revisions, which is // currently the only problem we experience in practice. $revision->openTransaction(); if ($diff) { $revision->setBranchName($diff->getBranch()); $revision->setArcanistProjectPHID($diff->getArcanistProjectPHID()); } $revision->save(); if ($diff) { $diff->setRevisionID($revision->getID()); $diff->save(); } $revision->saveTransaction(); // 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), ); $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(); $xscript_phid = null; if ($diff) { $unsubscribed_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $revision, $diff); $adapter->setExplicitCCs($new['ccs']); $adapter->setExplicitReviewers($new['rev']); $adapter->setForbiddenCCs($unsubscribed_phids); $adapter->setIsNewObject($is_new); $xscript = HeraldEngine::loadAndApplyRules($adapter); $xscript_uri = '/herald/transcript/'.$xscript->getID().'/'; $xscript_phid = $xscript->getPHID(); $xscript_header = $xscript->getXHeraldRulesHeader(); $xscript_header = HeraldTranscript::saveXHeraldRulesHeader( $revision->getPHID(), $xscript_header); $sub = array( 'rev' => $adapter->getReviewersAddedByHerald(), 'ccs' => $adapter->getCCsAddedByHerald(), ); $rem_ccs = $adapter->getCCsRemovedByHerald(); $blocking_reviewers = array_keys( $adapter->getBlockingReviewersAddedByHerald()); HarbormasterBuildable::applyBuildPlans( $diff->getPHID(), $revision->getPHID(), $adapter->getBuildPlans()); } else { $sub = array( 'rev' => array(), 'ccs' => array(), ); $blocking_reviewers = 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]); } // Prevent Herald rules from adding a revision's owner as a reviewer. unset($add['rev'][$revision->getAuthorPHID()]); self::updateReviewers( $revision, $this->getActor(), array_keys($add['rev']), array_keys($rem['rev']), $blocking_reviewers); // We want to attribute new CCs to a "reasonPHID", representing the reason // they were added. This is either a user (if some user explicitly CCs // them, or uses "Add CCs...") or a Herald transcript PHID, indicating that // they were added by a Herald rule. if ($add['ccs'] || $rem['ccs']) { $reasons = array(); foreach ($add['ccs'] as $phid => $ignored) { if (empty($new['ccs'][$phid])) { $reasons[$phid] = $xscript_phid; } else { $reasons[$phid] = $this->getActorPHID(); } } foreach ($rem['ccs'] as $phid => $ignored) { if (empty($new['ccs'][$phid])) { $reasons[$phid] = $this->getActorPHID(); } else { $reasons[$phid] = $xscript_phid; } } } else { $reasons = $this->getActorPHID(); } self::alterCCs( $revision, $this->cc, array_keys($rem['ccs']), array_keys($add['ccs']), $reasons); $this->updateAuxiliaryFields(); // Add the author and users included from Herald rules 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; if ($diff) { $add['rev'] += $adapter->getEmailPHIDsAddedByHerald(); } } else { $stable['rev'][$this->getActorPHID()] = true; if ($diff) { $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald(); } } } $mail = array(); $phids = array($this->getActorPHID()); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); $actor_handle = $handles[$this->getActorPHID()]; $changesets = null; $old_status = $revision->getStatus(); if ($diff) { $changesets = $diff->loadChangesets(); // TODO: This should probably be in DifferentialFeedbackEditor? if (!$is_new) { $this->createComment(); $mail[] = id(new DifferentialNewDiffMail( $revision, $actor_handle, $changesets)) ->setActor($this->getActor()) ->setIsFirstMailAboutRevision(false) ->setIsFirstMailToRecipients(false) ->setCommentText($this->getComments()) ->setToPHIDs(array_keys($stable['rev'])) ->setCCPHIDs(array_keys($stable['ccs'])); } // Save the changes we made above. $diff->setDescription(preg_replace('/\n.*/s', '', $this->getComments())); $diff->save(); $this->updateAffectedPathTable($revision, $diff, $changesets); $this->updateRevisionHashTable($revision, $diff); // An updated diff should require review, as long as it's not closed // or accepted. The "accepted" status is "sticky" to encourage courtesy // re-diffs after someone accepts with minor changes/suggestions. $status = $revision->getStatus(); if ($status != ArcanistDifferentialRevisionStatus::CLOSED && $status != ArcanistDifferentialRevisionStatus::ACCEPTED) { $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } } else { $diff = $revision->loadActiveDiff(); if ($diff) { $changesets = $diff->loadChangesets(); } else { $changesets = array(); } } $revision->save(); // If the actor just deleted all the blocking/rejected reviewers, we may // be able to put the revision into "accepted". switch ($revision->getStatus()) { case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $revision = self::updateAcceptedStatus( $this->getActor(), $revision); break; } - $this->didWriteRevision(); - $event_data = array( 'revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $is_new ? DifferentialAction::ACTION_CREATE : DifferentialAction::ACTION_UPDATE, 'feedback_content' => $is_new ? phutil_utf8_shorten($revision->getSummary(), 140) : $this->getComments(), 'actor_phid' => $revision->getAuthorPHID(), ); $mailed_phids = array(); if (!$this->silentUpdate) { $revision->loadRelationships(); if ($add['rev']) { $message = id(new DifferentialNewDiffMail( $revision, $actor_handle, $changesets)) ->setActor($this->getActor()) ->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 we added CCs, we want to send them an email, but only if they were // not already a reviewer and were not added as one (in these cases, they // got a "NewDiff" mail, either in the past or just a moment ago). 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. $implied_ccs = self::getImpliedCCs($revision); $implied_ccs = array_fill_keys($implied_ccs, true); $add['ccs'] = array_diff_key($add['ccs'], $implied_ccs); if (!$is_new && $add['ccs']) { $mail[] = id(new DifferentialCCWelcomeMail( $revision, $actor_handle, $changesets)) ->setActor($this->getActor()) ->setIsFirstMailToRecipients(true) ->setToPHIDs(array_keys($add['ccs'])); } foreach ($mail as $message) { $message->setHeraldTranscriptURI($xscript_uri); $message->setXHeraldRulesHeader($xscript_header); $message->send(); $mailed_phids[] = $message->getRawMail()->buildRecipientList(); } $mailed_phids = array_mergev($mailed_phids); } id(new PhabricatorFeedStoryPublisher()) ->setStoryType('PhabricatorFeedStoryDifferential') ->setStoryData($event_data) ->setStoryTime(time()) ->setStoryAuthorPHID($revision->getAuthorPHID()) ->setRelatedPHIDs( array( $revision->getPHID(), $revision->getAuthorPHID(), )) ->setPrimaryObjectPHID($revision->getPHID()) ->setSubscribedPHIDs( array_merge( array($revision->getAuthorPHID()), $revision->getReviewers(), $revision->getCCPHIDs())) ->setMailRecipientPHIDs($mailed_phids) ->publish(); id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing($revision->getPHID()); } protected static function alterCCs( DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid) { $dont_add = self::getImpliedCCs($revision); $add_phids = array_diff($add_phids, $dont_add); id(new PhabricatorSubscriptionsEditor()) ->setActor(PhabricatorUser::getOmnipotentUser()) ->setObject($revision) ->subscribeExplicit($add_phids) ->unsubscribe($rem_phids) ->save(); } private static function getImpliedCCs(DifferentialRevision $revision) { return array_merge( $revision->getReviewers(), array($revision->getAuthorPHID())); } public static function updateReviewers( DifferentialRevision $revision, PhabricatorUser $actor, array $add_phids, array $remove_phids, array $blocking_phids = array()) { $reviewers = $revision->getReviewers(); $editor = id(new PhabricatorEdgeEditor()) ->setActor($actor); $reviewer_phids_map = array_fill_keys($reviewers, true); $blocking_phids = array_fuse($blocking_phids); foreach ($add_phids as $phid) { // Adding an already existing edge again would have cause memory loss // That is, the previous state for that reviewer would be lost if (isset($reviewer_phids_map[$phid])) { // TODO: If we're writing a blocking edge, we should overwrite an // existing weaker edge (like "added" or "commented"), just not a // stronger existing edge. continue; } if (isset($blocking_phids[$phid])) { $status = DifferentialReviewerStatus::STATUS_BLOCKING; } else { $status = DifferentialReviewerStatus::STATUS_ADDED; } $options = array( 'data' => array( 'status' => $status, ) ); $editor->addEdge( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $phid, $options); } foreach ($remove_phids as $phid) { $editor->removeEdge( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $phid); } $editor->save(); } private function createComment() { $template = id(new DifferentialComment()) ->setAuthorPHID($this->getActorPHID()) ->setRevision($this->revision); if ($this->contentSource) { $content_source = $this->contentSource; } else { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_LEGACY, array()); } $template->setContentSource($content_source); // Write the "update active diff" transaction. id(clone $template) ->setAction(DifferentialAction::ACTION_UPDATE) ->setMetadata( array( DifferentialComment::METADATA_DIFF_ID => $this->getDiff()->getID(), )) ->save(); // If we have a comment, write the "add a comment" transaction. if (strlen($this->getComments())) { id(clone $template) ->setAction(DifferentialAction::ACTION_COMMENT) ->setContent($this->getComments()) ->save(); } } private function updateAuxiliaryFields() { $aux_map = array(); foreach ($this->auxiliaryFields as $aux_field) { $key = $aux_field->getStorageKey(); if ($key !== null) { $val = $aux_field->getValueForStorage(); $aux_map[$key] = $val; } } if (!$aux_map) { return; } $revision = $this->revision; $fields = id(new DifferentialCustomFieldStorage())->loadAllWhere( 'objectPHID = %s', $revision->getPHID()); $fields = mpull($fields, null, 'getFieldIndex'); foreach ($aux_map as $key => $val) { $index = PhabricatorHash::digestForIndex($key); $obj = idx($fields, $index); if (!strlen($val)) { // If the new value is empty, just delete the old row if one exists and // don't add a new row if it doesn't. if ($obj) { $obj->delete(); } } else { if (!$obj) { $obj = new DifferentialCustomFieldStorage(); $obj->setObjectPHID($revision->getPHID()); $obj->setFieldIndex($index); } if ($obj->getFieldValue() !== $val) { $obj->setFieldValue($val); $obj->save(); } } } } - private function willWriteRevision() { - foreach ($this->auxiliaryFields as $aux_field) { - $aux_field->willWriteRevision($this); - } - - $this->dispatchEvent( - PhabricatorEventType::TYPE_DIFFERENTIAL_WILLEDITREVISION); - } - - private function didWriteRevision() { - foreach ($this->auxiliaryFields as $aux_field) { - $aux_field->didWriteRevision($this); - } - - $this->dispatchEvent( - PhabricatorEventType::TYPE_DIFFERENTIAL_DIDEDITREVISION); - } - - private function dispatchEvent($type) { - $event = new PhabricatorEvent( - $type, - array( - 'revision' => $this->revision, - 'new' => $this->isCreate, - )); - - $event->setUser($this->getActor()); - - $request = $this->getAphrontRequestForEventDispatch(); - if ($request) { - $event->setAphrontRequest($request); - } - - PhutilEventEngine::dispatchEvent($event); - } - /** * 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, array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $project = $diff->loadArcanistProject(); if (!$project) { // Probably an old revision from before projects. return; } $repository = $project->loadRepository(); if (!$repository) { // Probably no project <-> repository link, or the repository where the // project 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)); } } $paths = array(); foreach ($changesets as $changeset) { $paths[] = $path_prefix.'/'.$changeset->getFilename(); } // 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)); } } /** * Try to move a revision to "accepted". We look for: * * - at least one accepting reviewer who is a user; and * - no rejects; and * - no blocking reviewers. */ public static function updateAcceptedStatus( PhabricatorUser $viewer, DifferentialRevision $revision) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($revision->getID())) ->needRelationships(true) ->needReviewerStatus(true) ->needReviewerAuthority(true) ->executeOne(); $has_user_accept = false; foreach ($revision->getReviewerStatus() as $reviewer) { $status = $reviewer->getStatus(); if ($status == DifferentialReviewerStatus::STATUS_BLOCKING) { // We have a blocking reviewer, so just leave the revision in its // existing state. return $revision; } if ($status == DifferentialReviewerStatus::STATUS_REJECTED) { // We have a rejecting reviewer, so leave the revisoin as is. return $revision; } if ($reviewer->isUser()) { if ($status == DifferentialReviewerStatus::STATUS_ACCEPTED) { $has_user_accept = true; } } } if ($has_user_accept) { $revision ->setStatus(ArcanistDifferentialRevisionStatus::ACCEPTED) ->save(); } return $revision; } } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index cefe319ff9..0e094255db 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1,1269 +1,1303 @@ 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 getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = DifferentialTransaction::TYPE_ACTION; $types[] = DifferentialTransaction::TYPE_INLINE; $types[] = DifferentialTransaction::TYPE_STATUS; $types[] = DifferentialTransaction::TYPE_UPDATE; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: return $object->getViewPolicy(); case PhabricatorTransactions::TYPE_EDIT_POLICY: return $object->getEditPolicy(); case DifferentialTransaction::TYPE_ACTION: return null; case DifferentialTransaction::TYPE_INLINE: return null; 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 PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case DifferentialTransaction::TYPE_ACTION: case DifferentialTransaction::TYPE_UPDATE: return $xaction->getNewValue(); case DifferentialTransaction::TYPE_INLINE: return null; } return parent::getCustomTransactionNewValue($object, $xaction); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return $xaction->hasComment(); case DifferentialTransaction::TYPE_ACTION: $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; $action_type = $xaction->getNewValue(); switch ($action_type) { case DifferentialAction::ACTION_ACCEPT: case DifferentialAction::ACTION_REJECT: if ($action_type == DifferentialAction::ACTION_ACCEPT) { $new_status = DifferentialReviewerStatus::STATUS_ACCEPTED; } else { $new_status = DifferentialReviewerStatus::STATUS_REJECTED; } $actor = $this->getActor(); $actor_phid = $actor->getPHID(); // These transactions can cause effects in two ways: by altering the // status of an existing reviewer; or by adding the actor as a new // reviewer. $will_add_reviewer = true; foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->hasAuthority($actor)) { if ($reviewer->getStatus() != $new_status) { return true; } } if ($reviewer->getReviewerPHID() == $actor_phid) { $will_add_reviwer = false; } } return $will_add_reviewer; case DifferentialAction::ACTION_CLOSE: return ($object->getStatus() != $status_closed); case DifferentialAction::ACTION_ABANDON: return ($object->getStatus() != $status_abandoned); case DifferentialAction::ACTION_RECLAIM: return ($object->getStatus() == $status_abandoned); case DifferentialAction::ACTION_REOPEN: return ($object->getStatus() == $status_closed); case DifferentialAction::ACTION_RETHINK: return ($object->getStatus() != $status_plan); case DifferentialAction::ACTION_REQUEST: return ($object->getStatus() != $status_review); case DifferentialAction::ACTION_RESIGN: $actor_phid = $this->getActor()->getPHID(); foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->getReviewerPHID() == $actor_phid) { return true; } } return false; case DifferentialAction::ACTION_CLAIM: $actor_phid = $this->getActor()->getPHID(); return ($actor_phid != $object->getAuthorPHID()); } } return parent::transactionHasEffect($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: $object->setViewPolicy($xaction->getNewValue()); return; case PhabricatorTransactions::TYPE_EDIT_POLICY: $object->setEditPolicy($xaction->getNewValue()); return; case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_COMMENT: case DifferentialTransaction::TYPE_INLINE: return; case PhabricatorTransactions::TYPE_EDGE: return; case DifferentialTransaction::TYPE_UPDATE: if (!$this->getIsCloseByCommit()) { $object->setStatus($status_review); } // TODO: Update the `diffPHID` once we add that. return; case DifferentialTransaction::TYPE_ACTION: switch ($xaction->getNewValue()) { case DifferentialAction::ACTION_RESIGN: case DifferentialAction::ACTION_ACCEPT: case DifferentialAction::ACTION_REJECT: // These have no direct effects, and affect review status only // indirectly by altering reviewers with TYPE_EDGE transactions. return; case DifferentialAction::ACTION_ABANDON: $object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED); return; case DifferentialAction::ACTION_RETHINK: $object->setStatus($status_plan); return; case DifferentialAction::ACTION_RECLAIM: $object->setStatus($status_review); return; case DifferentialAction::ACTION_REOPEN: $object->setStatus($status_review); return; case DifferentialAction::ACTION_REQUEST: $object->setStatus($status_review); return; case DifferentialAction::ACTION_CLOSE: $object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED); return; case DifferentialAction::ACTION_CLAIM: $object->setAuthorPHID($this->getActor()->getPHID()); return; } break; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $actor = $this->getActor(); $actor_phid = $actor->getPHID(); $type_edge = PhabricatorTransactions::TYPE_EDGE; + $edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; + $edge_ref_task = PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK; $results = parent::expandTransaction($object, $xaction); 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; } $new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED; $new_reject = DifferentialReviewerStatus::STATUS_REJECTED; $old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER; $old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER; // When a revision is updated, change all "reject" to "rejected older // revision". This means we won't immediately push the update back into // "needs review", but outstanding rejects will still block it from // moving to "accepted". $edits = array(); foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->getStatus() == $new_reject) { $edits[$reviewer->getReviewerPHID()] = array( 'data' => array( 'status' => $old_reject, ), ); } // TODO: If sticky accept is off, do a similar update for accepts. } if ($edits) { $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => $edits)); } + + // 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 = 'PhabricatorApplicationManiphest'; + if (PhabricatorApplication::isClassInstalled($maniphest)) { + $diff = $this->loadDiff($xaction->getNewValue()); + if ($diff) { + $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 PhabricatorTransactions::TYPE_COMMENT: // When a user leaves a comment, upgrade their reviewer status from // "added" to "commented" if they're also a reviewer. We may further // upgrade this based on other actions in the transaction group. $status_added = DifferentialReviewerStatus::STATUS_ADDED; $status_commented = DifferentialReviewerStatus::STATUS_COMMENTED; $data = array( 'status' => $status_commented, ); $edits = array(); foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->getReviewerPHID() == $actor_phid) { if ($reviewer->getStatus() == $status_added) { $edits[$actor_phid] = array( 'data' => $data, ); } } } if ($edits) { $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => $edits)); } break; case DifferentialTransaction::TYPE_ACTION: $action_type = $xaction->getNewValue(); switch ($action_type) { case DifferentialAction::ACTION_ACCEPT: case DifferentialAction::ACTION_REJECT: if ($action_type == DifferentialAction::ACTION_ACCEPT) { $data = array( 'status' => DifferentialReviewerStatus::STATUS_ACCEPTED, ); } else { $data = array( 'status' => DifferentialReviewerStatus::STATUS_REJECTED, ); } $edits = array(); foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->hasAuthority($actor)) { $edits[$reviewer->getReviewerPHID()] = array( 'data' => $data, ); } } // Also either update or add the actor themselves as a reviewer. $edits[$actor_phid] = array( 'data' => $data, ); $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => $edits)); break; case DifferentialAction::ACTION_CLAIM: // If the user is commandeering, add the previous owner as a // reviewer and remove the actor. $edits = array( '-' => array( $actor_phid => $actor_phid, ), ); $owner_phid = $object->getAuthorPHID(); if ($owner_phid) { $reviewer = new DifferentialReviewer( $owner_phid, array( 'status' => DifferentialReviewerStatus::STATUS_ADDED, )); $edits['+'] = array( $owner_phid => array( 'data' => $reviewer->getEdgeData(), ), ); } $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue($edits); break; case DifferentialAction::ACTION_RESIGN: // If the user is resigning, add a separate reviewer edit // transaction which removes them as a reviewer. $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue( array( '-' => array( $actor_phid => $actor_phid, ), )); break; } break; } return $results; } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: return; case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_COMMENT: case DifferentialTransaction::TYPE_ACTION: case DifferentialTransaction::TYPE_INLINE: return; case DifferentialTransaction::TYPE_UPDATE: // Now that we're inside the transaction, do a final check. $diff = $this->loadDiff($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. if (!$diff) { throw new Exception(pht('Diff does not exist!')); } else { $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?')); } } $diff->setRevisionID($object->getID()); $diff->save(); $object->setLineCount($diff->getLineCount()); $object->setRepositoryPHID($diff->getRepositoryPHID()); return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function mergeEdgeData($type, array $u, array $v) { $result = parent::mergeEdgeData($type, $u, $v); switch ($type) { case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: // When the same reviewer has their status updated by multiple // transactions, we want the strongest status to win. An example of // this is when a user adds a comment and also accepts a revision which // they are a reviewer on. The comment creates a "commented" status, // while the accept creates an "accepted" status. Since accept is // stronger, it should win and persist. $u_status = idx($u, 'status'); $v_status = idx($v, 'status'); $u_str = DifferentialReviewerStatus::getStatusStrength($u_status); $v_str = DifferentialReviewerStatus::getStatusStrength($v_status); if ($u_str > $v_str) { $result['status'] = $u_status; } else { $result['status'] = $v_status; } break; } return $result; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; $old_status = $object->getStatus(); switch ($old_status) { case $status_accepted: case $status_revision: case $status_review: // Load the most up-to-date version of the revision and its reviewers, // so we don't need to try to deduce the state of reviewers by examining // all the changes made by the transactions. $new_revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewerStatus(true) ->withIDs(array($object->getID())) ->executeOne(); if (!$new_revision) { throw new Exception( pht('Failed to load revision from transaction finalization.')); } // Try to move a revision to "accepted". We look for: // // - at least one accepting reviewer who is a user; and // - no rejects; and // - no 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; foreach ($new_revision->getReviewerStatus() as $reviewer) { $reviewer_status = $reviewer->getStatus(); switch ($reviewer_status) { case DifferentialReviewerStatus::STATUS_REJECTED: $has_rejecting_reviewer = true; break; case DifferentialReviewerStatus::STATUS_REJECTED_OLDER: $has_rejecting_older_reviewer = true; break; case DifferentialReviewerStatus::STATUS_BLOCKING: $has_blocking_reviewer = true; break; case DifferentialReviewerStatus::STATUS_ACCEPTED: if ($reviewer->isUser()) { $has_accepting_user = true; } break; } } $new_status = null; if ($has_accepting_user && !$has_rejecting_reviewer && !$has_rejecting_older_reviewer && !$has_blocking_reviewer) { $new_status = $status_accepted; } else if ($has_rejecting_reviewer) { // This isn't accepted, and there's at least one rejecting reviewer, // so the revision needs changes. This usually happens after a // "reject". $new_status = $status_revision; } else if ($old_status == $status_accepted) { // This revision was accepted, but it no longer satisfies the // conditions for acceptance. This usually happens after an accepting // reviewer resigns or is removed. $new_status = $status_review; } if ($new_status !== null && $new_status != $old_status) { $xaction = id(new DifferentialTransaction()) ->setTransactionType(DifferentialTransaction::TYPE_STATUS) ->setOldValue($old_status) ->setNewValue($new_status); $xaction = $this->populateTransaction($object, $xaction)->save(); $xactions[] = $xaction; $object->setStatus($new_status)->save(); } break; default: // Revisions can't transition out of other statuses (like closed or // abandoned) as a side effect of reviewer status changes. break; } return $xactions; } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); foreach ($xactions as $xaction) { switch ($type) { case DifferentialTransaction::TYPE_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; case DifferentialTransaction::TYPE_ACTION: $error = $this->validateDifferentialAction( $object, $type, $xaction, $xaction->getNewValue()); if ($error) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), $error, $xaction); } break; } } return $errors; } private function validateDifferentialAction( DifferentialRevision $revision, $type, DifferentialTransaction $xaction, $action) { $author_phid = $revision->getAuthorPHID(); $actor_phid = $this->getActor()->getPHID(); $actor_is_author = ($author_phid == $actor_phid); $config_close_key = 'differential.always-allow-close'; $always_allow_close = PhabricatorEnv::getEnvConfig($config_close_key); $config_reopen_key = 'differential.allow-reopen'; $allow_reopen = PhabricatorEnv::getEnvConfig($config_reopen_key); $config_self_accept_key = 'differential.allow-self-accept'; $allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key); $revision_status = $revision->getStatus(); $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; switch ($action) { case DifferentialAction::ACTION_ACCEPT: if ($actor_is_author && !$allow_self_accept) { return pht( 'You can not accept this revision because you are the owner.'); } if ($revision_status == $status_abandoned) { return pht( 'You can not accept this revision because it has been '. 'abandoned.'); } if ($revision_status == $status_closed) { return pht( 'You can not accept this revision because it has already been '. 'closed.'); } break; case DifferentialAction::ACTION_REJECT: if ($actor_is_author) { return pht( 'You can not request changes to your own revision.'); } if ($revision_status == $status_abandoned) { return pht( 'You can not request changes to this revision because it has been '. 'abandoned.'); } if ($revision_status == $status_closed) { return pht( 'You can not request changes to this revision because it has '. 'already been closed.'); } break; case DifferentialAction::ACTION_RESIGN: // You can always resign from a revision if you're a reviewer. If you // aren't, this is a no-op rather than invalid. break; case DifferentialAction::ACTION_CLAIM: // You can claim a revision if you're not the owner. If you are, this // is a no-op rather than invalid. if ($revision_status == $status_closed) { return pht( "You can not commandeer this revision because it has already been ". "closed."); } break; case DifferentialAction::ACTION_ABANDON: if (!$actor_is_author) { return pht( "You can not abandon this revision because you do not own it. ". "You can only abandon revisions you own."); } if ($revision_status == $status_closed) { return pht( "You can not abandon this revision because it has already been ". "closed."); } // NOTE: Abandons of already-abandoned revisions are treated as no-op // instead of invalid. Other abandons are OK. break; case DifferentialAction::ACTION_RECLAIM: if (!$actor_is_author) { return pht( "You can not reclaim this revision because you do not own ". "it. You can only reclaim revisions you own."); } if ($revision_status == $status_closed) { return pht( "You can not reclaim this revision because it has already been ". "closed."); } // NOTE: Reclaims of other non-abandoned revisions are treated as no-op // instead of invalid. break; case DifferentialAction::ACTION_REOPEN: if (!$allow_reopen) { return pht( 'The reopen action is not enabled on this Phabricator install. '. 'Adjust your configuration to enable it.'); } // NOTE: If the revision is not closed, this is caught as a no-op // instead of an invalid transaction. break; case DifferentialAction::ACTION_RETHINK: if (!$actor_is_author) { return pht( "You can not plan changes to this revision because you do not ". "own it. To plan changes to a revision, you must be its owner."); } switch ($revision_status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: // These are OK. break; case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: // Let this through, it's a no-op. break; case ArcanistDifferentialRevisionStatus::ABANDONED: return pht( "You can not plan changes to this revision because it has ". "been abandoned."); case ArcanistDifferentialRevisionStatus::CLOSED: return pht( "You can not plan changes to this revision because it has ". "already been closed."); default: throw new Exception( pht( 'Encountered unexpected revision status ("%s") when '. 'validating "%s" action.', $revision_status, $action)); } break; case DifferentialAction::ACTION_REQUEST: if (!$actor_is_author) { return pht( "You can not request review of this revision because you do ". "not own it. To request review of a revision, you must be its ". "owner."); } switch ($revision_status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: // These are OK. break; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: // This will be caught as "no effect" later on. break; case ArcanistDifferentialRevisionStatus::ABANDONED: return pht( "You can not request review of this revision because it has ". "been abandoned. Instead, reclaim it."); case ArcanistDifferentialRevisionStatus::CLOSED: return pht( "You can not request review of this revision because it has ". "already been closed."); default: throw new Exception( pht( 'Encountered unexpected revision status ("%s") when '. 'validating "%s" action.', $revision_status, $action)); } break; case DifferentialAction::ACTION_CLOSE: // We force revisions closed when we discover a corresponding commit. // In this case, revisions are allowed to transition to closed from // any state. This is an automated action taken by the daemons. if (!$this->getIsCloseByCommit()) { if (!$actor_is_author && !$always_allow_close) { return pht( "You can not close this revision because you do not own it. To ". "close a revision, you must be its owner."); } if ($revision_status != $status_accepted) { return pht( "You can not close this revision because it has not been ". "accepted. You can only close accepted revisions."); } } break; } return null; } protected function sortTransactions(array $xactions) { $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 requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { } return parent::requireCapabilities($object, $xaction); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); $phids[] = $object->getAuthorPHID(); foreach ($object->getReviewerStatus() as $reviewer) { $phids[] = $reviewer->getReviewerPHID(); } return $phids; } protected function getMailCC(PhabricatorLiskDAO $object) { $phids = parent::getMailCC($object); if ($this->heraldEmailPHIDs) { foreach ($this->heraldEmailPHIDs as $phid) { $phids[] = $phid; } } return $phids; } protected function getMailAction( PhabricatorLiskDAO $object, array $xactions) { $action = parent::getMailAction($object, $xactions); $strongest = $this->getStrongestAction($object, $xactions); switch ($strongest->getTransactionType()) { case DifferentialTransaction::TYPE_UPDATE: $count = new PhutilNumber($object->getLineCount()); - $action = pht('%s, %s line(s)', $action, $count); + $action = pht('%s, %d line(s)', $action, $count); break; } 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(); $original_title = $object->getOriginalTitle(); $subject = "D{$id}: {$title}"; $thread_topic = "D{$id}: {$original_title}"; return id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->addHeader('Thread-Topic', $thread_topic); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $type_inline = DifferentialTransaction::TYPE_INLINE; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } } $changed_uri = $this->getChangedPriorToCommitURI(); if ($changed_uri) { $body->addTextSection( pht('CHANGED PRIOR TO COMMIT'), $changed_uri); } if ($inlines) { $body->addTextSection( pht('INLINE COMMENTS'), $this->renderInlineCommentsForMail($object, $inlines)); } $body->addTextSection( pht('REVISION DETAIL'), PhabricatorEnv::getProductionURI('/D'.$object->getID())); return $body; } protected function supportsSearch() { return true; } protected function extractFilePHIDsFromCustomTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { } return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, $blocks, PhutilMarkupEngine $engine) { $flat_blocks = array_mergev($blocks); $huge_block = implode("\n\n", $flat_blocks); $task_map = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($huge_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($huge_block); foreach ($rev_refs as $match) { foreach ($match['monograms'] as $monogram) { $rev_id = (int)trim($monogram, 'dD'); $rev_map[$rev_id] = true; } } $edges = array(); if ($task_map) { $tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withIDs(array_keys($task_map)) ->execute(); if ($tasks) { $edge_related = PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK; $edges[$edge_related] = mpull($tasks, 'getPHID', 'getPHID'); } } 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) { $edge_depends = PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV; $edges[$edge_depends] = $rev_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 renderInlineCommentsForMail( PhabricatorLiskDAO $object, array $inlines) { $context_key = 'metamta.differential.unified-comment-context'; $show_context = PhabricatorEnv::getEnvConfig($context_key); $changeset_ids = array(); foreach ($inlines as $inline) { $id = $inline->getComment()->getChangesetID(); $changeset_ids[$id] = $id; } // TODO: We should write a proper Query class for this eventually. $changesets = id(new DifferentialChangeset())->loadAllWhere( 'id IN (%Ld)', $changeset_ids); if ($show_context) { $hunk_parser = new DifferentialHunkParser(); foreach ($changesets as $changeset) { $changeset->attachHunks($changeset->loadHunks()); } } $inline_groups = DifferentialTransactionComment::sortAndGroupInlines( $inlines, $changesets); $result = array(); foreach ($inline_groups as $changeset_id => $group) { $changeset = idx($changesets, $changeset_id); if (!$changeset) { continue; } foreach ($group as $inline) { $comment = $inline->getComment(); $file = $changeset->getFilename(); $start = $comment->getLineNumber(); $len = $comment->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $inline_content = $comment->getContent(); if (!$show_context) { $result[] = "{$file}:{$range} {$inline_content}"; } else { $result[] = "================"; $result[] = "Comment at: " . $file . ":" . $range; $result[] = $hunk_parser->makeContextDiff( $changeset->getHunks(), $comment->getIsNewFile(), $comment->getLineNumber(), $comment->getLineLength(), 1); $result[] = "----------------"; $result[] = $inline_content; $result[] = null; } } } return implode("\n", $result); } private function loadDiff($phid) { return id(new DifferentialDiffQuery()) ->withPHIDs(array($phid)) ->setViewer($this->getActor()) ->executeOne(); } /* -( Herald Integration )------------------------------------------------- */ protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { if ($this->getIsNewObject()) { return true; } foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_UPDATE: if (!$this->getIsCloseByCommit()) { return true; } } } return parent::shouldApplyHeraldRules($object, $xactions); } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { $unsubscribed_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); $subscribed_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); $revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($object->getPHID())) ->needActiveDiffs(true) ->needReviewerStatus(true) ->executeOne(); if (!$revision) { throw new Exception( pht( 'Failed to load revision for Herald adapter construction!')); } $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $revision, $revision->getActiveDiff()); $reviewers = $revision->getReviewerStatus(); $reviewer_phids = mpull($reviewers, 'getReviewerPHID'); $adapter->setExplicitCCs($subscribed_phids); $adapter->setExplicitReviewers($reviewer_phids); $adapter->setForbiddenCCs($unsubscribed_phids); $adapter->setIsNewObject($this->getIsNewObject()); return $adapter; } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $xactions = array(); // Build a transaction to adjust CCs. $ccs = array( '+' => array_keys($adapter->getCCsAddedByHerald()), '-' => array_keys($adapter->getCCsRemovedByHerald()), ); $value = array(); foreach ($ccs as $type => $phids) { foreach ($phids as $phid) { $value[$type][$phid] = $phid; } } if ($value) { $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue($value); } // Build a transaction to adjust reviewers. $reviewers = array( DifferentialReviewerStatus::STATUS_ADDED => array_keys($adapter->getReviewersAddedByHerald()), DifferentialReviewerStatus::STATUS_BLOCKING => array_keys($adapter->getBlockingReviewersAddedByHerald()), ); $value = array(); foreach ($reviewers as $status => $phids) { foreach ($phids as $phid) { $value['+'][$phid] = array( 'data' => array( 'status' => $status, ), ); } } if ($value) { $edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_reviewer) ->setNewValue($value); } // Save extra email PHIDs for later. $this->heraldEmailPHIDs = $adapter->getEmailPHIDsAddedByHerald(); // Apply build plans. HarbormasterBuildable::applyBuildPlans( $adapter->getDiff(), $adapter->getPHID(), $adapter->getBuildPlans()); return $xactions; } } diff --git a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php index d5fbcf2b49..af427fe34c 100644 --- a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php @@ -1,78 +1,52 @@ getBranch(); $bookmark = $diff->getBookmark(); $has_branch = ($branch != ''); $has_bookmark = ($bookmark != ''); if ($has_branch && $has_bookmark) { return "{$bookmark} bookmark on {$branch} branch"; } else if ($has_bookmark) { return "{$bookmark} bookmark"; } else if ($has_branch) { return $branch; } return null; } public function renderValueForRevisionView() { $diff = $this->getManualDiff(); return $this->getBranchOrBookmarkDescription($diff); } public function renderValueForMail($phase) { $diff = $this->getRevision()->loadActiveDiff(); if ($diff) { $description = $this->getBranchOrBookmarkDescription($diff); if ($description) { return "BRANCH\n {$description}"; } } return null; } - public function didWriteRevision(DifferentialRevisionEditor $editor) { - $maniphest = 'PhabricatorApplicationManiphest'; - if (!PhabricatorApplication::isClassInstalled($maniphest)) { - return; - } - - $branch = $this->getDiff()->getBranch(); - $match = null; - if (preg_match('/^T(\d+)/i', $branch, $match)) { // No $ to allow T123_demo. - list(, $task_id) = $match; - $task = id(new ManiphestTaskQuery()) - ->setViewer($editor->requireActor()) - ->withIDs(array($task_id)) - ->executeOne(); - if ($task) { - id(new PhabricatorEdgeEditor()) - ->setActor($this->getUser()) - ->addEdge( - $this->getRevision()->getPHID(), - PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK, - $task->getPHID()) - ->save(); - } - } - } - public function getCommitMessageTips() { return array( 'Name branch "T123" to attach the diff to a task.', ); } } diff --git a/src/applications/differential/field/specification/DifferentialCCsFieldSpecification.php b/src/applications/differential/field/specification/DifferentialCCsFieldSpecification.php index 92c3106345..ed2e7e9a2f 100644 --- a/src/applications/differential/field/specification/DifferentialCCsFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialCCsFieldSpecification.php @@ -1,110 +1,106 @@ getCCPHIDs(); } public function renderLabelForRevisionView() { return 'CCs:'; } public function renderValueForRevisionView() { return $this->renderUserList($this->getCCPHIDs()); } private function getCCPHIDs() { $revision = $this->getRevision(); return $revision->getCCPHIDs(); } public function shouldAppearOnEdit() { return true; } protected function didSetRevision() { $this->ccs = $this->getCCPHIDs(); } public function getRequiredHandlePHIDsForRevisionEdit() { return $this->ccs; } public function getRequiredHandlePHIDsForCommitMessage() { return $this->ccs; } public function setValueFromRequest(AphrontRequest $request) { $this->ccs = $request->getArr('cc'); return $this; } public function renderEditControl() { $cc_map = array(); foreach ($this->ccs as $phid) { $cc_map[] = $this->getHandle($phid); } return id(new AphrontFormTokenizerControl()) ->setLabel('CC') ->setName('cc') ->setUser($this->getUser()) ->setDatasource('/typeahead/common/mailable/') ->setValue($cc_map); } - public function willWriteRevision(DifferentialRevisionEditor $editor) { - $editor->setCCPHIDs($this->ccs); - } - public function shouldAppearOnCommitMessage() { return true; } public function getCommitMessageKey() { return 'ccPHIDs'; } public function setValueFromParsedCommitMessage($value) { $this->ccs = array_unique(nonempty($value, array())); return $this; } public function renderLabelForCommitMessage() { return 'CC'; } public function renderValueForCommitMessage($is_edit) { if (!$this->ccs) { return null; } $names = array(); foreach ($this->ccs as $phid) { $handle = $this->getHandle($phid); if ($handle->isComplete()) { $names[] = $handle->getObjectName(); } } return implode(', ', $names); } public function getSupportedCommitMessageLabels() { return array( 'CC', 'CCs', ); } public function parseValueFromCommitMessage($value) { return $this->parseCommitMessageMailableList($value); } } diff --git a/src/applications/differential/field/specification/DifferentialEditPolicyFieldSpecification.php b/src/applications/differential/field/specification/DifferentialEditPolicyFieldSpecification.php index 62963e3d67..50cf1beecf 100644 --- a/src/applications/differential/field/specification/DifferentialEditPolicyFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialEditPolicyFieldSpecification.php @@ -1,42 +1,38 @@ value = $this->getRevision()->getEditPolicy(); } public function setValueFromRequest(AphrontRequest $request) { $this->value = $request->getStr('editPolicy'); return $this; } public function renderEditControl() { $viewer = $this->getUser(); $revision = $this->getRevision(); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($revision) ->execute(); return id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($revision) ->setPolicies($policies) ->setName('editPolicy'); } - public function willWriteRevision(DifferentialRevisionEditor $editor) { - $this->getRevision()->setEditPolicy($this->value); - } - } diff --git a/src/applications/differential/field/specification/DifferentialFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFieldSpecification.php index a7d45ab2f9..7ef49e7aed 100644 --- a/src/applications/differential/field/specification/DifferentialFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialFieldSpecification.php @@ -1,1004 +1,972 @@ value = $request->getStr('my-custom-field'); * * If you have some particularly complicated field, you may need to read * more data; this is why you have access to the entire request. * * You must implement this if you implement @{method:shouldAppearOnEdit}. * * You should not perform field validation here; instead, you should implement * @{method:validateField}. * * @param AphrontRequest HTTP request representing a user submitting a form * with this field in it. * @return this * @task edit */ public function setValueFromRequest(AphrontRequest $request) { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Build a renderable object (generally, some @{class:AphrontFormControl}) * which can be appended to a @{class:AphrontFormView} and represents the * interface the user sees on the "Edit Revision" screen when interacting * with this field. * * For example: * * return id(new AphrontFormTextControl()) * ->setLabel('Custom Field') * ->setName('my-custom-key') * ->setValue($this->value); * * You must implement this if you implement @{method:shouldAppearOnEdit}. * * @return AphrontView|string Something renderable. * @task edit */ public function renderEditControl() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Optionally, build a preview panel for the field which will appear on the * edit interface. This is used for the "Summary" field, but custom fields * generally need not implement it. * * @return AphrontView|string Something renderable. * @task edit */ public function renderEditPreview() { return null; } /** * This method will be called after @{method:setValueFromRequest} but before * the field is saved. It gives you an opportunity to inspect the field value * and throw a @{class:DifferentialFieldValidationException} if there is a * problem with the value the user has provided (for example, the value the * user entered is not correctly formatted). This method is also called after * @{method:setValueFromParsedCommitMessage} before the revision is saved. * * By default, fields are not validated. * * @return void * @task edit */ public function validateField() { return; } /** * Determine if user mentions should be extracted from the value and added to * CC when creating revision. Mentions are then extracted from the string * returned by @{method:renderValueForCommitMessage}. * * By default, mentions are not extracted. * * @return bool * @task edit */ public function shouldExtractMentions() { return false; } - /** - * Hook for applying revision changes via the editor. Normally, you should - * not implement this, but a number of builtin fields use the revision object - * itself as storage. If you need to do something similar for whatever reason, - * this method gives you an opportunity to interact with the editor or - * revision before changes are saved (for example, you can write the field's - * value into some property of the revision). - * - * @param DifferentialRevisionEditor Active editor which is applying changes - * to the revision. - * @return void - * @task edit - */ - public function willWriteRevision(DifferentialRevisionEditor $editor) { - return; - } - - /** - * Hook after an edit operation has completed. This allows you to update - * link tables or do other write operations which should happen after the - * revision is saved. Normally you don't need to implement this. - * - * - * @param DifferentialRevisionEditor Active editor which has just applied - * changes to the revision. - * @return void - * @task edit - */ - public function didWriteRevision(DifferentialRevisionEditor $editor) { - return; - } - /* -( Extending the Revision View Interface )------------------------------ */ /** * Determine if this field should appear on the revision detail view * interface. One use of this interface is to add purely informational * fields to the revision view, without any sort of backing storage. * * If you return true from this method, you must implement the methods * @{method:renderLabelForRevisionView} and * @{method:renderValueForRevisionView}. * * @return bool True if this field should appear when viewing a revision. * @task view */ public function shouldAppearOnRevisionView() { return false; } /** * Return a string field label which will appear in the revision detail * table. * * You must implement this method if you return true from * @{method:shouldAppearOnRevisionView}. * * @return string Label for field in revision detail view. * @task view */ public function renderLabelForRevisionView() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Return a markup block representing the field for the revision detail * view. Note that you can return null to suppress display (for instance, * if the field shows related objects of some type and the revision doesn't * have any related objects). * * You must implement this method if you return true from * @{method:shouldAppearOnRevisionView}. * * @return string|null Display markup for field value, or null to suppress * field rendering. * @task view */ public function renderValueForRevisionView() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Load users, their current statuses and return a markup with links to the * user profiles and information about their current status. * * @return string Display markup. * @task view */ public function renderUserList(array $user_phids) { if (!$user_phids) { return phutil_tag('em', array(), pht('None')); } return implode_selected_handle_links(', ', $this->getLoadedHandles(), $user_phids); } /** * Return a markup block representing a warning to display with the comment * box when preparing to accept a diff. A return value of null indicates no * warning box should be displayed for this field. * * @return string|null Display markup for warning box, or null for no warning */ public function renderWarningBoxForRevisionAccept() { return null; } /* -( Extending the Revision List Interface )------------------------------ */ /** * Determine if this field should appear in the table on the revision list * interface. * * @return bool True if this field should appear in the table. * * @task list */ public function shouldAppearOnRevisionList() { return false; } /** * Return a column header for revision list tables. * * @return string Column header. * * @task list */ public function renderHeaderForRevisionList() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Optionally, return a column class for revision list tables. * * @return string CSS class for table cells. * * @task list */ public function getColumnClassForRevisionList() { return null; } /** * Return a table cell value for revision list tables. * * @param DifferentialRevision The revision to render a value for. * @return string Table cell value. * * @task list */ public function renderValueForRevisionList(DifferentialRevision $revision) { throw new DifferentialFieldSpecificationIncompleteException($this); } /* -( Extending the Diff View Interface )------------------------------ */ /** * Determine if this field should appear on the diff detail view * interface. One use of this interface is to add purely informational * fields to the diff view, without any sort of backing storage. * * NOTE: These diffs are not necessarily attached yet to a revision. * As such, a field on the diff view can not rely on the existence of a * revision or use storage attached to the revision. * * If you return true from this method, you must implement the methods * @{method:renderLabelForDiffView} and * @{method:renderValueForDiffView}. * * @return bool True if this field should appear when viewing a diff. * @task view */ public function shouldAppearOnDiffView() { return false; } /** * Return a string field label which will appear in the diff detail * table. * * You must implement this method if you return true from * @{method:shouldAppearOnDiffView}. * * @return string Label for field in revision detail view. * @task view */ public function renderLabelForDiffView() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Return a markup block representing the field for the diff detail * view. Note that you can return null to suppress display (for instance, * if the field shows related objects of some type and the revision doesn't * have any related objects). * * You must implement this method if you return true from * @{method:shouldAppearOnDiffView}. * * @return string|null Display markup for field value, or null to suppress * field rendering. * @task view */ public function renderValueForDiffView() { throw new DifferentialFieldSpecificationIncompleteException($this); } /* -( Extending the E-mail Interface )------------------------------------- */ /** * Return plain text to render in e-mail messages. The text may span * multiple lines. * * @return int One of DifferentialMailPhase constants. * @return string|null Plain text, or null for no message. * * @task mail */ public function renderValueForMail($phase) { return null; } /* -( Extending the Conduit Interface )------------------------------------ */ /** * @task conduit */ public function shouldAppearOnConduitView() { return false; } /** * @task conduit */ public function getValueForConduit() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * @task conduit */ public function getKeyForConduit() { $key = $this->getStorageKey(); if ($key === null) { throw new DifferentialFieldSpecificationIncompleteException($this); } return $key; } /* -( Extending the Search Interface )------------------------------------ */ /** * @task search */ public function shouldAddToSearchIndex() { return false; } /** * @task search */ public function getValueForSearchIndex() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * NOTE: Keys *must be* 4 characters for * @{class:PhabricatorSearchEngineMySQL}. * * @task search */ public function getKeyForSearchIndex() { throw new DifferentialFieldSpecificationIncompleteException($this); } /* -( Extending Commit Messages )------------------------------------------ */ /** * Determine if this field should appear in commit messages. You should return * true if this field participates in any part of the commit message workflow, * even if it is not rendered by default. * * If you implement this method, you must implement * @{method:getCommitMessageKey} and * @{method:setValueFromParsedCommitMessage}. * * @return bool True if this field appears in commit messages in any capacity. * @task commit */ public function shouldAppearOnCommitMessage() { return false; } /** * Key which identifies this field in parsed commit messages. Commit messages * exist in two forms: raw textual commit messages and parsed dictionaries of * fields. This method must return a unique string which identifies this field * in dictionaries. Principally, this dictionary is shipped to and from arc * over Conduit. Keys should be appropriate property names, like "testPlan" * (not "Test Plan") and must be globally unique. * * You must implement this method if you return true from * @{method:shouldAppearOnCommitMessage}. * * @return string Key which identifies the field in dictionaries. * @task commit */ public function getCommitMessageKey() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Set this field's value from a value in a parsed commit message dictionary. * Afterward, this field will go through the normal write workflows and the * change will be permanently stored via either the storage mechanisms (if * your field implements them), revision write hooks (if your field implements * them) or discarded (if your field implements neither, e.g. is just a * display field). * * The value you receive will either be null or something you originally * returned from @{method:parseValueFromCommitMessage}. * * You must implement this method if you return true from * @{method:shouldAppearOnCommitMessage}. * * @param mixed Field value from a parsed commit message dictionary. * @return this * @task commit */ public function setValueFromParsedCommitMessage($value) { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * In revision control systems which read revision information from the * working copy, the user may edit the commit message outside of invoking * "arc diff --edit". When they do this, only some fields (those fields which * can not be edited by other users) are safe to overwrite. For instance, it * is fine to overwrite "Summary" because no one else can edit it, but not * to overwrite "Reviewers" because reviewers may have been added or removed * via the web interface. * * If a field is safe to overwrite when edited in a working copy commit * message, return true. If the authoritative value should always be used, * return false. By default, fields can not be overwritten. * * arc will only attempt to overwrite field values if run with "--verbatim". * * @return bool True to indicate the field is save to overwrite. * @task commit */ public function shouldOverwriteWhenCommitMessageIsEdited() { return false; } /** * Return true if this field should be suggested to the user during * "arc diff --edit". Basicially, return true if the field is something the * user might want to fill out (like "Summary"), and false if it's a * system/display/readonly field (like "Differential Revision"). If this * method returns true, the field will be rendered even if it has no value * during edit and update operations. * * @return bool True to indicate the field should appear in the edit template. * @task commit */ public function shouldAppearOnCommitMessageTemplate() { return true; } /** * Render a human-readable label for this field, like "Summary" or * "Test Plan". This is distinct from the commit message key, but generally * they should be similar. * * @return string Human-readable field label for commit messages. * @task commit */ public function renderLabelForCommitMessage() { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Render a human-readable value for this field when it appears in commit * messages (for instance, lists of users should be rendered as user names). * * The ##$is_edit## parameter allows you to distinguish between commit * messages being rendered for editing and those being rendered for amending * or commit. Some fields may decline to render a value in one mode (for * example, "Reviewed By" appears only when doing commit/amend, not while * editing). * * @param bool True if the message is being edited. * @return string Human-readable field value. * @task commit */ public function renderValueForCommitMessage($is_edit) { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * Return one or more labels which this field parses in commit messages. For * example, you might parse all of "Task", "Tasks" and "Task Numbers" or * similar. This is just to make it easier to get commit messages to parse * when users are typing in the fields manually as opposed to using a * template, by accepting alternate spellings / pluralizations / etc. By * default, only the label returned from @{method:renderLabelForCommitMessage} * is parsed. * * @return list List of supported labels that this field can parse from commit * messages. * @task commit */ public function getSupportedCommitMessageLabels() { return array($this->renderLabelForCommitMessage()); } /** * Parse a raw text block from a commit message into a canonical * representation of the field value. For example, the "CC" field accepts a * comma-delimited list of usernames and emails and parses them into valid * PHIDs, emitting a PHID list. * * If you encounter errors (like a nonexistent username) while parsing, * you should throw a @{class:DifferentialFieldParseException}. * * Generally, this method should accept whatever you return from * @{method:renderValueForCommitMessage} and parse it back into a sensible * representation. * * You must implement this method if you return true from * @{method:shouldAppearOnCommitMessage}. * * @param string * @return mixed The canonical representation of the field value. For example, * you should lookup usernames and object references. * @task commit */ public function parseValueFromCommitMessage($value) { throw new DifferentialFieldSpecificationIncompleteException($this); } /** * This method allows you to take action when a commit appears in a tracked * branch (for example, by closing tasks associated with the commit). * * @param PhabricatorRepository The repository the commit appeared in. * @param PhabricatorRepositoryCommit The commit itself. * @param PhabricatorRepostioryCommitData Commit data. * @return void * * @task commit */ public function didParseCommit( PhabricatorRepository $repo, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { return; } public function getCommitMessageTips() { return array(); } /* -( Loading Additional Data )-------------------------------------------- */ /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly. * * This is a convenience method which makes the handles available on all * interfaces where the field appears. If your field needs handles on only * some interfaces (or needs different handles on different interfaces) you * can overload the more specific methods to customize which interfaces you * retrieve handles for. Requesting only the handles you need will improve * the performance of your field. * * You can later retrieve these handles by calling @{method:getHandle}. * * @return list List of PHIDs to load handles for. * @task load */ protected function getRequiredHandlePHIDs() { return array(); } /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly on the view interface. * * This is a more specific version of @{method:getRequiredHandlePHIDs} which * can be overridden to improve field performance by loading only data you * need. * * @return list List of PHIDs to load handles for. * @task load */ public function getRequiredHandlePHIDsForRevisionView() { return $this->getRequiredHandlePHIDs(); } /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly on the list interface. * * This is a more specific version of @{method:getRequiredHandlePHIDs} which * can be overridden to improve field performance by loading only data you * need. * * @param DifferentialRevision The revision to pull PHIDs for. * @return list List of PHIDs to load handles for. * @task load */ public function getRequiredHandlePHIDsForRevisionList( DifferentialRevision $revision) { return array(); } /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly on the edit interface. * * This is a more specific version of @{method:getRequiredHandlePHIDs} which * can be overridden to improve field performance by loading only data you * need. * * @return list List of PHIDs to load handles for. * @task load */ public function getRequiredHandlePHIDsForRevisionEdit() { return $this->getRequiredHandlePHIDs(); } /** * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your * field to render correctly on the commit message interface. * * This is a more specific version of @{method:getRequiredHandlePHIDs} which * can be overridden to improve field performance by loading only data you * need. * * @return list List of PHIDs to load handles for. * @task load */ public function getRequiredHandlePHIDsForCommitMessage() { return $this->getRequiredHandlePHIDs(); } /** * Parse a list of users into a canonical PHID list. * * @param string Raw list of comma-separated user names. * @return list List of corresponding PHIDs. * @task load */ protected function parseCommitMessageUserList($value) { return $this->parseCommitMessageObjectList($value, $mailables = false); } protected function parseCommitMessageUserOrProjectList($value) { return $this->parseCommitMessageObjectList( $value, $mailables = false, $allow_partial = false); } /** * Parse a list of mailable objects into a canonical PHID list. * * @param string Raw list of comma-separated mailable names. * @return list List of corresponding PHIDs. * @task load */ protected function parseCommitMessageMailableList($value) { return $this->parseCommitMessageObjectList($value, $mailables = true); } /** * Parse and lookup a list of object names, converting them to PHIDs. * * @param string Raw list of comma-separated object names. * @param bool True to include mailing lists. * @param bool True to make a best effort. By default, an exception is * thrown if any item is invalid. * @return list List of corresponding PHIDs. * @task load */ public static function parseCommitMessageObjectList( $value, $include_mailables, $allow_partial = false) { $types = array( PhabricatorPeoplePHIDTypeUser::TYPECONST, PhabricatorProjectPHIDTypeProject::TYPECONST, ); if ($include_mailables) { $types[] = PhabricatorMailingListPHIDTypeList::TYPECONST; } return id(new PhabricatorObjectListQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setAllowPartialResults($allow_partial) ->setAllowedTypes($types) ->setObjectList($value) ->execute(); } /* -( Contextual Data )---------------------------------------------------- */ /** * @task context */ final public function setRevision(DifferentialRevision $revision) { $this->revision = $revision; $this->didSetRevision(); return $this; } /** * @task context */ protected function didSetRevision() { return; } /** * @task context */ final public function setDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } /** * @task context */ final public function setManualDiff(DifferentialDiff $diff) { $this->manualDiff = $diff; return $this; } /** * @task context */ final public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } /** * @task context */ final public function setDiffProperties(array $diff_properties) { $this->diffProperties = $diff_properties; return $this; } /** * @task context */ final public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } /** * @task context */ final protected function getRevision() { if (empty($this->revision)) { throw new DifferentialFieldDataNotAvailableException($this); } return $this->revision; } /** * Determine if revision context is currently available. * * @task context */ final protected function hasRevision() { return (bool)$this->revision; } /** * @task context */ final protected function getDiff() { if (empty($this->diff)) { throw new DifferentialFieldDataNotAvailableException($this); } return $this->diff; } /** * @task context */ final protected function getManualDiff() { if (!$this->manualDiff) { return $this->getDiff(); } return $this->manualDiff; } /** * @task context */ final protected function getUser() { if (empty($this->user)) { throw new DifferentialFieldDataNotAvailableException($this); } return $this->user; } /** * Get the handle for an object PHID. You must overload * @{method:getRequiredHandlePHIDs} (or a more specific version thereof) * and include the PHID you want in the list for it to be available here. * * @return PhabricatorObjectHandle Handle to the object. * @task context */ final protected function getHandle($phid) { if ($this->handles === null) { throw new DifferentialFieldDataNotAvailableException($this); } if (empty($this->handles[$phid])) { $class = get_class($this); throw new Exception( "A differential field (of class '{$class}') is attempting to retrieve ". "a handle ('{$phid}') which it did not request. Return all handle ". "PHIDs you need from getRequiredHandlePHIDs()."); } return $this->handles[$phid]; } final protected function getLoadedHandles() { if ($this->handles === null) { throw new DifferentialFieldDataNotAvailableException($this); } return $this->handles; } /** * Get the list of properties for a diff set by @{method:setManualDiff}. * * @return array Array of all Diff properties. * @task context */ final public function getDiffProperties() { if ($this->diffProperties === null) { // This will be set to some (possibly empty) array if we've loaded // properties, so null means diff properties aren't available in this // context. throw new DifferentialFieldDataNotAvailableException($this); } return $this->diffProperties; } /** * Get a property of a diff set by @{method:setManualDiff}. * * @param string Diff property key. * @return mixed|null Diff property, or null if the property does not have * a value. * @task context */ final public function getDiffProperty($key) { return idx($this->getDiffProperties(), $key); } } diff --git a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php index a00dc7b9fb..694fbf1b62 100644 --- a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php @@ -1,192 +1,164 @@ parseCorpus($message); $task_statuses = array(); foreach ($matches as $match) { $prefix = phutil_utf8_strtolower($match['prefix']); $suffix = phutil_utf8_strtolower($match['suffix']); $status = idx($suffixes, $suffix); if (!$status) { $status = idx($prefixes, $prefix); } foreach ($match['monograms'] as $task_monogram) { $task_id = (int)trim($task_monogram, 'tT'); $task_statuses[$task_id] = $status; } } return $task_statuses; } private function findDependentRevisions($message) { $matches = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($message); $dependents = array(); foreach ($matches as $match) { foreach ($match['monograms'] as $monogram) { $id = (int)trim($monogram, 'dD'); $dependents[$id] = $id; } } return $dependents; } public static function findRevertedCommits($message) { $matches = id(new DifferentialCustomFieldRevertsParser()) ->parseCorpus($message); $result = array(); foreach ($matches as $match) { foreach ($match['monograms'] as $monogram) { $result[$monogram] = $monogram; } } return $result; } - public function didWriteRevision(DifferentialRevisionEditor $editor) { - $message = $this->renderValueForCommitMessage(false); - - $tasks = $this->findMentionedTasks($message); - if ($tasks) { - $tasks = id(new ManiphestTaskQuery()) - ->setViewer($editor->getActor()) - ->withIDs(array_keys($tasks)) - ->execute(); - $this->saveFieldEdges( - $editor->getRevision(), - PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK, - mpull($tasks, 'getPHID')); - } - - $dependents = $this->findDependentRevisions($message); - if ($dependents) { - $dependents = id(new DifferentialRevisionQuery()) - ->setViewer($editor->getActor()) - ->withIDs($dependents) - ->execute(); - $this->saveFieldEdges( - $editor->getRevision(), - PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV, - mpull($dependents, 'getPHID')); - } - } - private function saveFieldEdges( DifferentialRevision $revision, $edge_type, array $add_phids) { $revision_phid = $revision->getPHID(); $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $revision_phid, $edge_type); $add_phids = array_diff($add_phids, $old_phids); if (!$add_phids) { return; } $edge_editor = id(new PhabricatorEdgeEditor())->setActor($this->getUser()); foreach ($add_phids as $phid) { $edge_editor->addEdge($revision_phid, $edge_type, $phid); } // NOTE: Deletes only through the fields. $edge_editor->save(); } public function didParseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { $message = $this->renderValueForCommitMessage($is_edit = false); $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $data->getCommitDetail('authorPHID')); if (!$user) { // TODO: Maybe after grey users, we should find a way to proceed even // if we don't know who the author is. return; } $commit_names = self::findRevertedCommits($message); if ($commit_names) { $reverts = id(new DiffusionCommitQuery()) ->setViewer($user) ->withIdentifiers($commit_names) ->withDefaultRepository($repository) ->execute(); foreach ($reverts as $revert) { // TODO: Do interesting things here. } } $tasks_statuses = $this->findMentionedTasks($message); if (!$tasks_statuses) { return; } $tasks = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array_keys($tasks_statuses)) ->execute(); foreach ($tasks as $task_id => $task) { id(new PhabricatorEdgeEditor()) ->setActor($user) ->addEdge( $task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT, $commit->getPHID()) ->save(); $status = $tasks_statuses[$task_id]; if (!$status) { // Text like "Ref T123", don't change the task status. continue; } if ($task->getStatus() == $status) { // Task is already in the specified status, so skip updating it. continue; } $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $call = new ConduitCall( 'maniphest.update', array( 'id' => $task->getID(), 'status' => $status, 'comments' => "Closed by commit {$commit_name}.", )); $call->setUser($user); $call->execute(); } } } diff --git a/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php b/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php index 8f9394d069..27a2db98e6 100644 --- a/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php @@ -1,195 +1,170 @@ value); } public function setValueFromStorage($value) { if (!strlen($value)) { $this->value = array(); } else { $this->value = json_decode($value, true); } return $this; } public function shouldAppearOnEdit() { return true; } public function setValueFromRequest(AphrontRequest $request) { $this->value = $request->getStrList($this->getStorageKey()); return $this; } public function renderEditControl() { return id(new AphrontFormTextControl()) ->setLabel(pht('JIRA Issues')) ->setCaption( pht('Example: %s', phutil_tag('tt', array(), 'JIS-3, JIS-9'))) ->setName($this->getStorageKey()) ->setValue(implode(', ', nonempty($this->value, array()))) ->setError($this->error); } public function shouldAppearOnRevisionView() { return true; } public function renderLabelForRevisionView() { return pht('JIRA Issues:'); } public function renderValueForRevisionView() { $xobjs = $this->loadDoorkeeperExternalObjects(); if (!$xobjs) { return null; } $links = array(); foreach ($xobjs as $xobj) { $links[] = id(new DoorkeeperTagView()) ->setExternalObject($xobj); } return phutil_implode_html(phutil_tag('br'), $links); } public function shouldAppearOnConduitView() { return true; } public function getValueForConduit() { return $this->value; } public function shouldAppearOnCommitMessage() { return true; } public function getCommitMessageKey() { return 'jira.issues'; } public function setValueFromParsedCommitMessage($value) { $this->value = $value; return $this; } public function shouldOverwriteWhenCommitMessageIsEdited() { return true; } public function renderLabelForCommitMessage() { return 'JIRA Issues'; } public function renderValueForCommitMessage($is_edit) { return implode(', ', $this->value); } public function getSupportedCommitMessageLabels() { return array( 'JIRA', 'JIRA Issues', 'JIRA Issue', ); } public function parseValueFromCommitMessage($value) { return preg_split('/[\s,]+/', $value, $limit = -1, PREG_SPLIT_NO_EMPTY); } public function validateField() { if ($this->value) { $refs = id(new DoorkeeperImportEngine()) ->setViewer($this->getUser()) ->setRefs($this->buildDoorkeeperRefs()) ->execute(); $bad = array(); foreach ($refs as $ref) { if (!$ref->getIsVisible()) { $bad[] = $ref->getObjectID(); } } if ($bad) { $bad = implode(', ', $bad); $this->error = pht('Invalid'); throw new DifferentialFieldValidationException( pht( "Some JIRA issues could not be loaded. They may not exist, or ". "you may not have permission to view them: %s", $bad)); } } } private function buildDoorkeeperRefs() { $provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); $refs = array(); if ($this->value) { foreach ($this->value as $jira_key) { $refs[] = id(new DoorkeeperObjectRef()) ->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA) ->setApplicationDomain($provider->getProviderDomain()) ->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE) ->setObjectID($jira_key); } } return $refs; } private function loadDoorkeeperExternalObjects() { $refs = $this->buildDoorkeeperRefs(); if (!$refs) { return array(); } $xobjs = id(new DoorkeeperExternalObjectQuery()) ->setViewer($this->getUser()) ->withObjectKeys(mpull($refs, 'getObjectKey')) ->execute(); return $xobjs; } - public function didWriteRevision(DifferentialRevisionEditor $editor) { - $revision = $editor->getRevision(); - $revision_phid = $revision->getPHID(); - - $edge_type = PhabricatorEdgeConfig::TYPE_PHOB_HAS_JIRAISSUE; - $edge_dsts = mpull($this->loadDoorkeeperExternalObjects(), 'getPHID'); - - $edges = PhabricatorEdgeQuery::loadDestinationPHIDs( - $revision_phid, - $edge_type); - - $editor = id(new PhabricatorEdgeEditor()) - ->setActor($this->getUser()); - - foreach (array_diff($edges, $edge_dsts) as $rem_edge) { - $editor->removeEdge($revision_phid, $edge_type, $rem_edge); - } - - foreach (array_diff($edge_dsts, $edges) as $add_edge) { - $editor->addEdge($revision_phid, $edge_type, $add_edge); - } - - $editor->save(); - } - } diff --git a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php index dd851590ab..9cdb7b72a3 100644 --- a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php @@ -1,186 +1,158 @@ getManiphestTaskPHIDs(); } public function renderLabelForRevisionView() { return 'Maniphest Tasks:'; } public function renderValueForRevisionView() { $task_phids = $this->getManiphestTaskPHIDs(); if (!$task_phids) { return null; } $links = array(); foreach ($task_phids as $task_phid) { $links[] = $this->getHandle($task_phid)->renderLink(); } return phutil_implode_html(phutil_tag('br'), $links); } private function getManiphestTaskPHIDs() { $revision = $this->getRevision(); if (!$revision->getPHID()) { return array(); } return PhabricatorEdgeQuery::loadDestinationPHIDs( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK); } - /** - * Attach the revision to the task(s) and the task(s) to the revision. - * - * @return void - */ - public function didWriteRevision(DifferentialRevisionEditor $editor) { - $revision = $editor->getRevision(); - $revision_phid = $revision->getPHID(); - $edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK; - - $old_phids = $this->oldManiphestTasks; - $add_phids = $this->maniphestTasks; - $rem_phids = array_diff($old_phids, $add_phids); - - $edge_editor = id(new PhabricatorEdgeEditor()) - ->setActor($this->getUser()); - - foreach ($add_phids as $phid) { - $edge_editor->addEdge($revision_phid, $edge_type, $phid); - } - - foreach ($rem_phids as $phid) { - $edge_editor->removeEdge($revision_phid, $edge_type, $phid); - } - - $edge_editor->save(); - } - protected function didSetRevision() { $this->maniphestTasks = $this->getManiphestTaskPHIDs(); $this->oldManiphestTasks = $this->maniphestTasks; } public function getRequiredHandlePHIDsForCommitMessage() { return $this->maniphestTasks; } public function shouldAppearOnCommitMessageTemplate() { return false; } public function shouldAppearOnCommitMessage() { return $this->shouldAppearOnRevisionView(); } public function getCommitMessageKey() { return 'maniphestTaskPHIDs'; } public function setValueFromParsedCommitMessage($value) { $this->maniphestTasks = array_unique(nonempty($value, array())); return $this; } public function renderLabelForCommitMessage() { return 'Maniphest Tasks'; } public function getSupportedCommitMessageLabels() { return array( 'Maniphest Task', 'Maniphest Tasks', ); } public function renderValueForCommitMessage($is_edit) { if (!$this->maniphestTasks) { return null; } $names = array(); foreach ($this->maniphestTasks as $phid) { $handle = $this->getHandle($phid); $names[] = $handle->getName(); } return implode(', ', $names); } public function parseValueFromCommitMessage($value) { $matches = null; preg_match_all('/T(\d+)/', $value, $matches); if (empty($matches[0])) { return array(); } // TODO: T603 Get a viewer here so we can issue the right query. $task_ids = $matches[1]; $tasks = id(new ManiphestTask()) ->loadAllWhere('id in (%Ld)', $task_ids); $task_phids = array(); $invalid = array(); foreach ($task_ids as $task_id) { $task = idx($tasks, $task_id); if (empty($task)) { $invalid[] = 'T'.$task_id; } else { $task_phids[] = $task->getPHID(); } } if ($invalid) { $what = pht('Maniphest Task(s)', count($invalid)); $invalid = implode(', ', $invalid); throw new DifferentialFieldParseException( "Commit message references nonexistent {$what}: {$invalid}."); } return $task_phids; } public function renderValueForMail($phase) { if ($phase == DifferentialMailPhase::COMMENT) { return null; } if (!$this->maniphestTasks) { return null; } $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getUser()) ->withPHIDs($this->maniphestTasks) ->execute(); $body = array(); $body[] = 'MANIPHEST TASKS'; foreach ($handles as $handle) { $body[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI()); } return implode("\n", $body); } public function getCommitMessageTips() { return array( 'Use "Fixes T123" in your summary to mark that the current '. 'revision completes a given task.' ); } } diff --git a/src/applications/differential/field/specification/DifferentialRepositoryFieldSpecification.php b/src/applications/differential/field/specification/DifferentialRepositoryFieldSpecification.php index 36adc65d1f..6cd56fa681 100644 --- a/src/applications/differential/field/specification/DifferentialRepositoryFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialRepositoryFieldSpecification.php @@ -1,48 +1,44 @@ value = $this->getRevision()->getRepositoryPHID(); } public function setValueFromRequest(AphrontRequest $request) { $value = head($request->getArr('repositoryPHID')); $this->value = nonempty($value, null); return $this; } public function getRequiredHandlePHIDsForRevisionEdit() { return array_filter(array($this->value)); } public function renderEditControl() { $value = array(); if ($this->value) { $value = array( $this->getHandle($this->value), ); } return id(new AphrontFormTokenizerControl()) ->setLabel('Repository') ->setName('repositoryPHID') ->setUser($this->getUser()) ->setLimit(1) ->setDatasource('/typeahead/common/repositories/') ->setValue($value); } - public function willWriteRevision(DifferentialRevisionEditor $editor) { - $this->getRevision()->setRepositoryPHID($this->value); - } - } diff --git a/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php b/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php index 35c47afef7..d6965b7900 100644 --- a/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php @@ -1,201 +1,197 @@ getReviewerPHIDs(); } public function renderLabelForRevisionView() { return pht('Reviewers'); } public function renderValueForRevisionView() { $reviewers = array(); foreach ($this->getRevision()->getReviewerStatus() as $reviewer) { if ($reviewer->isUser()) { $reviewers[] = $reviewer; } } if (!$reviewers) { // Renders "None". return $this->renderUserList(array()); } $view = id(new DifferentialReviewersView()) ->setUser($this->getUser()) ->setReviewers($reviewers) ->setHandles($this->getLoadedHandles()); $diff = $this->getRevision()->loadActiveDiff(); if ($diff) { $view->setActiveDiff($diff); } return $view; } private function getReviewerPHIDs() { $revision = $this->getRevision(); return $revision->getReviewers(); } public function shouldAppearOnEdit() { return true; } protected function didSetRevision() { $this->reviewers = $this->getReviewerPHIDs(); } public function getRequiredHandlePHIDsForRevisionEdit() { return $this->reviewers; } public function setValueFromRequest(AphrontRequest $request) { $this->reviewers = $request->getArr('reviewers'); return $this; } public function validateField() { if (!$this->hasRevision()) { return; } $self = PhabricatorEnv::getEnvConfig('differential.allow-self-accept'); if ($self) { return; } $author_phid = $this->getRevision()->getAuthorPHID(); if (!in_array($author_phid, $this->reviewers)) { return; } $this->error = 'Invalid'; throw new DifferentialFieldValidationException( "The owner of a revision may not be a reviewer."); } public function renderEditControl() { $reviewer_map = array(); foreach ($this->reviewers as $phid) { $reviewer_map[] = $this->getHandle($phid); } return id(new AphrontFormTokenizerControl()) ->setLabel(pht('Reviewers')) ->setName('reviewers') ->setUser($this->getUser()) ->setDatasource('/typeahead/common/usersorprojects/') ->setValue($reviewer_map) ->setError($this->error); } - public function willWriteRevision(DifferentialRevisionEditor $editor) { - $editor->setReviewers($this->reviewers); - } - public function shouldAppearOnCommitMessage() { return true; } public function getCommitMessageKey() { return 'reviewerPHIDs'; } public function setValueFromParsedCommitMessage($value) { $this->reviewers = array_unique(nonempty($value, array())); return $this; } public function renderLabelForCommitMessage() { return 'Reviewers'; } public function getRequiredHandlePHIDsForCommitMessage() { return $this->reviewers; } public function renderValueForCommitMessage($is_edit) { if (!$this->reviewers) { return null; } $names = array(); foreach ($this->reviewers as $phid) { $names[] = $this->getHandle($phid)->getObjectName(); } return implode(', ', $names); } public function getSupportedCommitMessageLabels() { return array( 'Reviewer', 'Reviewers', ); } public function parseValueFromCommitMessage($value) { return $this->parseCommitMessageUserOrProjectList($value); } public function shouldAppearOnRevisionList() { return true; } public function renderHeaderForRevisionList() { return 'Reviewers'; } public function renderValueForRevisionList(DifferentialRevision $revision) { $primary_reviewer = $revision->getPrimaryReviewer(); if ($primary_reviewer) { $names = array(); foreach ($revision->getReviewers() as $reviewer) { $names[] = $this->getHandle($reviewer)->renderLink(); } return phutil_implode_html(', ', $names); } else { return phutil_tag('em', array(), 'None'); } } public function getRequiredHandlePHIDsForRevisionList( DifferentialRevision $revision) { return $revision->getReviewers(); } public function renderValueForMail($phase) { if ($phase == DifferentialMailPhase::COMMENT) { return null; } if (!$this->reviewers) { return null; } $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getUser()) ->withPHIDs($this->reviewers) ->execute(); $handles = array_select_keys( $handles, array($this->getRevision()->getPrimaryReviewer())) + $handles; $names = mpull($handles, 'getObjectName'); return 'Reviewers: '.implode(', ', $names); } } diff --git a/src/applications/differential/field/specification/DifferentialSummaryFieldSpecification.php b/src/applications/differential/field/specification/DifferentialSummaryFieldSpecification.php index cb1515d6f7..19f6004a07 100644 --- a/src/applications/differential/field/specification/DifferentialSummaryFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialSummaryFieldSpecification.php @@ -1,105 +1,101 @@ summary = (string)$this->getRevision()->getSummary(); } public function setValueFromRequest(AphrontRequest $request) { $this->summary = $request->getStr('summary'); return $this; } public function renderEditControl() { return id(new PhabricatorRemarkupControl()) ->setLabel(pht('Summary')) ->setName('summary') ->setID($this->getControlID()) ->setValue($this->summary); } public function renderEditPreview() { return id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Summary Preview')) ->setControlID($this->getControlID()) ->setPreviewURI('/differential/preview/'); } public function shouldExtractMentions() { return true; } - public function willWriteRevision(DifferentialRevisionEditor $editor) { - $this->getRevision()->setSummary($this->summary); - } - public function shouldAppearOnCommitMessage() { return true; } public function getCommitMessageKey() { return 'summary'; } public function setValueFromParsedCommitMessage($value) { $this->summary = (string)$value; return $this; } public function shouldOverwriteWhenCommitMessageIsEdited() { return true; } public function renderLabelForCommitMessage() { return 'Summary'; } public function renderValueForCommitMessage($is_edit) { return $this->summary; } public function parseValueFromCommitMessage($value) { return (string)$value; } public function renderValueForMail($phase) { if ($phase != DifferentialMailPhase::WELCOME) { return null; } if ($this->summary == '') { return null; } return $this->summary; } public function shouldAddToSearchIndex() { return true; } public function getValueForSearchIndex() { return $this->summary; } public function getKeyForSearchIndex() { return PhabricatorSearchField::FIELD_BODY; } private function getControlID() { if (!$this->controlID) { $this->controlID = celerity_generate_unique_node_id(); } return $this->controlID; } } diff --git a/src/applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php b/src/applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php index 74dcb42014..7928902843 100644 --- a/src/applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php @@ -1,128 +1,124 @@ plan = (string)$this->getRevision()->getTestPlan(); } public function setValueFromRequest(AphrontRequest $request) { $this->plan = $request->getStr('testplan'); $this->error = null; return $this; } public function renderEditControl() { if ($this->error === false) { if ($this->isRequired()) { $this->error = true; } else { $this->error = null; } } return id(new PhabricatorRemarkupControl()) ->setLabel('Test Plan') ->setName('testplan') ->setValue($this->plan) ->setError($this->error); } public function shouldExtractMentions() { return true; } - public function willWriteRevision(DifferentialRevisionEditor $editor) { - $this->getRevision()->setTestPlan($this->plan); - } - public function validateField() { if ($this->isRequired()) { if (!strlen($this->plan)) { $this->error = 'Required'; throw new DifferentialFieldValidationException( "You must provide a test plan."); } } } public function shouldAppearOnCommitMessage() { return PhabricatorEnv::getEnvConfig('differential.show-test-plan-field'); } public function getCommitMessageKey() { return 'testPlan'; } public function setValueFromParsedCommitMessage($value) { $this->plan = (string)$value; return $this; } public function shouldOverwriteWhenCommitMessageIsEdited() { return true; } public function renderLabelForCommitMessage() { return 'Test Plan'; } public function getSupportedCommitMessageLabels() { return array( 'Test Plan', 'Testplan', 'Tested', 'Tests', ); } public function renderValueForCommitMessage($is_edit) { return $this->plan; } public function parseValueFromCommitMessage($value) { return $value; } public function renderValueForMail($phase) { if ($phase != DifferentialMailPhase::WELCOME) { return null; } if ($this->plan == '') { return null; } return "TEST PLAN\n".preg_replace('/^/m', ' ', $this->plan); } public function shouldAddToSearchIndex() { return true; } public function getValueForSearchIndex() { return $this->plan; } public function getKeyForSearchIndex() { return 'tpln'; } private function isRequired() { return PhabricatorEnv::getEnvConfig('differential.require-test-plan-field'); } } diff --git a/src/applications/differential/field/specification/DifferentialTitleFieldSpecification.php b/src/applications/differential/field/specification/DifferentialTitleFieldSpecification.php index 59fb375e32..b05a210b05 100644 --- a/src/applications/differential/field/specification/DifferentialTitleFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialTitleFieldSpecification.php @@ -1,111 +1,107 @@ title = $this->getRevision()->getTitle(); } public function setValueFromRequest(AphrontRequest $request) { $this->title = $request->getStr('title'); $this->error = null; return $this; } public function renderEditControl() { return id(new AphrontFormTextAreaControl()) ->setLabel('Title') ->setName('title') ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setError($this->error) ->setValue($this->title); } public function shouldExtractMentions() { return true; } - public function willWriteRevision(DifferentialRevisionEditor $editor) { - $this->getRevision()->setTitle($this->title); - } - public function validateField() { if (!strlen($this->title)) { $this->error = 'Required'; throw new DifferentialFieldValidationException( "You must provide a revision title in the first line ". "of your commit message."); } if (preg_match('/^<<.*>>$/', $this->title)) { $default_title = self::getDefaultRevisionTitle(); $this->error = 'Required'; throw new DifferentialFieldValidationException( "Replace the line '{$default_title}' with a revision title ". "that describes the change."); } } public function shouldAppearOnCommitMessage() { return true; } public function getCommitMessageKey() { return 'title'; } public function setValueFromParsedCommitMessage($value) { $this->title = $value; return $this; } public function shouldOverwriteWhenCommitMessageIsEdited() { return true; } public function renderLabelForCommitMessage() { return 'Title'; } public function renderValueForCommitMessage($is_edit) { return $this->title; } public function parseValueFromCommitMessage($value) { return preg_replace('/\s*\n\s*/', ' ', $value); } public function shouldAppearOnRevisionList() { return true; } public function renderHeaderForRevisionList() { return 'Revision'; } public function getColumnClassForRevisionList() { return 'wide pri'; } public static function getDefaultRevisionTitle() { return '<>'; } public function renderValueForRevisionList(DifferentialRevision $revision) { return phutil_tag( 'a', array( 'href' => '/D'.$revision->getID(), ), $revision->getTitle()); } } diff --git a/src/applications/differential/field/specification/DifferentialViewPolicyFieldSpecification.php b/src/applications/differential/field/specification/DifferentialViewPolicyFieldSpecification.php index df8568c33e..f8205afdfe 100644 --- a/src/applications/differential/field/specification/DifferentialViewPolicyFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialViewPolicyFieldSpecification.php @@ -1,42 +1,38 @@ value = $this->getRevision()->getViewPolicy(); } public function setValueFromRequest(AphrontRequest $request) { $this->value = $request->getStr('viewPolicy'); return $this; } public function renderEditControl() { $viewer = $this->getUser(); $revision = $this->getRevision(); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($revision) ->execute(); return id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($revision) ->setPolicies($policies) ->setName('viewPolicy'); } - public function willWriteRevision(DifferentialRevisionEditor $editor) { - $this->getRevision()->setViewPolicy($this->value); - } - }