diff --git a/src/applications/differential/conduit/ConduitAPI_differential_createcomment_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_createcomment_Method.php index 58d2b43056..3f6c67a01a 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_createcomment_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_createcomment_Method.php @@ -1,65 +1,67 @@ 'required revisionid', 'message' => 'optional string', 'action' => 'optional string', 'silent' => 'optional bool', 'attach_inlines' => 'optional bool', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_REVISION' => 'Bad revision ID.', ); } protected function execute(ConduitAPIRequest $request) { - $revision = id(new DifferentialRevision())->load( - $request->getValue('revision_id')); + $revision = id(new DifferentialRevisionQuery()) + ->setViewer($request->getUser()) + ->withIDs(array($request->getValue('revision_id'))) + ->executeOne(); if (!$revision) { throw new ConduitException('ERR_BAD_REVISION'); } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_CONDUIT, array()); $action = $request->getValue('action'); if (!$action) { $action = 'none'; } $editor = new DifferentialCommentEditor( $revision, $action); $editor->setActor($request->getUser()); $editor->setContentSource($content_source); $editor->setMessage($request->getValue('message')); $editor->setNoEmail($request->getValue('silent')); $editor->setAttachInlineComments($request->getValue('attach_inlines')); $editor->save(); return array( 'revisionid' => $revision->getID(), 'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()), ); } } diff --git a/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php index a62cca98e2..d2f4b9a499 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php @@ -1,150 +1,153 @@ 'required list', 'sourceMachine' => 'required string', 'sourcePath' => 'required string', 'branch' => 'required string', 'bookmark' => 'optional string', 'sourceControlSystem' => 'required enum', 'sourceControlPath' => 'required string', 'sourceControlBaseRevision' => 'required string', 'parentRevisionID' => 'optional revisionid', 'creationMethod' => 'optional string', 'authorPHID' => 'optional phid', 'arcanistProject' => 'optional string', 'repositoryUUID' => 'optional string', 'lintStatus' => 'required enum', 'unitStatus' => 'required enum', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $change_data = $request->getValue('changes'); $changes = array(); foreach ($change_data as $dict) { $changes[] = ArcanistDiffChange::newFromDictionary($dict); } $diff = DifferentialDiff::newFromRawChanges($changes); $diff->setSourcePath($request->getValue('sourcePath')); $diff->setSourceMachine($request->getValue('sourceMachine')); $diff->setBranch($request->getValue('branch')); $diff->setCreationMethod($request->getValue('creationMethod')); $diff->setAuthorPHID($request->getValue('authorPHID')); $diff->setBookmark($request->getValue('bookmark')); $parent_id = $request->getValue('parentRevisionID'); if ($parent_id) { - $parent_rev = id(new DifferentialRevision())->load($parent_id); + $parent_rev = id(new DifferentialRevisionQuery()) + ->setViewer($request->getUser()) + ->withIDs(array($parent_id)) + ->executeOne(); if ($parent_rev) { if ($parent_rev->getStatus() != ArcanistDifferentialRevisionStatus::CLOSED) { $diff->setParentRevisionID($parent_id); } } } $system = $request->getValue('sourceControlSystem'); $diff->setSourceControlSystem($system); $diff->setSourceControlPath($request->getValue('sourceControlPath')); $diff->setSourceControlBaseRevision( $request->getValue('sourceControlBaseRevision')); $project_name = $request->getValue('arcanistProject'); $project_phid = null; if ($project_name) { $arcanist_project = id(new PhabricatorRepositoryArcanistProject()) ->loadOneWhere( 'name = %s', $project_name); if (!$arcanist_project) { $arcanist_project = new PhabricatorRepositoryArcanistProject(); $arcanist_project->setName($project_name); $arcanist_project->save(); } $project_phid = $arcanist_project->getPHID(); } $diff->setArcanistProjectPHID($project_phid); $diff->setRepositoryUUID($request->getValue('repositoryUUID')); switch ($request->getValue('lintStatus')) { case 'skip': $diff->setLintStatus(DifferentialLintStatus::LINT_SKIP); break; case 'okay': $diff->setLintStatus(DifferentialLintStatus::LINT_OKAY); break; case 'warn': $diff->setLintStatus(DifferentialLintStatus::LINT_WARN); break; case 'fail': $diff->setLintStatus(DifferentialLintStatus::LINT_FAIL); break; case 'postponed': $diff->setLintStatus(DifferentialLintStatus::LINT_POSTPONED); break; case 'none': default: $diff->setLintStatus(DifferentialLintStatus::LINT_NONE); break; } switch ($request->getValue('unitStatus')) { case 'skip': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP); break; case 'okay': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_OKAY); break; case 'warn': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_WARN); break; case 'fail': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_FAIL); break; case 'postponed': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_POSTPONED); break; case 'none': default: $diff->setUnitStatus(DifferentialUnitStatus::UNIT_NONE); break; } $diff->save(); $path = '/differential/diff/'.$diff->getID().'/'; $uri = PhabricatorEnv::getURI($path); return array( 'diffid' => $diff->getID(), 'uri' => $uri, ); } } diff --git a/src/applications/differential/conduit/ConduitAPI_differential_createinline_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_createinline_Method.php index b2359a82cc..bd1bf83a66 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_createinline_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_createinline_Method.php @@ -1,105 +1,108 @@ 'optional revisionid', 'diffID' => 'optional diffid', 'filePath' => 'required string', 'isNewFile' => 'required bool', 'lineNumber' => 'required int', 'lineLength' => 'optional int', 'content' => 'required string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR-BAD-REVISION' => 'Bad revision ID.', 'ERR-BAD-DIFF' => 'Bad diff ID, or diff does not belong to revision.', 'ERR-NEED-DIFF' => 'Neither revision ID nor diff ID was provided.', 'ERR-NEED-FILE' => 'A file path was not provided.', 'ERR-BAD-FILE' => "Requested file doesn't exist in this revision." ); } protected function execute(ConduitAPIRequest $request) { $rid = $request->getValue('revisionID'); $did = $request->getValue('diffID'); if ($rid) { // Given both a revision and a diff, check that they match. // Given only a revision, find the active diff. - $revision = id(new DifferentialRevision())->load($rid); + $revision = id(new DifferentialRevisionQuery()) + ->setViewer($request->getUser()) + ->withIDs(array($rid)) + ->executeOne(); if (!$revision) { throw new ConduitException('ERR-BAD-REVISION'); } if (!$did) { // did not! $diff = $revision->loadActiveDiff(); $did = $diff->getID(); } else { // did too! $diff = id(new DifferentialDiff())->load($did); if (!$diff || $diff->getRevisionID() != $rid) { throw new ConduitException('ERR-BAD-DIFF'); } } } else if ($did) { // Given only a diff, find the parent revision. $diff = id(new DifferentialDiff())->load($did); if (!$diff) { throw new ConduitException('ERR-BAD-DIFF'); } $rid = $diff->getRevisionID(); } else { // Given neither, bail. throw new ConduitException('ERR-NEED-DIFF'); } $file = $request->getValue('filePath'); if (!$file) { throw new ConduitException('ERR-NEED-FILE'); } $changes = id(new DifferentialChangeset())->loadAllWhere( 'diffID = %d', $did); $cid = null; foreach ($changes as $id => $change) { if ($file == $change->getFilename()) { $cid = $id; } } if ($cid == null) { throw new ConduitException('ERR-BAD-FILE'); } $inline = id(new DifferentialInlineComment()) ->setRevisionID($rid) ->setChangesetID($cid) ->setAuthorPHID($request->getUser()->getPHID()) ->setContent($request->getValue('content')) ->setIsNewFile($request->getValue('isNewFile')) ->setLineNumber($request->getValue('lineNumber')) ->setLineLength($request->getValue('lineLength', 0)) ->save(); // Load everything again, just to be safe. $changeset = id(new DifferentialChangeset()) ->load($inline->getChangesetID()); return $this->buildInlineInfoDictionary($inline, $changeset); } } diff --git a/src/applications/differential/controller/DifferentialCommentSaveController.php b/src/applications/differential/controller/DifferentialCommentSaveController.php index d79b0dcb7b..6e888947a9 100644 --- a/src/applications/differential/controller/DifferentialCommentSaveController.php +++ b/src/applications/differential/controller/DifferentialCommentSaveController.php @@ -1,84 +1,89 @@ getRequest(); if (!$request->isFormPost()) { return new Aphront400Response(); } + $viewer = $request->getUser(); + $revision_id = $request->getInt('revision_id'); - $revision = id(new DifferentialRevision())->load($revision_id); + $revision = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withIDs(array($revision_id)) + ->executeOne(); if (!$revision) { return new Aphront400Response(); } $comment = $request->getStr('comment'); $action = $request->getStr('action'); $reviewers = $request->getArr('reviewers'); $ccs = $request->getArr('ccs'); $editor = new DifferentialCommentEditor( $revision, $action); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), )); try { $editor ->setActor($request->getUser()) ->setMessage($comment) ->setContentSource($content_source) ->setAttachInlineComments(true) ->setAddedReviewers($reviewers) ->setAddedCCs($ccs) ->save(); } catch (DifferentialActionHasNoEffectException $no_effect) { $has_inlines = id(new DifferentialInlineCommentQuery()) ->withDraftComments($request->getUser()->getPHID(), $revision->getID()) ->execute(); $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->addCancelButton('/D'.$revision_id); $dialog->addHiddenInput('revision_id', $revision_id); $dialog->addHiddenInput('action', 'none'); $dialog->addHiddenInput('reviewers', $reviewers); $dialog->addHiddenInput('ccs', $ccs); $dialog->addHiddenInput('comment', $comment); $dialog->setTitle(pht('Action Has No Effect')); $dialog->appendChild( phutil_tag('p', array(), $no_effect->getMessage())); if (strlen($comment) || $has_inlines) { $dialog->addSubmitButton(pht('Post as Comment')); $dialog->appendChild(phutil_tag('br')); $dialog->appendChild(phutil_tag('p', array(), pht( 'Do you want to post your feedback anyway, as a normal comment?'))); } return id(new AphrontDialogResponse())->setDialog($dialog); } // TODO: Diff change detection? $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $request->getUser()->getPHID(), 'differential-comment-'.$revision->getID()); if ($draft) { $draft->delete(); } return id(new AphrontRedirectResponse()) ->setURI('/D'.$revision->getID()); } } diff --git a/src/applications/differential/controller/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/DifferentialInlineCommentEditController.php index 83756fa88d..62c5d6c781 100644 --- a/src/applications/differential/controller/DifferentialInlineCommentEditController.php +++ b/src/applications/differential/controller/DifferentialInlineCommentEditController.php @@ -1,70 +1,76 @@ revisionID = $data['id']; } protected function createComment() { // Verify revision and changeset correspond to actual objects. $revision_id = $this->revisionID; $changeset_id = $this->getChangesetID(); - if (!id(new DifferentialRevision())->load($revision_id)) { + $viewer = $this->getRequest()->getUser(); + $revision = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withIDs(array($revision_id)) + ->executeOne(); + + if (!$revision) { throw new Exception("Invalid revision ID!"); } if (!id(new DifferentialChangeset())->load($changeset_id)) { throw new Exception("Invalid changeset ID!"); } return id(new DifferentialInlineComment()) ->setRevisionID($revision_id) ->setChangesetID($changeset_id); } protected function loadComment($id) { return id(new DifferentialInlineCommentQuery()) ->withIDs(array($id)) ->executeOne(); } protected function loadCommentForEdit($id) { $request = $this->getRequest(); $user = $request->getUser(); $inline = $this->loadComment($id); if (!$this->canEditInlineComment($user, $inline)) { throw new Exception("That comment is not editable!"); } return $inline; } private function canEditInlineComment( PhabricatorUser $user, DifferentialInlineComment $inline) { // Only the author may edit a comment. if ($inline->getAuthorPHID() != $user->getPHID()) { return false; } // Saved comments may not be edited. if ($inline->getCommentID()) { return false; } // Inline must be attached to the active revision. if ($inline->getRevisionID() != $this->revisionID) { return false; } return true; } } diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php index 0bdc2b6ec0..ed4886643c 100644 --- a/src/applications/differential/controller/DifferentialRevisionEditController.php +++ b/src/applications/differential/controller/DifferentialRevisionEditController.php @@ -1,207 +1,212 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if (!$this->id) { $this->id = $request->getInt('revisionID'); } if ($this->id) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needRelationships(true) ->needReviewerStatus(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->executeOne(); if (!$revision) { return new Aphront404Response(); } } else { $revision = new DifferentialRevision(); $revision->attachRelationships(array()); } $aux_fields = $this->loadAuxiliaryFields($revision); $diff_id = $request->getInt('diffID'); if ($diff_id) { $diff = id(new DifferentialDiffQuery()) ->setViewer($viewer) ->withIDs(array($diff_id)) ->executeOne(); if (!$diff) { return new Aphront404Response(); } if ($diff->getRevisionID()) { // TODO: Redirect? throw new Exception("This diff is already attached to a revision!"); } } else { $diff = null; } $errors = array(); if ($request->isFormPost() && !$request->getStr('viaDiffView')) { foreach ($aux_fields as $aux_field) { $aux_field->setValueFromRequest($request); try { $aux_field->validateField(); } catch (DifferentialFieldValidationException $ex) { $errors[] = $ex->getMessage(); } } if (!$errors) { $is_new = !$revision->getID(); $user = $request->getUser(); $editor = new DifferentialRevisionEditor($revision); $editor->setActor($request->getUser()); if ($diff) { $editor->addDiff($diff, $request->getStr('comments')); } $editor->setAuxiliaryFields($aux_fields); $editor->setAphrontRequestForEventDispatch($request); $editor->save(); return id(new AphrontRedirectResponse()) ->setURI('/D'.$revision->getID()); } } $aux_phids = array(); foreach ($aux_fields as $key => $aux_field) { $aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionEdit(); } $phids = array_mergev($aux_phids); $phids = array_unique($phids); $handles = $this->loadViewerHandles($phids); foreach ($aux_fields as $key => $aux_field) { $aux_field->setHandles(array_select_keys($handles, $aux_phids[$key])); } $form = new AphrontFormView(); $form->setUser($request->getUser()); if ($diff) { $form->addHiddenInput('diffID', $diff->getID()); } if ($revision->getID()) { $form->setAction('/differential/revision/edit/'.$revision->getID().'/'); } else { $form->setAction('/differential/revision/edit/'); } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } if ($diff && $revision->getID()) { $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Comments')) ->setName('comments') ->setCaption(pht("Explain what's new in this diff.")) ->setValue($request->getStr('comments'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save'))) ->appendChild( id(new AphrontFormDividerControl())); } $preview = array(); foreach ($aux_fields as $aux_field) { $control = $aux_field->renderEditControl(); if ($control) { $form->appendChild($control); } $preview[] = $aux_field->renderEditPreview(); } $submit = id(new AphrontFormSubmitControl()) ->setValue('Save'); if ($diff) { $submit->addCancelButton('/differential/diff/'.$diff->getID().'/'); } else { $submit->addCancelButton('/D'.$revision->getID()); } $form->appendChild($submit); $crumbs = $this->buildApplicationCrumbs(); if ($revision->getID()) { if ($diff) { $title = pht('Update Differential Revision'); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName('D'.$revision->getID()) ->setHref('/differential/diff/'.$diff->getID().'/')); } else { $title = pht('Edit Differential Revision'); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName('D'.$revision->getID()) ->setHref('/D'.$revision->getID())); } } else { $title = pht('Create New Differential Revision'); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($title)); return $this->buildApplicationPage( array( $crumbs, $form_box, $preview), array( 'title' => $title, 'device' => true, )); } private function loadAuxiliaryFields(DifferentialRevision $revision) { $user = $this->getRequest()->getUser(); $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setRevision($revision); if (!$aux_field->shouldAppearOnEdit()) { unset($aux_fields[$key]); } else { $aux_field->setUser($user); } } return DifferentialAuxiliaryField::loadFromStorage( $revision, $aux_fields); } } diff --git a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php index db29521739..ff5f8f73bc 100644 --- a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php @@ -1,272 +1,274 @@ ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'resolves' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'fix' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'fixes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'wontfixes' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'wontfixed' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'spites' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'spited' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'invalidate' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 'invaldiates' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 'invalidated' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 'close' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'closes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'closed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'ref' => null, 'refs' => null, 'references' => null, 'cf.' => null, ); $suffixes = array( 'as resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'as fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'as wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'as spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'out of spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'as invalid' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, '' => null, ); $prefix_regex = array(); foreach ($prefixes as $prefix => $resolution) { $prefix_regex[] = preg_quote($prefix, '/'); } $prefix_regex = implode('|', $prefix_regex); $suffix_regex = array(); foreach ($suffixes as $suffix => $resolution) { $suffix_regex[] = preg_quote($suffix, '/'); } $suffix_regex = implode('|', $suffix_regex); $matches = null; preg_match_all( "/({$prefix_regex})\s+T(\d+)\s*({$suffix_regex})/i", $message, $matches, PREG_SET_ORDER); $tasks_statuses = array(); foreach ($matches as $set) { $prefix = strtolower($set[1]); $task_id = (int)$set[2]; $suffix = strtolower($set[3]); $status = idx($suffixes, $suffix); if (!$status) { $status = idx($prefixes, $prefix); } $tasks_statuses[$task_id] = $status; } return $tasks_statuses; } private function findDependentRevisions($message) { $dependents = array(); $matches = null; preg_match_all( '/\b(?i:depends\s+on):?\s+D(\d+(,\s+D\d++)*)\b/', $message, $matches); foreach ($matches[1] as $revisions) { foreach (preg_split('/,\s+D/', $revisions) as $id) { $dependents[$id] = $id; } } return $dependents; } public static function findRevertedCommits($message) { $reverts = array(); $matches = null; // NOTE: Git language is "This reverts commit X." // NOTE: Mercurial language is "Backed out changeset Y". $prefixes = array( 'revert' => true, 'reverts' => true, 'back\s*out' => true, 'backs\s*out' => true, 'backed\s*out' => true, 'undo' => true, 'undoes' => true, ); $optional = array( 'commit' => true, 'changeset' => true, 'rev' => true, 'revision' => true, 'change' => true, 'diff' => true, ); $pre_re = implode('|', array_keys($prefixes)); $opt_re = implode('|', array_keys($optional)); $matches = null; preg_match_all( '/\b(?i:'.$pre_re.')(?:\s+(?i:'.$opt_re.'))?([rA-Z0-9a-f,\s]+)\b/', $message, $matches); $result = array(); foreach ($matches[1] as $commits) { $commits = preg_split('/[,\s]+/', $commits); $commits = array_filter($commits); foreach ($commits as $commit) { $result[$commit] = $commit; } } 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 DifferentialRevision()) - ->loadAllWhere('id IN (%Ld)', $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() != ManiphestTaskStatus::STATUS_OPEN) { // Task is already closed. 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/diffusion/conduit/ConduitAPI_diffusion_getcommits_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_getcommits_Method.php index ac5e5d31c8..173699fa80 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_getcommits_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_getcommits_Method.php @@ -1,260 +1,264 @@ 'required list', ); } public function defineReturnType() { return 'nonempty list>'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $results = array(); $commits = $request->getValue('commits'); $commits = array_fill_keys($commits, array()); foreach ($commits as $name => $info) { $matches = null; if (!preg_match('/^r([A-Z]+)([0-9a-f]+)$/', $name, $matches)) { $results[$name] = array( 'error' => 'ERR-UNPARSEABLE', ); unset($commits[$name]); continue; } $commits[$name] = array( 'callsign' => $matches[1], 'commitIdentifier' => $matches[2], ); } if (!$commits) { return $results; } $callsigns = ipull($commits, 'callsign'); $callsigns = array_unique($callsigns); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($request->getUser()) ->withCallsigns($callsigns) ->execute(); $repos = mpull($repos, null, 'getCallsign'); foreach ($commits as $name => $info) { $repo = idx($repos, $info['callsign']); if (!$repo) { $results[$name] = $info + array( 'error' => 'ERR-UNKNOWN-REPOSITORY', ); unset($commits[$name]); continue; } $commits[$name] += array( 'repositoryPHID' => $repo->getPHID(), 'repositoryID' => $repo->getID(), ); } if (!$commits) { return $results; } // Execute a complicated query to figure out the primary commit information // for each referenced commit. $cdata = $this->queryCommitInformation($commits, $repos); // We've built the queries so that each row also has the identifier we used // to select it, which might be a git prefix rather than a full identifier. $ref_map = ipull($cdata, 'commitIdentifier', 'commitRef'); $cobjs = id(new PhabricatorRepositoryCommit())->loadAllFromArray($cdata); $cobjs = mgroup($cobjs, 'getRepositoryID', 'getCommitIdentifier'); foreach ($commits as $name => $commit) { // Expand short git names into full identifiers. For SVN this map is just // the identity. $full_identifier = idx($ref_map, $commit['commitIdentifier']); $repo_id = $commit['repositoryID']; unset($commits[$name]['repositoryID']); if (empty($full_identifier) || empty($cobjs[$commit['repositoryID']][$full_identifier])) { $results[$name] = $commit + array( 'error' => 'ERR-UNKNOWN-COMMIT', ); unset($commits[$name]); continue; } $cobj_arr = $cobjs[$commit['repositoryID']][$full_identifier]; $cobj = head($cobj_arr); $commits[$name] += array( 'epoch' => $cobj->getEpoch(), 'commitPHID' => $cobj->getPHID(), 'commitID' => $cobj->getID(), ); // Upgrade git short references into full commit identifiers. $identifier = $cobj->getCommitIdentifier(); $commits[$name]['commitIdentifier'] = $identifier; $callsign = $commits[$name]['callsign']; $uri = "/r{$callsign}{$identifier}"; $commits[$name]['uri'] = PhabricatorEnv::getProductionURI($uri); } if (!$commits) { return $results; } $commits = $this->addRepositoryCommitDataInformation($commits); $commits = $this->addDifferentialInformation($commits); foreach ($commits as $name => $commit) { $results[$name] = $commit; } return $results; } /** * Retrieve primary commit information for all referenced commits. */ private function queryCommitInformation(array $commits, array $repos) { assert_instances_of($repos, 'PhabricatorRepository'); $conn_r = id(new PhabricatorRepositoryCommit())->establishConnection('r'); $repos = mpull($repos, null, 'getID'); $groups = array(); foreach ($commits as $name => $commit) { $groups[$commit['repositoryID']][] = $commit['commitIdentifier']; } // NOTE: MySQL goes crazy and does a massive table scan if we build a more // sensible version of this query. Make sure the query plan is OK if you // attempt to reduce the craziness here. METANOTE: The addition of prefix // selection for Git further complicates matters. $query = array(); $commit_table = id(new PhabricatorRepositoryCommit())->getTableName(); foreach ($groups as $repository_id => $identifiers) { $vcs = $repos[$repository_id]->getVersionControlSystem(); $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); if ($is_git) { foreach ($identifiers as $identifier) { if (strlen($identifier) < 7) { // Don't bother with silly stuff like 'rX2', which will select // 1/16th of all commits. Note that with length 7 we'll still get // collisions in repositories at the tens-of-thousands-of-commits // scale. continue; } $query[] = qsprintf( $conn_r, 'SELECT %T.*, %s commitRef FROM %T WHERE repositoryID = %d AND commitIdentifier LIKE %>', $commit_table, $identifier, $commit_table, $repository_id, $identifier); } } else { $query[] = qsprintf( $conn_r, 'SELECT %T.*, commitIdentifier commitRef FROM %T WHERE repositoryID = %d AND commitIdentifier IN (%Ls)', $commit_table, $commit_table, $repository_id, $identifiers); } } return queryfx_all( $conn_r, '%Q', implode(' UNION ALL ', $query)); } /** * Enhance the commit list with RepositoryCommitData information. */ private function addRepositoryCommitDataInformation(array $commits) { $commit_ids = ipull($commits, 'commitID'); $data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 'commitID in (%Ld)', $commit_ids); $data = mpull($data, null, 'getCommitID'); foreach ($commits as $name => $commit) { if (isset($data[$commit['commitID']])) { $dobj = $data[$commit['commitID']]; $commits[$name] += array( 'commitMessage' => $dobj->getCommitMessage(), 'commitDetails' => $dobj->getCommitDetails(), ); } // Remove this information so we don't expose it via the API since // external services shouldn't be storing internal Commit IDs. unset($commits[$name]['commitID']); } return $commits; } /** * Enhance the commit list with Differential information. */ private function addDifferentialInformation(array $commits) { $commit_phids = ipull($commits, 'commitPHID'); + // TODO: (T603) This should be policy checked, either by moving to + // DifferentialRevisionQuery or by doing a followup query to make sure + // the matched objects are visible. + $rev_conn_r = id(new DifferentialRevision())->establishConnection('r'); $revs = queryfx_all( $rev_conn_r, 'SELECT r.id id, r.phid phid, c.commitPHID commitPHID FROM %T r JOIN %T c ON r.id = c.revisionID WHERE c.commitPHID in (%Ls)', id(new DifferentialRevision())->getTableName(), DifferentialRevision::TABLE_COMMIT, $commit_phids); $revs = ipull($revs, null, 'commitPHID'); foreach ($commits as $name => $commit) { if (isset($revs[$commit['commitPHID']])) { $rev = $revs[$commit['commitPHID']]; $commits[$name] += array( 'differentialRevisionID' => 'D'.$rev['id'], 'differentialRevisionPHID' => $rev['phid'], ); } } return $commits; } } diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php index 3e6887daf9..279b107889 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php @@ -1,971 +1,972 @@ getRequest(); $drequest = $this->getDiffusionRequest(); $before = $request->getStr('before'); if ($before) { return $this->buildBeforeResponse($before); } $path = $drequest->getPath(); $preferences = $request->getUser()->loadPreferences(); $show_blame = $request->getBool( 'blame', $preferences->getPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME, false)); $show_color = $request->getBool( 'color', $preferences->getPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR, true)); $view = $request->getStr('view'); if ($request->isFormPost() && $view != 'raw') { $preferences->setPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME, $show_blame); $preferences->setPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR, $show_color); $preferences->save(); $uri = $request->getRequestURI() ->alter('blame', null) ->alter('color', null); return id(new AphrontRedirectResponse())->setURI($uri); } // We need the blame information if blame is on and we're building plain // text, or blame is on and this is an Ajax request. If blame is on and // this is a colorized request, we don't show blame at first (we ajax it // in afterward) so we don't need to query for it. $needs_blame = ($show_blame && !$show_color) || ($show_blame && $request->isAjax()); $file_content = DiffusionFileContent::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.filecontentquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'needsBlame' => $needs_blame, ))); $data = $file_content->getCorpus(); if ($view === 'raw') { return $this->buildRawResponse($path, $data); } $this->loadLintMessages(); $binary_uri = null; if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) { $file = $this->loadFileForData($path, $data); $file_uri = $file->getBestURI(); if ($file->isViewableImage()) { $corpus = $this->buildImageCorpus($file_uri); } else { $corpus = $this->buildBinaryCorpus($file_uri, $data); $binary_uri = $file_uri; } } else { // Build the content of the file. $corpus = $this->buildCorpus( $show_blame, $show_color, $file_content, $needs_blame, $drequest, $path, $data); } if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($corpus); } require_celerity_resource('diffusion-source-css'); // Render the page. $content = array(); $content[] = $this->buildHeaderView($drequest); $view = $this->buildActionView($drequest); $content[] = $this->enrichActionView( $view, $drequest, $show_blame, $show_color, $binary_uri); $content[] = $this->buildPropertyView($drequest); $follow = $request->getStr('follow'); if ($follow) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_WARNING); $notice->setTitle(pht('Unable to Continue')); switch ($follow) { case 'first': $notice->appendChild( pht("Unable to continue tracing the history of this file because ". "this commit is the first commit in the repository.")); break; case 'created': $notice->appendChild( pht("Unable to continue tracing the history of this file because ". "this commit created the file.")); break; } $content[] = $notice; } $renamed = $request->getStr('renamed'); if ($renamed) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('File Renamed')); $notice->appendChild( pht("File history passes through a rename from '%s' to '%s'.", $drequest->getPath(), $renamed)); $content[] = $notice; } $content[] = $corpus; $content[] = $this->buildOpenRevisions(); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $basename = basename($this->getDiffusionRequest()->getPath()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => $basename, )); } private function loadLintMessages() { $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); if (!$branch || !$branch->getLintCommit()) { return; } $this->lintCommit = $branch->getLintCommit(); $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = ''; if ($drequest->getLint()) { $where = qsprintf( $conn, 'AND code = %s', $drequest->getLint()); } $this->lintMessages = queryfx_all( $conn, 'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s', PhabricatorRepository::TABLE_LINTMESSAGE, $branch->getID(), $where, '/'.$drequest->getPath()); } private function buildCorpus( $show_blame, $show_color, DiffusionFileContent $file_content, $needs_blame, DiffusionRequest $drequest, $path, $data) { if (!$show_color) { $style = "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; if (!$show_blame) { $corpus = phutil_tag( 'textarea', array( 'style' => $style, ), $file_content->getCorpus()); } else { $text_list = $file_content->getTextList(); $rev_list = $file_content->getRevList(); $blame_dict = $file_content->getBlameDict(); $rows = array(); foreach ($text_list as $k => $line) { $rev = $rev_list[$k]; $author = $blame_dict[$rev]['author']; $rows[] = sprintf("%-10s %-20s %s", substr($rev, 0, 7), $author, $line); } $corpus = phutil_tag( 'textarea', array( 'style' => $style, ), implode("\n", $rows)); } } else { require_celerity_resource('syntax-highlighting-css'); $text_list = $file_content->getTextList(); $rev_list = $file_content->getRevList(); $blame_dict = $file_content->getBlameDict(); $text_list = implode("\n", $text_list); $text_list = PhabricatorSyntaxHighlighter::highlightWithFilename( $path, $text_list); $text_list = explode("\n", $text_list); $rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict, $needs_blame, $drequest, $show_blame, $show_color); $corpus_table = javelin_tag( 'table', array( 'class' => "diffusion-source remarkup-code PhabricatorMonospaced", 'sigil' => 'phabricator-source', ), $rows); if ($this->getRequest()->isAjax()) { return $corpus_table; } $id = celerity_generate_unique_node_id(); $projects = $drequest->loadArcanistProjects(); $langs = array(); foreach ($projects as $project) { $ls = $project->getSymbolIndexLanguages(); if (!$ls) { continue; } $dep_projects = $project->getSymbolIndexProjects(); $dep_projects[] = $project->getPHID(); foreach ($ls as $lang) { if (!isset($langs[$lang])) { $langs[$lang] = array(); } $langs[$lang] += $dep_projects + array($project); } } $lang = last(explode('.', $drequest->getPath())); if (isset($langs[$lang])) { Javelin::initBehavior( 'repository-crossreference', array( 'container' => $id, 'lang' => $lang, 'projects' => $langs[$lang], )); } $corpus = phutil_tag( 'div', array( 'style' => 'padding: 0 2em;', 'id' => $id, ), $corpus_table); Javelin::initBehavior('load-blame', array('id' => $id)); } return $corpus; } private function enrichActionView( PhabricatorActionListView $view, DiffusionRequest $drequest, $show_blame, $show_color, $binary_uri) { $viewer = $this->getRequest()->getUser(); $base_uri = $this->getRequest()->getRequestURI(); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Last Change')) ->setHref( $drequest->generateURI( array( 'action' => 'change', ))) ->setIcon('new')); if ($show_blame) { $blame_text = pht('Disable Blame'); $blame_icon = 'blame-grey'; $blame_value = 0; } else { $blame_text = pht('Enable Blame'); $blame_icon = 'blame'; $blame_value = 1; } $view->addAction( id(new PhabricatorActionView()) ->setName($blame_text) ->setHref($base_uri->alter('blame', $blame_value)) ->setIcon($blame_icon) ->setUser($viewer) ->setRenderAsForm(true)); if ($show_color) { $highlight_text = pht('Disable Highlighting'); $highlight_icon = 'highlight-grey'; $highlight_value = 0; } else { $highlight_text = pht('Enable Highlighting'); $highlight_icon = 'highlight'; $highlight_value = 1; } $view->addAction( id(new PhabricatorActionView()) ->setName($highlight_text) ->setHref($base_uri->alter('color', $highlight_value)) ->setIcon($highlight_icon) ->setUser($viewer) ->setRenderAsForm(true)); $href = null; if ($this->getRequest()->getStr('lint') !== null) { $lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages)); $href = $base_uri->alter('lint', null); } else if ($this->lintCommit === null) { $lint_text = pht('Lint not Available'); } else { $lint_text = pht( 'Show %d Lint Message(s)', count($this->lintMessages)); $href = $this->getDiffusionRequest()->generateURI(array( 'action' => 'browse', 'commit' => $this->lintCommit, ))->alter('lint', ''); } $view->addAction( id(new PhabricatorActionView()) ->setName($lint_text) ->setHref($href) ->setIcon('warning') ->setDisabled(!$href)); if ($binary_uri) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Raw File')) ->setHref($binary_uri) ->setIcon('download')); } else { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View Raw File')) ->setHref($base_uri->alter('view', 'raw')) ->setIcon('file')); } $view->addAction($this->createEditAction()); return $view; } private function createEditAction() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $line = nonempty((int)$drequest->getLine(), 1); $callsign = $repository->getCallsign(); $editor_link = $user->loadEditorLink($path, $line, $callsign); $action = id(new PhabricatorActionView()) ->setName(pht('Open in Editor')) ->setIcon('edit'); $action->setHref($editor_link); $action->setDisabled(!$editor_link); return $action; } private function buildDisplayRows( array $text_list, array $rev_list, array $blame_dict, $needs_blame, DiffusionRequest $drequest, $show_blame, $show_color) { $handles = array(); if ($blame_dict) { $epoch_list = ipull(ifilter($blame_dict, 'epoch'), 'epoch'); $epoch_min = min($epoch_list); $epoch_max = max($epoch_list); $epoch_range = ($epoch_max - $epoch_min) + 1; $author_phids = ipull(ifilter($blame_dict, 'authorPHID'), 'authorPHID'); $handles = $this->loadViewerHandles($author_phids); } $line_arr = array(); $line_str = $drequest->getLine(); $ranges = explode(',', $line_str); foreach ($ranges as $range) { if (strpos($range, '-') !== false) { list($min, $max) = explode('-', $range, 2); $line_arr[] = array( 'min' => min($min, $max), 'max' => max($min, $max), ); } else if (strlen($range)) { $line_arr[] = array( 'min' => $range, 'max' => $range, ); } } $display = array(); $line_number = 1; $last_rev = null; $color = null; foreach ($text_list as $k => $line) { $display_line = array( 'epoch' => null, 'commit' => null, 'author' => null, 'target' => null, 'highlighted' => null, 'line' => $line_number, 'data' => $line, ); if ($show_blame) { // If the line's rev is same as the line above, show empty content // with same color; otherwise generate blame info. The newer a change // is, the more saturated the color. $rev = idx($rev_list, $k, $last_rev); if ($last_rev == $rev) { $display_line['color'] = $color; } else { $blame = $blame_dict[$rev]; if (!isset($blame['epoch'])) { $color = '#ffd'; // Render as warning. } else { $color_ratio = ($blame['epoch'] - $epoch_min) / $epoch_range; $color_value = 0xE6 * (1.0 - $color_ratio); $color = sprintf( '#%02x%02x%02x', $color_value, 0xF6, $color_value); } $display_line['epoch'] = idx($blame, 'epoch'); $display_line['color'] = $color; $display_line['commit'] = $rev; $author_phid = idx($blame, 'authorPHID'); if ($author_phid && $handles[$author_phid]) { $author_link = $handles[$author_phid]->renderLink(); } else { $author_link = phutil_tag( 'span', array( ), $blame['author']); } $display_line['author'] = $author_link; $last_rev = $rev; } } if ($line_arr) { if ($line_number == $line_arr[0]['min']) { $display_line['target'] = true; } foreach ($line_arr as $range) { if ($line_number >= $range['min'] && $line_number <= $range['max']) { $display_line['highlighted'] = true; } } } $display[] = $display_line; ++$line_number; } $commits = array_filter(ipull($display, 'commit')); if ($commits) { $commits = id(new PhabricatorAuditCommitQuery()) ->withIdentifiers($drequest->getRepository()->getID(), $commits) ->needCommitData(true) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); } + $request = $this->getRequest(); + $user = $request->getUser(); + $revision_ids = id(new DifferentialRevision()) ->loadIDsByCommitPHIDs(mpull($commits, 'getPHID')); $revisions = array(); if ($revision_ids) { - $revisions = id(new DifferentialRevision())->loadAllWhere( - 'id IN (%Ld)', - $revision_ids); + $revisions = id(new DifferentialRevisionQuery()) + ->setViewer($user) + ->withIDs($revision_ids) + ->execute(); } - $request = $this->getRequest(); - $user = $request->getUser(); - Javelin::initBehavior('phabricator-oncopy', array()); $engine = null; $inlines = array(); if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) { $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); foreach ($this->lintMessages as $message) { $inline = id(new PhabricatorAuditInlineComment()) ->setID($message['id']) ->setSyntheticAuthor( ArcanistLintSeverity::getStringForSeverity($message['severity']). ' '.$message['code'].' ('.$message['name'].')') ->setLineNumber($message['line']) ->setContent($message['description']); $inlines[$message['line']][] = $inline; $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); require_celerity_resource('differential-changeset-view-css'); } $rows = $this->renderInlines( idx($inlines, 0, array()), ($show_blame), $engine); foreach ($display as $line) { $line_href = $drequest->generateURI( array( 'action' => 'browse', 'line' => $line['line'], 'stable' => true, )); $blame = array(); $style = null; if (array_key_exists('color', $line)) { if ($line['color']) { $style = 'background: '.$line['color'].';'; } $before_link = null; $commit_link = null; $revision_link = null; if (idx($line, 'commit')) { $commit = $line['commit']; $summary = 'Unknown'; if (idx($commits, $commit)) { $summary = $commits[$commit]->getCommitData()->getSummary(); } $tooltip = phabricator_date( $line['epoch'], $user)." \xC2\xB7 ".$summary; Javelin::initBehavior('phabricator-tooltips', array()); require_celerity_resource('aphront-tooltip-css'); $commit_link = javelin_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $line['commit'], )), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), phutil_utf8_shorten($line['commit'], 9, '')); $revision_id = null; if (idx($commits, $commit)) { $revision_id = idx($revision_ids, $commits[$commit]->getPHID()); } if ($revision_id) { $revision = idx($revisions, $revision_id); if (!$revision) { $tooltip = pht('(Invalid revision)'); } else { $tooltip = phabricator_date($revision->getDateModified(), $user). " \xC2\xB7 ". $revision->getTitle(); } $revision_link = javelin_tag( 'a', array( 'href' => '/D'.$revision_id, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), 'D'.$revision_id); } $uri = $line_href->alter('before', $commit); $before_link = javelin_tag( 'a', array( 'href' => $uri->setQueryParam('view', 'blame'), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Skip Past This Commit'), 'align' => 'E', 'size' => 300, ), ), "\xC2\xAB"); } $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-blame-link', 'style' => $style, ), $before_link); $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-rev-link', 'style' => $style, ), $commit_link); $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-rev-link', 'style' => $style, ), $revision_link); $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-author-link', 'style' => $style, ), idx($line, 'author')); } $line_link = phutil_tag( 'a', array( 'href' => $line_href, ), $line['line']); $blame[] = javelin_tag( 'th', array( 'class' => 'diffusion-line-link', 'sigil' => 'phabricator-source-line', 'style' => $style, ), $line_link); Javelin::initBehavior('phabricator-line-linker'); if ($line['target']) { Javelin::initBehavior( 'diffusion-jump-to', array( 'target' => 'scroll_target', )); $anchor_text = phutil_tag( 'a', array( 'id' => 'scroll_target', ), ''); } else { $anchor_text = null; } $blame[] = phutil_tag( 'td', array( ), array( $anchor_text, // NOTE: See phabricator-oncopy behavior. "\xE2\x80\x8B", // TODO: [HTML] Not ideal. phutil_safe_html($line['data']), )); $rows[] = phutil_tag( 'tr', array( 'class' => ($line['highlighted'] ? 'phabricator-source-highlight' : null), ), $blame); $rows = array_merge($rows, $this->renderInlines( idx($inlines, $line['line'], array()), ($show_blame), $engine)); } return $rows; } private function renderInlines(array $inlines, $needs_blame, $engine) { $rows = array(); foreach ($inlines as $inline) { $inline_view = id(new DifferentialInlineCommentView()) ->setMarkupEngine($engine) ->setInlineComment($inline) ->render(); $row = array_fill(0, ($needs_blame ? 5 : 1), phutil_tag('th')); $row[] = phutil_tag('td', array(), $inline_view); $rows[] = phutil_tag('tr', array('class' => 'inline'), $row); } return $rows; } private function loadFileForData($path, $data) { return PhabricatorFile::buildFromFileDataOrHash( $data, array( 'name' => basename($path), 'ttl' => time() + 60 * 60 * 24, )); } private function buildRawResponse($path, $data) { $file = $this->loadFileForData($path, $data); return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } private function buildImageCorpus($file_uri) { $properties = new PhabricatorPropertyListView(); $properties->addProperty( pht('Image'), phutil_tag( 'img', array( 'src' => $file_uri, ))); return $properties; } private function buildBinaryCorpus($file_uri, $data) { $properties = new PhabricatorPropertyListView(); $size = strlen($data); $properties->addTextContent( pht( 'This is a binary file. It is %s byte(s) in length.', new PhutilNumber($size))); return $properties; } private function buildBeforeResponse($before) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); // NOTE: We need to get the grandparent so we can capture filename changes // in the parent. $parent = $this->loadParentRevisionOf($before); $old_filename = null; $was_created = false; if ($parent) { $grandparent = $this->loadParentRevisionOf( $parent->getCommitIdentifier()); if ($grandparent) { $rename_query = new DiffusionRenameHistoryQuery(); $rename_query->setRequest($drequest); $rename_query->setOldCommit($grandparent->getCommitIdentifier()); $rename_query->setViewer($request->getUser()); $old_filename = $rename_query->loadOldFilename(); $was_created = $rename_query->getWasCreated(); } } $follow = null; if ($was_created) { // If the file was created in history, that means older commits won't // have it. Since we know it existed at 'before', it must have been // created then; jump there. $target_commit = $before; $follow = 'created'; } else if ($parent) { // If we found a parent, jump to it. This is the normal case. $target_commit = $parent->getCommitIdentifier(); } else { // If there's no parent, this was probably created in the initial commit? // And the "was_created" check will fail because we can't identify the // grandparent. Keep the user at 'before'. $target_commit = $before; $follow = 'first'; } $path = $drequest->getPath(); $renamed = null; if ($old_filename !== null && $old_filename !== '/'.$path) { $renamed = $path; $path = $old_filename; } $line = null; // If there's a follow error, drop the line so the user sees the message. if (!$follow) { $line = $this->getBeforeLineNumber($target_commit); } $before_uri = $drequest->generateURI( array( 'action' => 'browse', 'commit' => $target_commit, 'line' => $line, 'path' => $path, )); $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); $before_uri = $before_uri->alter('before', null); $before_uri = $before_uri->alter('renamed', $renamed); $before_uri = $before_uri->alter('follow', $follow); return id(new AphrontRedirectResponse())->setURI($before_uri); } private function getBeforeLineNumber($target_commit) { $drequest = $this->getDiffusionRequest(); $line = $drequest->getLine(); if (!$line) { return null; } $raw_diff = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'againstCommit' => $target_commit)); $old_line = 0; $new_line = 0; foreach (explode("\n", $raw_diff) as $text) { if ($text[0] == '-' || $text[0] == ' ') { $old_line++; } if ($text[0] == '+' || $text[0] == ' ') { $new_line++; } if ($new_line == $line) { return $old_line; } } // We didn't find the target line. return $line; } private function loadParentRevisionOf($commit) { $drequest = $this->getDiffusionRequest(); $user = $this->getRequest()->getUser(); $before_req = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $drequest->getRepository(), 'commit' => $commit, )); $parents = DiffusionQuery::callConduitWithDiffusionRequest( $user, $before_req, 'diffusion.commitparentsquery', array( 'commit' => $commit)); return head($parents); } } diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php index 74cdf73a7e..8e07b0163c 100644 --- a/src/applications/herald/adapter/HeraldCommitAdapter.php +++ b/src/applications/herald/adapter/HeraldCommitAdapter.php @@ -1,371 +1,372 @@ pht('Affected packages that need audit'), self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'), self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'), self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'), ) + parent::getFieldNameMap(); } public function getFields() { return array( self::FIELD_BODY, self::FIELD_AUTHOR, self::FIELD_COMMITTER, self::FIELD_REVIEWER, self::FIELD_REPOSITORY, self::FIELD_DIFF_FILE, self::FIELD_DIFF_CONTENT, self::FIELD_RULE, self::FIELD_AFFECTED_PACKAGE, self::FIELD_AFFECTED_PACKAGE_OWNER, self::FIELD_NEED_AUDIT_FOR_PACKAGE, self::FIELD_DIFFERENTIAL_REVISION, self::FIELD_DIFFERENTIAL_REVIEWERS, self::FIELD_DIFFERENTIAL_CCS, ); } public function getConditionsForField($field) { switch ($field) { case self::FIELD_DIFFERENTIAL_REVIEWERS: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_CCS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_REVISION: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return array( self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); } return parent::getConditionsForField($field); } public function getActions($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: return array( self::ACTION_ADD_CC, self::ACTION_EMAIL, self::ACTION_AUDIT, self::ACTION_NOTHING, ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( self::ACTION_ADD_CC, self::ACTION_EMAIL, self::ACTION_FLAG, self::ACTION_AUDIT, self::ACTION_NOTHING, ); } } public function getValueTypeForFieldAndCondition($field, $condition) { switch ($field) { case self::FIELD_DIFFERENTIAL_CCS: return self::VALUE_EMAIL; case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return self::VALUE_OWNERS_PACKAGE; } return parent::getValueTypeForFieldAndCondition($field, $condition); } public static function newLegacyAdapter( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $commit_data) { $object = new HeraldCommitAdapter(); $object->repository = $repository; $object->commit = $commit; $object->commitData = $commit_data; return $object; } public function getPHID() { return $this->commit->getPHID(); } public function getEmailPHIDs() { return array_keys($this->emailPHIDs); } public function getAddCCMap() { return $this->addCCPHIDs; } public function getAuditMap() { return $this->auditMap; } public function getHeraldName() { return 'r'. $this->repository->getCallsign(). $this->commit->getCommitIdentifier(); } public function loadAffectedPaths() { if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->repository, $this->commit, PhabricatorUser::getOmnipotentUser()); $this->affectedPaths = $result; } return $this->affectedPaths; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $this->repository, $this->loadAffectedPaths()); $this->affectedPackages = $packages; } return $this->affectedPackages; } public function loadAuditNeededPackage() { if ($this->auditNeededPackages === null) { $status_arr = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::CONCERNED, ); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( "commitPHID = %s AND auditStatus IN (%Ls)", $this->commit->getPHID(), $status_arr); $packages = mpull($requests, 'getAuditorPHID'); $this->auditNeededPackages = $packages; } return $this->auditNeededPackages; } public function loadDifferentialRevision() { if ($this->affectedRevision === null) { $this->affectedRevision = false; $data = $this->commitData; $revision_id = $data->getCommitDetail('differential.revisionID'); if ($revision_id) { + // TODO: (T603) Herald policy stuff. $revision = id(new DifferentialRevision())->load($revision_id); if ($revision) { $revision->loadRelationships(); $this->affectedRevision = $revision; } } } return $this->affectedRevision; } private function loadCommitDiff() { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => PhabricatorUser::getOmnipotentUser(), 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), )); $raw = DiffusionQuery::callConduitWithDiffusionRequest( PhabricatorUser::getOmnipotentUser(), $drequest, 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), 'timeout' => 60 * 60 * 15, 'linesOfContext' => 0)); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); $diff = DifferentialDiff::newFromRawChanges($changes); return $diff; } public function getHeraldField($field) { $data = $this->commitData; switch ($field) { case self::FIELD_BODY: return $data->getCommitMessage(); case self::FIELD_AUTHOR: return $data->getCommitDetail('authorPHID'); case self::FIELD_COMMITTER: return $data->getCommitDetail('committerPHID'); case self::FIELD_REVIEWER: return $data->getCommitDetail('reviewerPHID'); case self::FIELD_DIFF_FILE: return $this->loadAffectedPaths(); case self::FIELD_REPOSITORY: return $this->repository->getPHID(); case self::FIELD_DIFF_CONTENT: try { $diff = $this->loadCommitDiff(); } catch (Exception $ex) { return array( '<<< Failed to load diff, this may mean the change was '. 'unimaginably enormous. >>>'); } $dict = array(); $lines = array(); $changes = $diff->getChangesets(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { $lines[] = $hunk->makeChanges(); } $dict[$change->getFilename()] = implode("\n", $lines); } return $dict; case self::FIELD_AFFECTED_PACKAGE: $packages = $this->loadAffectedPackages(); return mpull($packages, 'getPHID'); case self::FIELD_AFFECTED_PACKAGE_OWNER: $packages = $this->loadAffectedPackages(); $owners = PhabricatorOwnersOwner::loadAllForPackages($packages); return mpull($owners, 'getUserPHID'); case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return $this->loadAuditNeededPackage(); case self::FIELD_DIFFERENTIAL_REVISION: $revision = $this->loadDifferentialRevision(); if (!$revision) { return null; } return $revision->getID(); case self::FIELD_DIFFERENTIAL_REVIEWERS: $revision = $this->loadDifferentialRevision(); if (!$revision) { return array(); } return $revision->getReviewers(); case self::FIELD_DIFFERENTIAL_CCS: $revision = $this->loadDifferentialRevision(); if (!$revision) { return array(); } return $revision->getCCPHIDs(); } return parent::getHeraldField($field); } public function applyHeraldEffects(array $effects) { assert_instances_of($effects, 'HeraldEffect'); $result = array(); foreach ($effects as $effect) { $action = $effect->getAction(); switch ($action) { case self::ACTION_NOTHING: $result[] = new HeraldApplyTranscript( $effect, true, pht('Great success at doing nothing.')); break; case self::ACTION_EMAIL: foreach ($effect->getTarget() as $phid) { $this->emailPHIDs[$phid] = true; } $result[] = new HeraldApplyTranscript( $effect, true, pht('Added address to email targets.')); break; case self::ACTION_ADD_CC: foreach ($effect->getTarget() as $phid) { if (empty($this->addCCPHIDs[$phid])) { $this->addCCPHIDs[$phid] = array(); } $this->addCCPHIDs[$phid][] = $effect->getRuleID(); } $result[] = new HeraldApplyTranscript( $effect, true, pht('Added address to CC.')); break; case self::ACTION_AUDIT: foreach ($effect->getTarget() as $phid) { if (empty($this->auditMap[$phid])) { $this->auditMap[$phid] = array(); } $this->auditMap[$phid][] = $effect->getRuleID(); } $result[] = new HeraldApplyTranscript( $effect, true, pht('Triggered an audit.')); break; case self::ACTION_FLAG: $result[] = parent::applyFlagEffect( $effect, $this->commit->getPHID()); break; default: throw new Exception("No rules to handle action '{$action}'."); } } return $result; } } diff --git a/src/applications/releeph/commitfinder/ReleephCommitFinder.php b/src/applications/releeph/commitfinder/ReleephCommitFinder.php index 71e4b2c301..1c7bfce23a 100644 --- a/src/applications/releeph/commitfinder/ReleephCommitFinder.php +++ b/src/applications/releeph/commitfinder/ReleephCommitFinder.php @@ -1,84 +1,85 @@ user = $user; return $this; } public function getUser() { return $this->user; } public function setReleephProject(ReleephProject $rp) { $this->releephProject = $rp; return $this; } public function fromPartial($partial_string) { // Look for diffs $matches = array(); if (preg_match('/^D([1-9]\d*)$/', $partial_string, $matches)) { $diff_id = $matches[1]; + // TOOD: (T603) This is all slated for annihilation. $diff_rev = id(new DifferentialRevision())->load($diff_id); if (!$diff_rev) { throw new ReleephCommitFinderException( "{$partial_string} does not refer to an existing diff."); } $commit_phids = $diff_rev->loadCommitPHIDs(); if (!$commit_phids) { throw new ReleephCommitFinderException( "{$partial_string} has no commits associated with it yet."); } $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 'phid IN (%Ls) ORDER BY epoch ASC', $commit_phids); return head($commits); } // Look for a raw commit number, or r. $repository = $this->releephProject->loadPhabricatorRepository(); $dr_data = null; $matches = array(); if (preg_match('/^r(?P[A-Z]+)(?P\w+)$/', $partial_string, $matches)) { $callsign = $matches['callsign']; if ($callsign != $repository->getCallsign()) { throw new ReleephCommitFinderException(sprintf( "%s is in a different repository to this Releeph project (%s).", $partial_string, $repository->getCallsign())); } else { $dr_data = $matches; } } else { $dr_data = array( 'callsign' => $repository->getCallsign(), 'commit' => $partial_string ); } try { $dr_data['user'] = $this->getUser(); $dr = DiffusionRequest::newFromDictionary($dr_data); } catch (Exception $ex) { $message = "No commit matches {$partial_string}: ".$ex->getMessage(); throw new ReleephCommitFinderException($message); } $phabricator_repository_commit = $dr->loadCommit(); if (!$phabricator_repository_commit) { throw new ReleephCommitFinderException( "The commit {$partial_string} doesn't exist in this repository."); } return $phabricator_repository_commit; } } diff --git a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php index 283924599a..65f4e3dc8f 100644 --- a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php +++ b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php @@ -1,99 +1,104 @@ load($diff_rev_id); - if (!$diff_rev) { - throw new Exception(sprintf('D%d not found!', $diff_rev_id)); - } - $this->revision = $diff_rev; + $this->revisionID = $data['diffRevID']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); + $diff_rev = id(new DifferentialRevisionQuery()) + ->setViewer($user) + ->withIDs(array($this->revisionID)) + ->executeOne(); + if (!$diff_rev) { + return new Aphront404Response(); + } + $this->revision = $diff_rev; + $arc_project = id(new PhabricatorRepositoryArcanistProject()) ->loadOneWhere('phid = %s', $this->revision->getArcanistProjectPHID()); $projects = id(new ReleephProject())->loadAllWhere( 'arcanistProjectID = %d AND isActive = 1', $arc_project->getID()); if (!$projects) { throw new Exception(sprintf( "D%d belongs to the '%s' Arcanist project, ". "which is not part of any Releeph project!", $this->revision->getID(), $arc_project->getName())); } $branches = id(new ReleephBranch())->loadAllWhere( 'releephProjectID IN (%Ld) AND isActive = 1', mpull($projects, 'getID')); if (!$branches) { throw new Exception(sprintf( "D%d could be in the Releeph project(s) %s, ". "but this project / none of these projects have open branches.", $this->revision->getID(), implode(', ', mpull($projects, 'getName')))); } if (count($branches) === 1) { return id(new AphrontRedirectResponse()) ->setURI($this->buildReleephRequestURI(head($branches))); } $projects = msort( mpull($projects, null, 'getID'), 'getName'); $branch_groups = mgroup($branches, 'getReleephProjectID'); require_celerity_resource('releeph-request-differential-create-dialog'); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Choose Releeph Branch')) ->setClass('releeph-request-differential-create-dialog') ->addCancelButton('/D'.$request->getStr('D')); $dialog->appendChild( pht("This differential revision changes code that is associated ". "with multiple Releeph branches. ". "Please select the branch where you would like this code to be picked.")); foreach ($branch_groups as $project_id => $branches) { $project = idx($projects, $project_id); $dialog->appendChild( phutil_tag( 'h1', array(), $project->getName())); $branches = msort($branches, 'getBasename'); foreach ($branches as $branch) { $uri = $this->buildReleephRequestURI($branch); $dialog->appendChild( phutil_tag( 'a', array( 'href' => $uri, ), $branch->getDisplayNameWithDetail())); } } return id(new AphrontDialogResponse) ->setDialog($dialog); } private function buildReleephRequestURI(ReleephBranch $branch) { $uri = $branch->getURI('request/'); return id(new PhutilURI($uri)) ->setQueryParam('D', $this->revision->getID()); } } diff --git a/src/applications/releeph/controller/request/ReleephRequestEditController.php b/src/applications/releeph/controller/request/ReleephRequestEditController.php index 5d3871f92e..65532155b8 100644 --- a/src/applications/releeph/controller/request/ReleephRequestEditController.php +++ b/src/applications/releeph/controller/request/ReleephRequestEditController.php @@ -1,304 +1,307 @@ id = idx($data, 'requestID'); parent::willProcessRequest($data); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $releeph_project = $this->getReleephProject(); $releeph_branch = $this->getReleephBranch(); $request_identifier = $request->getStr('requestIdentifierRaw'); $e_request_identifier = true; // Load the RQ we're editing, or create a new one if ($this->id) { $rq = id(new ReleephRequestQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); $is_edit = true; } else { $is_edit = false; $rq = id(new ReleephRequest()) ->setRequestUserPHID($user->getPHID()) ->setBranchID($releeph_branch->getID()) ->setInBranch(0); } // Load all the ReleephFieldSpecifications $selector = $this->getReleephProject()->getReleephFieldSelector(); $fields = $selector->getFieldSpecifications(); foreach ($fields as $field) { $field ->setReleephProject($releeph_project) ->setReleephBranch($releeph_branch) ->setReleephRequest($rq); } $field_list = PhabricatorCustomField::getObjectFields( $rq, PhabricatorCustomField::ROLE_EDIT); foreach ($field_list->getFields() as $field) { $field ->setReleephProject($releeph_project) ->setReleephBranch($releeph_branch) ->setReleephRequest($rq); } $field_list->readFieldsFromStorage($rq); // epriestley: Is it common to pass around a referer URL to // return from whence one came? [...] // If you only have two places, maybe consider some parameter // rather than the full URL. switch ($request->getStr('origin')) { case 'request': $origin_uri = '/RQ'.$rq->getID(); break; case 'branch': default: $origin_uri = $releeph_branch->getURI(); break; } // Make edits $errors = array(); if ($request->isFormPost()) { $xactions = array(); // The commit-identifier being requested... if (!$is_edit) { if ($request_identifier === ReleephRequestTypeaheadControl::PLACEHOLDER) { $errors[] = "No commit ID was provided."; $e_request_identifier = 'Required'; } else { $pr_commit = null; $finder = id(new ReleephCommitFinder()) ->setUser($user) ->setReleephProject($releeph_project); try { $pr_commit = $finder->fromPartial($request_identifier); } catch (Exception $e) { $e_request_identifier = 'Invalid'; $errors[] = "Request {$request_identifier} is probably not a valid commit"; $errors[] = $e->getMessage(); } $pr_commit_data = null; if (!$errors) { $pr_commit_data = $pr_commit->loadCommitData(); if (!$pr_commit_data) { $e_request_identifier = 'Not parsed yet'; $errors[] = "The requested commit hasn't been parsed yet."; } } } if (!$errors) { $existing = id(new ReleephRequest()) ->loadOneWhere('requestCommitPHID = %s AND branchID = %d', $pr_commit->getPHID(), $releeph_branch->getID()); if ($existing) { return id(new AphrontRedirectResponse()) ->setURI('/releeph/request/edit/'.$existing->getID(). '?existing=1'); } $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_REQUEST) ->setNewValue($pr_commit->getPHID()); $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT) // To help hide these implicit intents... ->setMetadataValue('isRQCreate', true) ->setMetadataValue('userPHID', $user->getPHID()) ->setMetadataValue( 'isAuthoritative', $releeph_project->isAuthoritative($user)) ->setNewValue(ReleephRequest::INTENT_WANT); } } // TODO: This should happen implicitly while building transactions // instead. foreach ($field_list->getFields() as $field) { $field->readValueFromRequest($request); } if (!$errors) { foreach ($fields as $field) { if ($field->isEditable()) { try { $data = $request->getRequestData(); $value = idx($data, $field->getRequiredStorageKey()); $field->validate($value); $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_EDIT_FIELD) ->setMetadataValue('fieldClass', get_class($field)) ->setNewValue($value); } catch (ReleephFieldParseException $ex) { $errors[] = $ex->getMessage(); } } } } if (!$errors) { $editor = id(new ReleephRequestTransactionalEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($rq, $xactions); return id(new AphrontRedirectResponse())->setURI($origin_uri); } } $releeph_branch->populateReleephRequestHandles($user, array($rq)); $handles = $rq->getHandles(); $age_string = ''; if ($is_edit) { $age_string = phabricator_format_relative_time( time() - $rq->getDateCreated()) . ' ago'; } // Warn the user if we've been redirected here because we tried to // re-request something. $notice_view = null; if ($request->getInt('existing')) { $notice_messages = array( 'You are editing an existing pick request!', hsprintf( "Requested %s by %s", $age_string, $handles[$rq->getRequestUserPHID()]->renderLink()) ); $notice_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setErrors($notice_messages); } /** * Build the rest of the page */ $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); $error_view->setTitle('Form Errors'); } $form = id(new AphrontFormView()) ->setUser($user); if ($is_edit) { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Original Commit') ->setValue( $handles[$rq->getRequestCommitPHID()]->renderLink())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Requestor') ->setValue(hsprintf( '%s %s', $handles[$rq->getRequestUserPHID()]->renderLink(), $age_string))); } else { $origin = null; $diff_rev_id = $request->getStr('D'); if ($diff_rev_id) { - $diff_rev = id(new DifferentialRevision())->load($diff_rev_id); + $diff_rev = id(new DifferentialRevisionQuery()) + ->setViewer($user) + ->withIDs(array($diff_rev_id)) + ->executeOne(); $origin = '/D'.$diff_rev->getID(); $title = sprintf( 'D%d: %s', $diff_rev_id, $diff_rev->getTitle()); $form ->addHiddenInput('requestIdentifierRaw', 'D'.$diff_rev_id) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Diff') ->setValue($title)); } else { $origin = $releeph_branch->getURI(); $repo = $releeph_project->loadPhabricatorRepository(); $branch_cut_point = id(new PhabricatorRepositoryCommit()) ->loadOneWhere( 'phid = %s', $releeph_branch->getCutPointCommitPHID()); $form->appendChild( id(new ReleephRequestTypeaheadControl()) ->setName('requestIdentifierRaw') ->setLabel('Commit ID') ->setRepo($repo) ->setValue($request_identifier) ->setError($e_request_identifier) ->setStartTime($branch_cut_point->getEpoch()) ->setCaption( 'Start typing to autocomplete on commit title, '. 'or give a Phabricator commit identifier like rFOO1234')); } } $field_list->appendFieldsToForm($form); $crumbs = $this->buildApplicationCrumbs(); if ($is_edit) { $title = pht('Edit Releeph Request'); $submit_name = pht('Save'); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName('RQ'.$rq->getID()) ->setHref('/RQ'.$rq->getID())); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName(pht('Edit'))); } else { $title = pht('Create Releeph Request'); $submit_name = pht('Create'); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName(pht('New Request'))); } $form->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($origin_uri, 'Cancel') ->setValue($submit_name)); return $this->buildApplicationPage( array( $crumbs, $notice_view, $error_view, $form, ), array( 'title' => $title, )); } } diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index 00beab892c..f8c6de032e 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -1,330 +1,331 @@ getPusherIntent() == self::INTENT_WANT && /** * We use "!= pass" instead of "== want" in case the requestor intent is * not present. In other words, only revert if the requestor explicitly * passed. */ $this->getRequestorIntent() != self::INTENT_PASS; } /** * Will return INTENT_WANT if any pusher wants this request, and no pusher * passes on this request. */ public function getPusherIntent() { $project = $this->loadReleephProject(); if (!$project->getPushers()) { return self::INTENT_WANT; } $found_pusher_want = false; foreach ($this->userIntents as $phid => $intent) { if ($project->isAuthoritativePHID($phid)) { if ($intent == self::INTENT_PASS) { return self::INTENT_PASS; } $found_pusher_want = true; } } if ($found_pusher_want) { return self::INTENT_WANT; } else { return null; } } public function getRequestorIntent() { return idx($this->userIntents, $this->requestUserPHID); } public function getStatus() { return $this->calculateStatus(); } private function calculateStatus() { if ($this->shouldBeInBranch()) { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_PICKED; } else { return ReleephRequestStatus::STATUS_NEEDS_PICK; } } else { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_NEEDS_REVERT; } else { $has_been_in_branch = $this->getCommitIdentifier(); // Regardless of why we reverted something, always say reverted if it // was once in the branch. if ($has_been_in_branch) { return ReleephRequestStatus::STATUS_REVERTED; } elseif ($this->getPusherIntent() === ReleephRequest::INTENT_PASS) { // Otherwise, if it has never been in the branch, explicitly say why: return ReleephRequestStatus::STATUS_REJECTED; } elseif ($this->getRequestorIntent() === ReleephRequest::INTENT_WANT) { return ReleephRequestStatus::STATUS_REQUESTED; } else { return ReleephRequestStatus::STATUS_ABANDONED; } } } } /* -( Lisk mechanics )----------------------------------------------------- */ public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'userIntents' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( ReleephPHIDTypeRequest::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( Helpful accessors )--------------------------------------------------- */ public function setHandles($handles) { $this->handles = $handles; return $this; } public function getHandles() { return $this->assertAttached($this->handles); } public function getDetail($key, $default = null) { return idx($this->getDetails(), $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getReason() { // Backward compatibility: reason used to be called comments $reason = $this->getDetail('reason'); if (!$reason) { return $this->getDetail('comments'); } return $reason; } public function getSummary() { /** * Instead, you can use: * - getDetail('summary') // the actual user-chosen summary * - getSummaryForDisplay() // falls back to the original commit title * * Or for the fastidious: * - id(new ReleephSummaryFieldSpecification()) * ->setReleephRequest($rr) * ->getValue() // programmatic equivalent to getDetail() */ throw new Exception( "getSummary() has been deprecated!"); } /** * Allow a null summary, and fall back to the title of the commit. */ public function getSummaryForDisplay() { $summary = $this->getDetail('summary'); if (!$summary) { $pr_commit_data = $this->loadPhabricatorRepositoryCommitData(); if ($pr_commit_data) { $message_lines = explode("\n", $pr_commit_data->getCommitMessage()); $message_lines = array_filter($message_lines); $summary = head($message_lines); } } if (!$summary) { $summary = '(no summary given and commit message empty or unparsed)'; } return $summary; } public function loadRequestCommitDiffPHID() { $phids = array(); $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $commit->getPHID(), PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV); } return head($phids); } /* -( Loading external objects )------------------------------------------- */ public function loadReleephBranch() { return $this->loadOneRelative( new ReleephBranch(), 'id', 'getBranchID'); } public function loadReleephProject() { return $this->loadReleephBranch()->loadReleephProject(); } public function loadPhabricatorRepositoryCommit() { return $this->loadOneRelative( new PhabricatorRepositoryCommit(), 'phid', 'getRequestCommitPHID'); } public function loadPhabricatorRepositoryCommitData() { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { return $commit->loadOneRelative( new PhabricatorRepositoryCommitData(), 'commitID'); } } + // TODO: (T603) Get rid of all this one-off ad-hoc loading. public function loadDifferentialRevision() { $diff_phid = $this->loadRequestCommitDiffPHID(); if (!$diff_phid) { return null; } return $this->loadOneRelative( new DifferentialRevision(), 'phid', 'loadRequestCommitDiffPHID'); } /* -( State change helpers )----------------------------------------------- */ public function setUserIntent(PhabricatorUser $user, $intent) { $this->userIntents[$user->getPHID()] = $intent; return $this; } /* -( Migrating to status-less ReleephRequests )--------------------------- */ protected function didReadData() { if ($this->userIntents === null) { $this->userIntents = array(); } } public function setStatus($value) { throw new Exception('`status` is now deprecated!'); } /* -( Make magic Lisk methods private )------------------------------------ */ private function setUserIntents(array $ar) { return parent::setUserIntents($ar); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('releeph.fields'); } public function getCustomFieldBaseClass() { return 'ReleephFieldSpecification'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index 9c06305e75..78d062d223 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -1,127 +1,129 @@ loadAllWhere( 'commitPHID = %s', $commit->getPHID()); $requests = mpull($requests, null, 'getAuditorPHID'); foreach ($affected_packages as $package) { $request = idx($requests, $package->getPHID()); if ($request) { // Don't update request if it exists already. continue; } if ($package->getAuditingEnabled()) { $reasons = $this->checkAuditReasons($commit, $package); if ($reasons) { $audit_status = PhabricatorAuditStatusConstants::AUDIT_REQUIRED; } else { $audit_status = PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED; } } else { $reasons = array(); $audit_status = PhabricatorAuditStatusConstants::NONE; } $relationship = new PhabricatorRepositoryAuditRequest(); $relationship->setAuditorPHID($package->getPHID()); $relationship->setCommitPHID($commit->getPHID()); $relationship->setAuditReasons($reasons); $relationship->setAuditStatus($audit_status); $relationship->save(); $requests[$package->getPHID()] = $relationship; } $commit->updateAuditStatus($requests); $commit->save(); } if ($this->shouldQueueFollowupTasks()) { PhabricatorWorker::scheduleTask( 'PhabricatorRepositoryCommitHeraldWorker', array( 'commitID' => $commit->getID(), )); } } private function checkAuditReasons( PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package) { $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); $reasons = array(); if ($data->getCommitDetail('vsDiff')) { $reasons[] = "Changed After Revision Was Accepted"; } $commit_author_phid = $data->getCommitDetail('authorPHID'); if (!$commit_author_phid) { $reasons[] = "Commit Author Not Recognized"; } $revision_id = $data->getCommitDetail('differential.revisionID'); $revision_author_phid = null; $commit_reviewedby_phid = null; if ($revision_id) { + // TODO: (T603) This is probably safe to use an omnipotent user on, + // but check things more closely. $revision = id(new DifferentialRevision())->load($revision_id); if ($revision) { $revision_author_phid = $revision->getAuthorPHID(); $revision_reviewedby_phid = $revision->loadReviewedBy(); $commit_reviewedby_phid = $data->getCommitDetail('reviewerPHID'); if ($revision_author_phid !== $commit_author_phid) { $reasons[] = "Author Not Matching with Revision"; } if ($revision_reviewedby_phid !== $commit_reviewedby_phid) { $reasons[] = "ReviewedBy Not Matching with Revision"; } } else { $reasons[] = "Revision Not Found"; } } else { $reasons[] = "No Revision Specified"; } $owners_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( array($package->getID())); if (!($commit_author_phid && in_array($commit_author_phid, $owners_phids) || $commit_reviewedby_phid && in_array($commit_reviewedby_phid, $owners_phids))) { $reasons[] = "Owners Not Involved"; } return $reasons; } } diff --git a/src/applications/search/controller/PhabricatorSearchSelectController.php b/src/applications/search/controller/PhabricatorSearchSelectController.php index cab9a98675..cf0f4e1aa0 100644 --- a/src/applications/search/controller/PhabricatorSearchSelectController.php +++ b/src/applications/search/controller/PhabricatorSearchSelectController.php @@ -1,117 +1,118 @@ type = $data['type']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $query = new PhabricatorSearchQuery(); $query_str = $request->getStr('query'); $query->setQuery($query_str); $query->setParameter('type', $this->type); switch ($request->getStr('filter')) { case 'assigned': $query->setParameter('owner', array($user->getPHID())); $query->setParameter('open', 1); break; case 'created'; $query->setParameter('author', array($user->getPHID())); // TODO - if / when we allow pholio mocks to be archived, etc // update this if ($this->type != PholioPHIDTypeMock::TYPECONST) { $query->setParameter('open', 1); } break; case 'open': $query->setParameter('open', 1); break; } $query->setParameter('exclude', $request->getStr('exclude')); $query->setParameter('limit', 100); $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); $results = $engine->executeSearch($query); $phids = array_fill_keys($results, true); $phids += $this->queryObjectNames($query_str); $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $data = array(); foreach ($handles as $handle) { $view = new PhabricatorHandleObjectSelectorDataView($handle); $data[] = $view->renderData(); } return id(new AphrontAjaxResponse())->setContent($data); } private function queryObjectNames($query) { $pattern = null; switch ($this->type) { case ManiphestPHIDTypeTask::TYPECONST: $pattern = '/\bT(\d+)\b/i'; break; case DifferentialPHIDTypeRevision::TYPECONST: $pattern = '/\bD(\d+)\b/i'; break; case PholioPHIDTypeMock::TYPECONST: $pattern = '/\bM(\d+)\b/i'; break; } if (!$pattern) { return array(); } $matches = array(); preg_match_all($pattern, $query, $matches); if (!$matches) { return array(); } $object_ids = $matches[1]; if (!$object_ids) { return array(); } switch ($this->type) { case DifferentialPHIDTypeRevision::TYPECONST: + // TODO: (T603) See below. This whole thing needs cleanup. $objects = id(new DifferentialRevision())->loadAllWhere( 'id IN (%Ld)', $object_ids); break; case ManiphestPHIDTypeTask::TYPECONST: // TODO: (T603) Clean this up. This should probably all run through // ObjectQuery? $objects = id(new ManiphestTask())->loadAllWhere( 'id IN (%Ld)', $object_ids); break; case PholioPHIDTypeMock::TYPECONST: $objects = id(new PholioMock())->loadAllWhere( 'id IN (%Ld)', $object_ids); break; } return array_fill_keys(mpull($objects, 'getPHID'), true); } }