diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 59c645c746..0d0a135e76 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1,982 +1,983 @@ allowsAnonymousAccess()) { return false; } return parent::shouldRequireLogin(); } public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->revisionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $viewer_is_anonymous = !$user->isLoggedIn(); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) ->setViewer($request->getUser()) ->needRelationships(true) ->needReviewerStatus(true) + ->needReviewerAuthority(true) ->executeOne(); if (!$revision) { return new Aphront404Response(); } $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withRevisionIDs(array($this->revisionID)) ->execute(); $diffs = array_reverse($diffs, $preserve_keys = true); if (!$diffs) { throw new Exception( "This revision has no diffs. Something has gone quite wrong."); } $diff_vs = $request->getInt('vs'); $target_id = $request->getInt('id'); $target = idx($diffs, $target_id, end($diffs)); $target_manual = $target; if (!$target_id) { foreach ($diffs as $diff) { if ($diff->getCreationMethod() != 'commit') { $target_manual = $diff; } } } if (empty($diffs[$diff_vs])) { $diff_vs = null; } $arc_project = $target->loadArcanistProject(); $repository = ($arc_project ? $arc_project->loadRepository() : null); list($changesets, $vs_map, $vs_changesets, $rendering_references) = $this->loadChangesetsAndVsMap( $target, idx($diffs, $diff_vs), $repository); if ($request->getExists('download')) { return $this->buildRawDiffResponse( $changesets, $vs_changesets, $vs_map, $repository); } $props = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $target_manual->getID()); $props = mpull($props, 'getData', 'getName'); $aux_fields = $this->loadAuxiliaryFields($revision); $comments = $revision->loadComments(); $comments = array_merge( $this->getImplicitComments($revision, reset($diffs)), $comments); $all_changesets = $changesets; $inlines = $this->loadInlineComments( $revision, $all_changesets); $object_phids = array_merge( $revision->getReviewers(), $revision->getCCPHIDs(), $revision->loadCommitPHIDs(), array( $revision->getAuthorPHID(), $user->getPHID(), ), mpull($comments, 'getAuthorPHID')); foreach ($comments as $comment) { $metadata = $comment->getMetadata(); $added_reviewers = idx( $metadata, DifferentialComment::METADATA_ADDED_REVIEWERS); if ($added_reviewers) { foreach ($added_reviewers as $phid) { $object_phids[] = $phid; } } $added_ccs = idx( $metadata, DifferentialComment::METADATA_ADDED_CCS); if ($added_ccs) { foreach ($added_ccs as $phid) { $object_phids[] = $phid; } } } foreach ($revision->getAttached() as $type => $phids) { foreach ($phids as $phid => $info) { $object_phids[] = $phid; } } $aux_phids = array(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setDiff($target); $aux_field->setManualDiff($target_manual); $aux_field->setDiffProperties($props); $aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionView(); } $object_phids = array_merge($object_phids, array_mergev($aux_phids)); $object_phids = array_unique($object_phids); $handles = $this->loadViewerHandles($object_phids); foreach ($aux_fields as $key => $aux_field) { // Make sure each field only has access to handles it specifically // requested, not all handles. Otherwise you can get a field which works // only in the presence of other fields. $aux_field->setHandles(array_select_keys($handles, $aux_phids[$key])); } $reviewer_warning = null; if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { $has_live_reviewer = false; foreach ($revision->getReviewers() as $reviewer) { if (!$handles[$reviewer]->isDisabled()) { $has_live_reviewer = true; break; } } if (!$has_live_reviewer) { $reviewer_warning = new AphrontErrorView(); $reviewer_warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $reviewer_warning->setTitle(pht('No Active Reviewers')); if ($revision->getReviewers()) { $reviewer_warning->appendChild( phutil_tag( 'p', array(), pht('All specified reviewers are disabled and this revision '. 'needs review. You may want to add some new reviewers.'))); } else { $reviewer_warning->appendChild( phutil_tag( 'p', array(), pht('This revision has no specified reviewers and needs '. 'review. You may want to add some reviewers.'))); } } } $request_uri = $request->getRequestURI(); $limit = 100; $large = $request->getStr('large'); if (count($changesets) > $limit && !$large) { $count = count($changesets); $warning = new AphrontErrorView(); $warning->setTitle('Very Large Diff'); $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $warning->appendChild(hsprintf( '%s %s', pht( 'This diff is very large and affects %s files. Load each file '. 'individually.', new PhutilNumber($count)), phutil_tag( 'a', array( 'href' => $request_uri ->alter('large', 'true') ->setFragment('toc'), ), pht('Show All Files Inline')))); $warning = $warning->render(); $my_inlines = id(new DifferentialInlineCommentQuery()) ->withDraftComments($user->getPHID(), $this->revisionID) ->execute(); $visible_changesets = array(); foreach ($inlines + $my_inlines as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { $visible_changesets[$changeset_id] = $changesets[$changeset_id]; } } if (!empty($props['arc:lint'])) { $changeset_paths = mpull($changesets, null, 'getFilename'); foreach ($props['arc:lint'] as $lint) { $changeset = idx($changeset_paths, $lint['path']); if ($changeset) { $visible_changesets[$changeset->getID()] = $changeset; } } } } else { $warning = null; $visible_changesets = $changesets; } $revision_detail = id(new DifferentialRevisionDetailView()) ->setUser($user) ->setRevision($revision) ->setDiff(end($diffs)) ->setAuxiliaryFields($aux_fields) ->setURI($request->getRequestURI()); $actions = $this->getRevisionActions($revision); $custom_renderer_class = PhabricatorEnv::getEnvConfig( 'differential.revision-custom-detail-renderer'); if ($custom_renderer_class) { // TODO: build a better version of the action links and deprecate the // whole DifferentialRevisionDetailRenderer class. $custom_renderer = newv($custom_renderer_class, array()); $custom_renderer->setUser($user); $custom_renderer->setDiff($target); if ($diff_vs) { $custom_renderer->setVSDiff($diffs[$diff_vs]); } $actions = array_merge( $actions, $custom_renderer->generateActionLinks($revision, $target_manual)); } $whitespace = $request->getStr( 'whitespace', DifferentialChangesetParser::WHITESPACE_IGNORE_ALL); if ($arc_project) { list($symbol_indexes, $project_phids) = $this->buildSymbolIndexes( $arc_project, $visible_changesets); } else { $symbol_indexes = array(); $project_phids = null; } $revision_detail->setActions($actions); $revision_detail->setUser($user); $comment_view = new DifferentialRevisionCommentListView(); $comment_view->setComments($comments); $comment_view->setHandles($handles); $comment_view->setInlineComments($inlines); $comment_view->setChangesets($all_changesets); $comment_view->setUser($user); $comment_view->setTargetDiff($target); $comment_view->setVersusDiffID($diff_vs); if ($arc_project) { Javelin::initBehavior( 'repository-crossreference', array( 'section' => $comment_view->getID(), 'projects' => $project_phids, )); } $changeset_view = new DifferentialChangesetListView(); $changeset_view->setChangesets($changesets); $changeset_view->setVisibleChangesets($visible_changesets); if (!$viewer_is_anonymous) { $changeset_view->setInlineCommentControllerURI( '/differential/comment/inline/edit/'.$revision->getID().'/'); } $changeset_view->setStandaloneURI('/differential/changeset/'); $changeset_view->setRawFileURIs( '/differential/changeset/?view=old', '/differential/changeset/?view=new'); $changeset_view->setUser($user); $changeset_view->setDiff($target); $changeset_view->setRenderingReferences($rendering_references); $changeset_view->setVsMap($vs_map); $changeset_view->setWhitespace($whitespace); if ($repository) { $changeset_view->setRepository($repository); } $changeset_view->setSymbolIndexes($symbol_indexes); $changeset_view->setTitle('Diff '.$target->getID()); $diff_history = new DifferentialRevisionUpdateHistoryView(); $diff_history->setDiffs($diffs); $diff_history->setSelectedVersusDiffID($diff_vs); $diff_history->setSelectedDiffID($target->getID()); $diff_history->setSelectedWhitespace($whitespace); $diff_history->setUser($user); $local_view = new DifferentialLocalCommitsView(); $local_view->setUser($user); $local_view->setLocalCommits(idx($props, 'local:commits')); if ($repository) { $other_revisions = $this->loadOtherRevisions( $changesets, $target, $repository); } else { $other_revisions = array(); } $other_view = null; if ($other_revisions) { $other_view = $this->renderOtherRevisions($other_revisions); } $toc_view = new DifferentialDiffTableOfContentsView(); $toc_view->setChangesets($changesets); $toc_view->setVisibleChangesets($visible_changesets); $toc_view->setRenderingReferences($rendering_references); $toc_view->setUnitTestData(idx($props, 'arc:unit', array())); if ($repository) { $toc_view->setRepository($repository); } $toc_view->setDiff($target); $toc_view->setUser($user); $toc_view->setRevisionID($revision->getID()); $toc_view->setWhitespace($whitespace); $comment_form = null; if (!$viewer_is_anonymous) { $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'differential-comment-'.$revision->getID()); $reviewers = array(); $ccs = array(); if ($draft) { $reviewers = idx($draft->getMetadata(), 'reviewers', array()); $ccs = idx($draft->getMetadata(), 'ccs', array()); if ($reviewers || $ccs) { $handles = $this->loadViewerHandles(array_merge($reviewers, $ccs)); $reviewers = array_select_keys($handles, $reviewers); $ccs = array_select_keys($handles, $ccs); } } $comment_form = new DifferentialAddCommentView(); $comment_form->setRevision($revision); $comment_form->setAuxFields($aux_fields); $comment_form->setActions($this->getRevisionCommentActions($revision)); $comment_form->setActionURI('/differential/comment/save/'); $comment_form->setUser($user); $comment_form->setDraft($draft); $comment_form->setReviewers(mpull($reviewers, 'getFullName', 'getPHID')); $comment_form->setCCs(mpull($ccs, 'getFullName', 'getPHID')); } $pane_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'differential-keyboard-navigation', array( 'haunt' => $pane_id, )); Javelin::initBehavior('differential-user-select'); $page_pane = id(new DifferentialPrimaryPaneView()) ->setID($pane_id) ->appendChild(array( $comment_view->render(), $diff_history->render(), $warning, $local_view->render(), $toc_view->render(), $other_view, $changeset_view->render(), )); if ($comment_form) { $page_pane->appendChild($comment_form->render()); } else { // TODO: For now, just use this to get "Login to Comment". $page_pane->appendChild( id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setRequestURI($request->getRequestURI())); } $object_id = 'D'.$revision->getID(); $top_anchor = id(new PhabricatorAnchorView()) ->setAnchorName('top') ->setNavigationMarker(true); $content = array( $reviewer_warning, $top_anchor, $revision_detail, $page_pane, ); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($object_id) ->setHref('/'.$object_id)); $prefs = $user->loadPreferences(); $pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE; if ($prefs->getPreference($pref_filetree)) { $collapsed = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED, false); $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setAnchorName('top') ->setTitle('D'.$revision->getID()) ->setBaseURI(new PhutilURI('/D'.$revision->getID())) ->setCollapsed((bool)$collapsed) ->build($changesets); $nav->appendChild($content); $nav->setCrumbs($crumbs); $content = $nav; } else { array_unshift($content, $crumbs); } return $this->buildApplicationPage( $content, array( 'title' => $object_id.' '.$revision->getTitle(), 'pageObjects' => array($revision->getPHID()), )); } private function getImplicitComments( DifferentialRevision $revision, DifferentialDiff $diff) { $author_phid = nonempty( $diff->getAuthorPHID(), $revision->getAuthorPHID()); $template = new DifferentialComment(); $template->setAuthorPHID($author_phid); $template->setRevisionID($revision->getID()); $template->setDateCreated($revision->getDateCreated()); $comments = array(); if (strlen($revision->getSummary())) { $summary_comment = clone $template; $summary_comment->setContent($revision->getSummary()); $summary_comment->setAction(DifferentialAction::ACTION_SUMMARIZE); $comments[] = $summary_comment; } if (strlen($revision->getTestPlan())) { $testplan_comment = clone $template; $testplan_comment->setContent($revision->getTestPlan()); $testplan_comment->setAction(DifferentialAction::ACTION_TESTPLAN); $comments[] = $testplan_comment; } return $comments; } private function getRevisionActions(DifferentialRevision $revision) { $user = $this->getRequest()->getUser(); $viewer_phid = $user->getPHID(); $viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs()); $logged_in = $this->getRequest()->getUser()->isLoggedIn(); $status = $revision->getStatus(); $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); $links = array(); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $revision, PhabricatorPolicyCapability::CAN_EDIT); $links[] = array( 'icon' => 'edit', 'href' => "/differential/revision/edit/{$revision_id}/", 'name' => pht('Edit Revision'), 'disabled' => !$can_edit, 'sigil' => $can_edit ? null : 'workflow', ); if (!$viewer_is_owner && !$viewer_is_reviewer) { $action = $viewer_is_cc ? 'rem' : 'add'; $links[] = array( 'icon' => $viewer_is_cc ? 'disable' : 'check', 'href' => "/differential/subscribe/{$action}/{$revision_id}/", 'name' => $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe'), 'instant' => $logged_in, 'disabled' => !$logged_in, 'sigil' => $can_edit ? null : 'workflow', ); } else { $links[] = array( 'icon' => 'enable', 'name' => pht('Automatically Subscribed'), 'disabled' => true, ); } require_celerity_resource('phabricator-object-selector-css'); require_celerity_resource('javelin-behavior-phabricator-object-selector'); $links[] = array( 'icon' => 'link', 'name' => pht('Edit Dependencies'), 'href' => "/search/attach/{$revision_phid}/DREV/dependencies/", 'sigil' => 'workflow', 'disabled' => !$can_edit, ); $maniphest = 'PhabricatorApplicationManiphest'; if (PhabricatorApplication::isClassInstalled($maniphest)) { $links[] = array( 'icon' => 'attach', 'name' => pht('Edit Maniphest Tasks'), 'href' => "/search/attach/{$revision_phid}/TASK/", 'sigil' => 'workflow', 'disabled' => !$can_edit, ); } $request_uri = $this->getRequest()->getRequestURI(); $links[] = array( 'icon' => 'download', 'name' => pht('Download Raw Diff'), 'href' => $request_uri->alter('download', 'true') ); return $links; } private function getRevisionCommentActions(DifferentialRevision $revision) { $actions = array( DifferentialAction::ACTION_COMMENT => true, ); $viewer = $this->getRequest()->getUser(); $viewer_phid = $viewer->getPHID(); $viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID()); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); $viewer_did_accept = ($viewer_phid === $revision->loadReviewedBy()); $status = $revision->getStatus(); $allow_self_accept = PhabricatorEnv::getEnvConfig( 'differential.allow-self-accept'); $always_allow_close = PhabricatorEnv::getEnvConfig( 'differential.always-allow-close'); $allow_reopen = PhabricatorEnv::getEnvConfig( 'differential.allow-reopen'); if ($viewer_is_owner) { switch ($status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept; $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_RETHINK] = true; break; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept; $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_REQUEST] = true; break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_REQUEST] = true; $actions[DifferentialAction::ACTION_RETHINK] = true; $actions[DifferentialAction::ACTION_CLOSE] = true; break; case ArcanistDifferentialRevisionStatus::CLOSED: break; case ArcanistDifferentialRevisionStatus::ABANDONED: $actions[DifferentialAction::ACTION_RECLAIM] = true; break; } } else { switch ($status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_REJECT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_REJECT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer && !$viewer_did_accept; break; case ArcanistDifferentialRevisionStatus::CLOSED: case ArcanistDifferentialRevisionStatus::ABANDONED: break; } if ($status != ArcanistDifferentialRevisionStatus::CLOSED) { $actions[DifferentialAction::ACTION_CLAIM] = true; $actions[DifferentialAction::ACTION_CLOSE] = $always_allow_close; } } $actions[DifferentialAction::ACTION_ADDREVIEWERS] = true; $actions[DifferentialAction::ACTION_ADDCCS] = true; $actions[DifferentialAction::ACTION_REOPEN] = $allow_reopen && ($status == ArcanistDifferentialRevisionStatus::CLOSED); $actions = array_keys(array_filter($actions)); $actions_dict = array(); foreach ($actions as $action) { $actions_dict[$action] = DifferentialAction::getActionVerb($action); } return $actions_dict; } private function loadInlineComments( DifferentialRevision $revision, array &$changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $inline_comments = array(); $inline_comments = id(new DifferentialInlineCommentQuery()) ->withRevisionIDs(array($revision->getID())) ->withNotDraft(true) ->execute(); $load_changesets = array(); foreach ($inline_comments as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { continue; } $load_changesets[$changeset_id] = true; } $more_changesets = array(); if ($load_changesets) { $changeset_ids = array_keys($load_changesets); $more_changesets += id(new DifferentialChangeset()) ->loadAllWhere( 'id IN (%Ld)', $changeset_ids); } if ($more_changesets) { $changesets += $more_changesets; $changesets = msort($changesets, 'getSortKey'); } return $inline_comments; } private function loadChangesetsAndVsMap( DifferentialDiff $target, DifferentialDiff $diff_vs = null, PhabricatorRepository $repository = null) { $load_ids = array(); if ($diff_vs) { $load_ids[] = $diff_vs->getID(); } $load_ids[] = $target->getID(); $raw_changesets = id(new DifferentialChangeset()) ->loadAllWhere( 'diffID IN (%Ld)', $load_ids); $changeset_groups = mgroup($raw_changesets, 'getDiffID'); $changesets = idx($changeset_groups, $target->getID(), array()); $changesets = mpull($changesets, null, 'getID'); $refs = array(); $vs_map = array(); $vs_changesets = array(); if ($diff_vs) { $vs_id = $diff_vs->getID(); $vs_changesets_path_map = array(); foreach (idx($changeset_groups, $vs_id, array()) as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs); $vs_changesets_path_map[$path] = $changeset; $vs_changesets[$changeset->getID()] = $changeset; } foreach ($changesets as $key => $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $target); if (isset($vs_changesets_path_map[$path])) { $vs_map[$changeset->getID()] = $vs_changesets_path_map[$path]->getID(); $refs[$changeset->getID()] = $changeset->getID().'/'.$vs_changesets_path_map[$path]->getID(); unset($vs_changesets_path_map[$path]); } else { $refs[$changeset->getID()] = $changeset->getID(); } } foreach ($vs_changesets_path_map as $path => $changeset) { $changesets[$changeset->getID()] = $changeset; $vs_map[$changeset->getID()] = -1; $refs[$changeset->getID()] = $changeset->getID().'/-1'; } } else { foreach ($changesets as $changeset) { $refs[$changeset->getID()] = $changeset->getID(); } } $changesets = msort($changesets, 'getSortKey'); return array($changesets, $vs_map, $vs_changesets, $refs); } private function loadAuxiliaryFields(DifferentialRevision $revision) { $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { if (!$aux_field->shouldAppearOnRevisionView()) { unset($aux_fields[$key]); } else { $aux_field->setUser($this->getRequest()->getUser()); } } $aux_fields = DifferentialAuxiliaryField::loadFromStorage( $revision, $aux_fields); return $aux_fields; } private function buildSymbolIndexes( PhabricatorRepositoryArcanistProject $arc_project, array $visible_changesets) { assert_instances_of($visible_changesets, 'DifferentialChangeset'); $engine = PhabricatorSyntaxHighlighter::newEngine(); $langs = $arc_project->getSymbolIndexLanguages(); if (!$langs) { return array(array(), array()); } $symbol_indexes = array(); $project_phids = array_merge( array($arc_project->getPHID()), nonempty($arc_project->getSymbolIndexProjects(), array())); $indexed_langs = array_fill_keys($langs, true); foreach ($visible_changesets as $key => $changeset) { $lang = $engine->getLanguageFromFilename($changeset->getFilename()); if (isset($indexed_langs[$lang])) { $symbol_indexes[$key] = array( 'lang' => $lang, 'projects' => $project_phids, ); } } return array($symbol_indexes, $project_phids); } private function loadOtherRevisions( array $changesets, DifferentialDiff $target, PhabricatorRepository $repository) { assert_instances_of($changesets, 'DifferentialChangeset'); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $changeset->getAbsoluteRepositoryPath( $repository, $target); } if (!$paths) { return array(); } $path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs(); if (!$path_map) { return array(); } $query = id(new DifferentialRevisionQuery()) ->setViewer($this->getRequest()->getUser()) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->setOrder(DifferentialRevisionQuery::ORDER_PATH_MODIFIED) ->setLimit(10) ->needRelationships(true); foreach ($path_map as $path => $path_id) { $query->withPath($repository->getID(), $path_id); } $results = $query->execute(); // Strip out *this* revision. foreach ($results as $key => $result) { if ($result->getID() == $this->revisionID) { unset($results[$key]); } } return $results; } private function renderOtherRevisions(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $user = $this->getRequest()->getUser(); $view = id(new DifferentialRevisionListView()) ->setRevisions($revisions) ->setFields(DifferentialRevisionListView::getDefaultFields($user)) ->setUser($user) ->loadAssets(); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Open Revisions Affecting These Files')) ->appendChild($view); } /** * Note this code is somewhat similar to the buildPatch method in * @{class:DifferentialReviewRequestMail}. * * @return @{class:AphrontRedirectResponse} */ private function buildRawDiffResponse( array $changesets, array $vs_changesets, array $vs_map, PhabricatorRepository $repository = null) { assert_instances_of($changesets, 'DifferentialChangeset'); assert_instances_of($vs_changesets, 'DifferentialChangeset'); $viewer = $this->getRequest()->getUser(); $engine = new PhabricatorDifferenceEngine(); $generated_changesets = array(); foreach ($changesets as $changeset) { $changeset->attachHunks($changeset->loadHunks()); $right = $changeset->makeNewFile(); $choice = $changeset; $vs = idx($vs_map, $changeset->getID()); if ($vs == -1) { $left = $right; $right = $changeset->makeOldFile(); } else if ($vs) { $choice = $vs_changeset = $vs_changesets[$vs]; $vs_changeset->attachHunks($vs_changeset->loadHunks()); $left = $vs_changeset->makeNewFile(); } else { $left = $changeset->makeOldFile(); } $synthetic = $engine->generateChangesetFromFileContent( $left, $right); if (!$synthetic->getAffectedLineCount()) { $filetype = $choice->getFileType(); if ($filetype == DifferentialChangeType::FILE_TEXT || $filetype == DifferentialChangeType::FILE_SYMLINK) { continue; } } $choice->attachHunks($synthetic->getHunks()); $generated_changesets[] = $choice; } $diff = new DifferentialDiff(); $diff->attachChangesets($generated_changesets); $raw_changes = $diff->buildChangesList(); $changes = array(); foreach ($raw_changes as $changedict) { $changes[] = ArcanistDiffChange::newFromDictionary($changedict); } $loader = id(new PhabricatorFileBundleLoader()) ->setViewer($viewer); $bundle = ArcanistBundle::newFromChanges($changes); $bundle->setLoadFileDataCallback(array($loader, 'loadFileData')); $vcs = $repository ? $repository->getVersionControlSystem() : null; switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $raw_diff = $bundle->toGitPatch(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: default: $raw_diff = $bundle->toUnifiedDiff(); break; } $request_uri = $this->getRequest()->getRequestURI(); // this ends up being something like // D123.diff // or the verbose // D123.vs123.id123.whitespaceignore-all.diff // lame but nice to include these options $file_name = ltrim($request_uri->getPath(), '/').'.'; foreach ($request_uri->getQueryParams() as $key => $value) { if ($key == 'download') { continue; } $file_name .= $key.$value.'.'; } $file_name .= 'diff'; $file = PhabricatorFile::buildFromFileDataOrHash( $raw_diff, array( 'name' => $file_name, )); return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } } diff --git a/src/applications/differential/field/specification/DifferentialProjectReviewersFieldSpecification.php b/src/applications/differential/field/specification/DifferentialProjectReviewersFieldSpecification.php index 53aa040560..8cfbdf8a5f 100644 --- a/src/applications/differential/field/specification/DifferentialProjectReviewersFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialProjectReviewersFieldSpecification.php @@ -1,57 +1,43 @@ getRevision()->getReviewers(); } public function renderLabelForRevisionView() { return pht('Project Reviewers'); } public function renderValueForRevisionView() { $reviewers = array(); foreach ($this->getRevision()->getReviewerStatus() as $reviewer) { if (!$reviewer->isUser()) { $reviewers[] = $reviewer; } } if (!$reviewers) { return null; } - $highlight = array(); - if ($this->getUser()->getPHID() != $this->getRevision()->getAuthorPHID()) { - // Determine which of these projects the viewer is a member of, so we can - // highlight them. (If the viewer is the author, skip this since they - // can't review.) - $phids = mpull($reviewers, 'getReviewerPHID'); - $projects = id(new PhabricatorProjectQuery()) - ->setViewer($this->getUser()) - ->withPHIDs($phids) - ->withMemberPHIDs(array($this->getUser()->getPHID())) - ->execute(); - $highlight = mpull($projects, 'getPHID'); - } - $view = id(new DifferentialReviewersView()) + ->setUser($this->getUser()) ->setReviewers($reviewers) - ->setHandles($this->getLoadedHandles()) - ->setHighlightPHIDs($highlight); + ->setHandles($this->getLoadedHandles()); $diff = $this->getRevision()->loadActiveDiff(); if ($diff) { $view->setActiveDiff($diff); } return $view; } } diff --git a/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php b/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php index f7fcf2056c..b6d00ce7a8 100644 --- a/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php @@ -1,203 +1,203 @@ 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) - ->setHighlightPHIDs(array($this->getUser()->getPHID())) ->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[$phid] = $this->getHandle($phid)->getFullName(); } 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; } $project_type = PhabricatorProjectPHIDTypeProject::TYPECONST; $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/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index 8dfcd60648..90284fefb9 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -1,1095 +1,1188 @@ withStatus(DifferentialRevisionQuery::STATUS_OPEN) * ->execute(); * * @task config Query Configuration * @task exec Query Execution * @task internal Internals */ final class DifferentialRevisionQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $pathIDs = array(); private $status = 'status-any'; const STATUS_ANY = 'status-any'; const STATUS_OPEN = 'status-open'; const STATUS_ACCEPTED = 'status-accepted'; const STATUS_NEEDS_REVIEW = 'status-needs-review'; const STATUS_NEEDS_REVISION = 'status-needs-revision'; const STATUS_CLOSED = 'status-closed'; // NOTE: Same as 'committed' const STATUS_COMMITTED = 'status-committed'; // TODO: Remove. const STATUS_ABANDONED = 'status-abandoned'; private $authors = array(); private $draftAuthors = array(); private $ccs = array(); private $reviewers = array(); private $revIDs = array(); private $commitHashes = array(); private $phids = array(); private $subscribers = array(); private $responsibles = array(); private $branches = array(); private $arcanistProjectPHIDs = array(); private $draftRevisions = array(); private $repositoryPHIDs; private $order = 'order-modified'; const ORDER_MODIFIED = 'order-modified'; const ORDER_CREATED = 'order-created'; /** * This is essentially a denormalized copy of the revision modified time that * should perform better for path queries with a LIMIT. Critically, when you * browse "/", every revision in that repository for all time will match so * the query benefits from being able to stop before fully materializing the * result set. */ const ORDER_PATH_MODIFIED = 'order-path-modified'; private $needRelationships = false; private $needActiveDiffs = false; private $needDiffIDs = false; private $needCommitPHIDs = false; private $needHashes = false; private $needReviewerStatus = false; + private $needReviewerAuthority; private $buildingGlobalOrder; /* -( Query Configuration )------------------------------------------------ */ /** * Filter results to revisions which affect a Diffusion path ID in a given * repository. You can call this multiple times to select revisions for * several paths. * * @param int Diffusion repository ID. * @param int Diffusion path ID. * @return this * @task config */ public function withPath($repository_id, $path_id) { $this->pathIDs[] = array( 'repositoryID' => $repository_id, 'pathID' => $path_id, ); return $this; } /** * Filter results to revisions authored by one of the given PHIDs. Calling * this function will clear anything set by previous calls to * @{method:withAuthors}. * * @param array List of PHIDs of authors * @return this * @task config */ public function withAuthors(array $author_phids) { $this->authors = $author_phids; return $this; } /** * Filter results to revisions with comments authored bythe given PHIDs * * @param array List of PHIDs of authors * @return this * @task config */ public function withDraftRepliesByAuthors(array $author_phids) { $this->draftAuthors = $author_phids; return $this; } /** * Filter results to revisions which CC one of the listed people. Calling this * function will clear anything set by previous calls to @{method:withCCs}. * * @param array List of PHIDs of subscribers * @return this * @task config */ public function withCCs(array $cc_phids) { $this->ccs = $cc_phids; return $this; } /** * Filter results to revisions that have one of the provided PHIDs as * reviewers. Calling this function will clear anything set by previous calls * to @{method:withReviewers}. * * @param array List of PHIDs of reviewers * @return this * @task config */ public function withReviewers(array $reviewer_phids) { $this->reviewers = $reviewer_phids; return $this; } /** * Filter results to revisions that have one of the provided commit hashes. * Calling this function will clear anything set by previous calls to * @{method:withCommitHashes}. * * @param array List of pairs * @return this * @task config */ public function withCommitHashes(array $commit_hashes) { $this->commitHashes = $commit_hashes; return $this; } /** * Filter results to revisions with a given status. Provide a class constant, * such as ##DifferentialRevisionQuery::STATUS_OPEN##. * * @param const Class STATUS constant, like STATUS_OPEN. * @return this * @task config */ public function withStatus($status_constant) { $this->status = $status_constant; return $this; } /** * Filter results to revisions on given branches. * * @param list List of branch names. * @return this * @task config */ public function withBranches(array $branches) { $this->branches = $branches; return $this; } /** * Filter results to only return revisions whose ids are in the given set. * * @param array List of revision ids * @return this * @task config */ public function withIDs(array $ids) { $this->revIDs = $ids; return $this; } /** * Filter results to only return revisions whose PHIDs are in the given set. * * @param array List of revision PHIDs * @return this * @task config */ public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } /** * Given a set of users, filter results to return only revisions they are * responsible for (i.e., they are either authors or reviewers). * * @param array List of user PHIDs. * @return this * @task config */ public function withResponsibleUsers(array $responsible_phids) { $this->responsibles = $responsible_phids; return $this; } /** * Filter results to only return revisions with a given set of subscribers * (i.e., they are authors, reviewers or CC'd). * * @param array List of user PHIDs. * @return this * @task config */ public function withSubscribers(array $subscriber_phids) { $this->subscribers = $subscriber_phids; return $this; } /** * Filter results to only return revisions with a given set of arcanist * projects. * * @param array List of project PHIDs. * @return this * @task config */ public function withArcanistProjectPHIDs(array $arc_project_phids) { $this->arcanistProjectPHIDs = $arc_project_phids; return $this; } public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } /** * Set result ordering. Provide a class constant, such as * ##DifferentialRevisionQuery::ORDER_CREATED##. * * @task config */ public function setOrder($order_constant) { $this->order = $order_constant; return $this; } /** * Set whether or not the query will load and attach relationships. * * @param bool True to load and attach relationships. * @return this * @task config */ public function needRelationships($need_relationships) { $this->needRelationships = $need_relationships; return $this; } /** * Set whether or not the query should load the active diff for each * revision. * * @param bool True to load and attach diffs. * @return this * @task config */ public function needActiveDiffs($need_active_diffs) { $this->needActiveDiffs = $need_active_diffs; return $this; } /** * Set whether or not the query should load the associated commit PHIDs for * each revision. * * @param bool True to load and attach diffs. * @return this * @task config */ public function needCommitPHIDs($need_commit_phids) { $this->needCommitPHIDs = $need_commit_phids; return $this; } /** * Set whether or not the query should load associated diff IDs for each * revision. * * @param bool True to load and attach diff IDs. * @return this * @task config */ public function needDiffIDs($need_diff_ids) { $this->needDiffIDs = $need_diff_ids; return $this; } /** * Set whether or not the query should load associated commit hashes for each * revision. * * @param bool True to load and attach commit hashes. * @return this * @task config */ public function needHashes($need_hashes) { $this->needHashes = $need_hashes; return $this; } /** * Set whether or not the query should load associated reviewer status. * * @param bool True to load and attach reviewers. * @return this * @task config */ public function needReviewerStatus($need_reviewer_status) { $this->needReviewerStatus = $need_reviewer_status; return $this; } + /** + * Request information about the viewer's authority to act on behalf of each + * reviewer. In particular, they have authority to act on behalf of projects + * they are a member of. + * + * @param bool True to load and attach authority. + * @return this + * @task config + */ + public function needReviewerAuthority($need_reviewer_authority) { + $this->needReviewerAuthority = $need_reviewer_authority; + return $this; + } + + /* -( Query Execution )---------------------------------------------------- */ /** * Execute the query as configured, returning matching * @{class:DifferentialRevision} objects. * * @return list List of matching DifferentialRevision objects. * @task exec */ public function loadPage() { $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); $data = $this->loadData(); return $table->loadAllFromArray($data); } public function willFilterPage(array $revisions) { $viewer = $this->getViewer(); $repository_phids = mpull($revisions, 'getRepositoryPHID'); $repository_phids = array_filter($repository_phids); $repositories = array(); if ($repository_phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs($repository_phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); } // If a revision is associated with a repository: // // - the viewer must be able to see the repository; or // - the viewer must have an automatic view capability. // // In the latter case, we'll load the revision but not load the repository. $can_view = PhabricatorPolicyCapability::CAN_VIEW; foreach ($revisions as $key => $revision) { $repo_phid = $revision->getRepositoryPHID(); if (!$repo_phid) { // The revision has no associated repository. Attach `null` and move on. $revision->attachRepository(null); continue; } $repository = idx($repositories, $repo_phid); if ($repository) { // The revision has an associated repository, and the viewer can see // it. Attach it and move on. $revision->attachRepository($repository); continue; } if ($revision->hasAutomaticCapability($can_view, $viewer)) { // The revision has an associated repository which the viewer can not // see, but the viewer has an automatic capability on this revision. // Load the revision without attaching a repository. $revision->attachRepository(null); continue; } // The revision has an associated repository, and the viewer can't see // it, and the viewer has no special capabilities. Filter out this // revision. $this->didRejectResult($revision); unset($revisions[$key]); } if (!$revisions) { return array(); } $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); if ($this->needRelationships) { $this->loadRelationships($conn_r, $revisions); } if ($this->needCommitPHIDs) { $this->loadCommitPHIDs($conn_r, $revisions); } $need_active = $this->needActiveDiffs; $need_ids = $need_active || $this->needDiffIDs; if ($need_ids) { $this->loadDiffIDs($conn_r, $revisions); } if ($need_active) { $this->loadActiveDiffs($conn_r, $revisions); } if ($this->needHashes) { $this->loadHashes($conn_r, $revisions); } - if ($this->needReviewerStatus) { + if ($this->needReviewerStatus || $this->needReviewerAuthority) { $this->loadReviewers($conn_r, $revisions); } return $revisions; } private function loadData() { $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); if ($this->draftAuthors) { $this->draftRevisions = array(); $draft_key = 'differential-comment-'; $drafts = id(new PhabricatorDraft())->loadAllWhere( 'authorPHID IN (%Ls) AND draftKey LIKE %> AND draft != %s', $this->draftAuthors, $draft_key, ''); $len = strlen($draft_key); foreach ($drafts as $draft) { $this->draftRevisions[] = substr($draft->getDraftKey(), $len); } $inlines = id(new DifferentialInlineCommentQuery()) ->withDraftsByAuthors($this->draftAuthors) ->execute(); foreach ($inlines as $inline) { $this->draftRevisions[] = $inline->getRevisionID(); } if (!$this->draftRevisions) { return array(); } } $selects = array(); // NOTE: If the query includes "responsiblePHIDs", we execute it as a // UNION of revisions they own and revisions they're reviewing. This has // much better performance than doing it with JOIN/WHERE. if ($this->responsibles) { $basic_authors = $this->authors; $basic_reviewers = $this->reviewers; $authority_projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($this->responsibles) ->execute(); $authority_phids = mpull($authority_projects, 'getPHID'); try { // Build the query where the responsible users are authors. $this->authors = array_merge($basic_authors, $this->responsibles); $this->reviewers = $basic_reviewers; $selects[] = $this->buildSelectStatement($conn_r); // Build the query where the responsible users are reviewers, or // projects they are members of are reviewers. $this->authors = $basic_authors; $this->reviewers = array_merge( $basic_reviewers, $this->responsibles, $authority_phids); $selects[] = $this->buildSelectStatement($conn_r); // Put everything back like it was. $this->authors = $basic_authors; $this->reviewers = $basic_reviewers; } catch (Exception $ex) { $this->authors = $basic_authors; $this->reviewers = $basic_reviewers; throw $ex; } } else { $selects[] = $this->buildSelectStatement($conn_r); } if (count($selects) > 1) { $this->buildingGlobalOrder = true; $query = qsprintf( $conn_r, '%Q %Q %Q', implode(' UNION DISTINCT ', $selects), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); } else { $query = head($selects); } return queryfx_all($conn_r, '%Q', $query); } private function buildSelectStatement(AphrontDatabaseConnection $conn_r) { $table = new DifferentialRevision(); $select = qsprintf( $conn_r, 'SELECT r.* FROM %T r', $table->getTableName()); $joins = $this->buildJoinsClause($conn_r); $where = $this->buildWhereClause($conn_r); $group_by = $this->buildGroupByClause($conn_r); $this->buildingGlobalOrder = false; $order_by = $this->buildOrderClause($conn_r); $limit = $this->buildLimitClause($conn_r); return qsprintf( $conn_r, '(%Q %Q %Q %Q %Q %Q)', $select, $joins, $where, $group_by, $order_by, $limit); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function buildJoinsClause($conn_r) { $joins = array(); if ($this->pathIDs) { $path_table = new DifferentialAffectedPath(); $joins[] = qsprintf( $conn_r, 'JOIN %T p ON p.revisionID = r.id', $path_table->getTableName()); } if ($this->commitHashes) { $joins[] = qsprintf( $conn_r, 'JOIN %T hash_rel ON hash_rel.revisionID = r.id', ArcanistDifferentialRevisionHash::TABLE_NAME); } if ($this->ccs) { $joins[] = qsprintf( $conn_r, 'JOIN %T cc_rel ON cc_rel.revisionID = r.id '. 'AND cc_rel.relation = %s '. 'AND cc_rel.objectPHID in (%Ls)', DifferentialRevision::RELATIONSHIP_TABLE, DifferentialRevision::RELATION_SUBSCRIBED, $this->ccs); } if ($this->reviewers) { $joins[] = qsprintf( $conn_r, 'JOIN %T e_reviewers ON e_reviewers.src = r.phid '. 'AND e_reviewers.type = %s '. 'AND e_reviewers.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $this->reviewers); } if ($this->subscribers) { // TODO: These can be expressed as a JOIN again (and the corresponding // WHERE clause removed) once subscribers move to edges. $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T sub_rel_cc ON sub_rel_cc.revisionID = r.id '. 'AND sub_rel_cc.relation = %s '. 'AND sub_rel_cc.objectPHID in (%Ls)', DifferentialRevision::RELATIONSHIP_TABLE, DifferentialRevision::RELATION_SUBSCRIBED, $this->subscribers); $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T sub_rel_reviewer ON sub_rel_reviewer.src = r.phid '. 'AND sub_rel_reviewer.type = %s '. 'AND sub_rel_reviewer.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $this->subscribers); } $joins = implode(' ', $joins); return $joins; } /** * @task internal */ private function buildWhereClause($conn_r) { $where = array(); if ($this->pathIDs) { $path_clauses = array(); $repo_info = igroup($this->pathIDs, 'repositoryID'); foreach ($repo_info as $repository_id => $paths) { $path_clauses[] = qsprintf( $conn_r, '(p.repositoryID = %d AND p.pathID IN (%Ld))', $repository_id, ipull($paths, 'pathID')); } $path_clauses = '('.implode(' OR ', $path_clauses).')'; $where[] = $path_clauses; } if ($this->authors) { $where[] = qsprintf( $conn_r, 'r.authorPHID IN (%Ls)', $this->authors); } if ($this->draftRevisions) { $where[] = qsprintf( $conn_r, 'r.id IN (%Ld)', $this->draftRevisions); } if ($this->revIDs) { $where[] = qsprintf( $conn_r, 'r.id IN (%Ld)', $this->revIDs); } if ($this->repositoryPHIDs) { $where[] = qsprintf( $conn_r, 'r.repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } if ($this->commitHashes) { $hash_clauses = array(); foreach ($this->commitHashes as $info) { list($type, $hash) = $info; $hash_clauses[] = qsprintf( $conn_r, '(hash_rel.type = %s AND hash_rel.hash = %s)', $type, $hash); } $hash_clauses = '('.implode(' OR ', $hash_clauses).')'; $where[] = $hash_clauses; } if ($this->phids) { $where[] = qsprintf( $conn_r, 'r.phid IN (%Ls)', $this->phids); } if ($this->branches) { $where[] = qsprintf( $conn_r, 'r.branchName in (%Ls)', $this->branches); } if ($this->arcanistProjectPHIDs) { $where[] = qsprintf( $conn_r, 'r.arcanistProjectPHID in (%Ls)', $this->arcanistProjectPHIDs); } if ($this->subscribers) { $where[] = qsprintf( $conn_r, '(sub_rel_cc.objectPHID IS NOT NULL) OR (sub_rel_reviewer.dst IS NOT NULL)'); } switch ($this->status) { case self::STATUS_ANY: break; case self::STATUS_OPEN: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::NEEDS_REVIEW, ArcanistDifferentialRevisionStatus::NEEDS_REVISION, ArcanistDifferentialRevisionStatus::ACCEPTED, )); break; case self::STATUS_NEEDS_REVIEW: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::NEEDS_REVIEW, )); break; case self::STATUS_NEEDS_REVISION: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::NEEDS_REVISION, )); break; case self::STATUS_ACCEPTED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::ACCEPTED, )); break; case self::STATUS_COMMITTED: phlog( "WARNING: DifferentialRevisionQuery using deprecated ". "STATUS_COMMITTED constant. This will be removed soon. ". "Use STATUS_CLOSED."); // fallthrough case self::STATUS_CLOSED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::CLOSED, )); break; case self::STATUS_ABANDONED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::ABANDONED, )); break; default: throw new Exception( "Unknown revision status filter constant '{$this->status}'!"); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } /** * @task internal */ private function buildGroupByClause($conn_r) { $join_triggers = array_merge( $this->pathIDs, $this->ccs, $this->reviewers, $this->subscribers); $needs_distinct = (count($join_triggers) > 1); if ($needs_distinct) { return 'GROUP BY r.id'; } else { return ''; } } private function loadCursorObject($id) { $results = id(new DifferentialRevisionQuery()) ->setViewer($this->getPagingViewer()) ->withIDs(array((int)$id)) ->execute(); return head($results); } protected function buildPagingClause(AphrontDatabaseConnection $conn_r) { $default = parent::buildPagingClause($conn_r); $before_id = $this->getBeforeID(); $after_id = $this->getAfterID(); if (!$before_id && !$after_id) { return $default; } if ($before_id) { $cursor = $this->loadCursorObject($before_id); } else { $cursor = $this->loadCursorObject($after_id); } if (!$cursor) { return null; } $columns = array(); switch ($this->order) { case self::ORDER_CREATED: return $default; case self::ORDER_MODIFIED: $columns[] = array( 'name' => 'r.dateModified', 'value' => $cursor->getDateModified(), 'type' => 'int', ); break; case self::ORDER_PATH_MODIFIED: $columns[] = array( 'name' => 'p.epoch', 'value' => $cursor->getDateCreated(), 'type' => 'int', ); break; } $columns[] = array( 'name' => 'r.id', 'value' => $cursor->getID(), 'type' => 'int', ); return $this->buildPagingClauseFromMultipleColumns( $conn_r, $columns, array( 'reversed' => (bool)($before_id xor $this->getReversePaging()), )); } protected function getPagingColumn() { $is_global = $this->buildingGlobalOrder; switch ($this->order) { case self::ORDER_MODIFIED: if ($is_global) { return 'dateModified'; } return 'r.dateModified'; case self::ORDER_CREATED: if ($is_global) { return 'id'; } return 'r.id'; case self::ORDER_PATH_MODIFIED: if (!$this->pathIDs) { throw new Exception( "To use ORDER_PATH_MODIFIED, you must specify withPath()."); } return 'p.epoch'; default: throw new Exception("Unknown query order constant '{$this->order}'."); } } private function loadRelationships($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $relationships = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE revisionID in (%Ld) AND relation != %s ORDER BY sequence', DifferentialRevision::RELATIONSHIP_TABLE, mpull($revisions, 'getID'), DifferentialRevision::RELATION_REVIEWER); $relationships = igroup($relationships, 'revisionID'); $type_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($revisions, 'getPHID')) ->withEdgeTypes(array($type_reviewer)) ->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST) ->execute(); foreach ($revisions as $revision) { $data = idx($relationships, $revision->getID(), array()); $revision_edges = $edges[$revision->getPHID()][$type_reviewer]; foreach ($revision_edges as $dst_phid => $edge_data) { $data[] = array( 'relation' => DifferentialRevision::RELATION_REVIEWER, 'objectPHID' => $dst_phid, 'reasonPHID' => null, ); } $revision->attachRelationships($data); } } private function loadCommitPHIDs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $commit_phids = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE revisionID IN (%Ld)', DifferentialRevision::TABLE_COMMIT, mpull($revisions, 'getID')); $commit_phids = igroup($commit_phids, 'revisionID'); foreach ($revisions as $revision) { $phids = idx($commit_phids, $revision->getID(), array()); $phids = ipull($phids, 'commitPHID'); $revision->attachCommitPHIDs($phids); } } private function loadDiffIDs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $diff_table = new DifferentialDiff(); $diff_ids = queryfx_all( $conn_r, 'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld) ORDER BY id DESC', $diff_table->getTableName(), mpull($revisions, 'getID')); $diff_ids = igroup($diff_ids, 'revisionID'); foreach ($revisions as $revision) { $ids = idx($diff_ids, $revision->getID(), array()); $ids = ipull($ids, 'id'); $revision->attachDiffIDs($ids); } } private function loadActiveDiffs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $diff_table = new DifferentialDiff(); $load_ids = array(); foreach ($revisions as $revision) { $diffs = $revision->getDiffIDs(); if ($diffs) { $load_ids[] = max($diffs); } } $active_diffs = array(); if ($load_ids) { $active_diffs = $diff_table->loadAllWhere( 'id IN (%Ld)', $load_ids); } $active_diffs = mpull($active_diffs, null, 'getRevisionID'); foreach ($revisions as $revision) { $revision->attachActiveDiff(idx($active_diffs, $revision->getID())); } } private function loadHashes( AphrontDatabaseConnection $conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE revisionID IN (%Ld)', 'differential_revisionhash', mpull($revisions, 'getID')); $data = igroup($data, 'revisionID'); foreach ($revisions as $revision) { $hashes = idx($data, $revision->getID(), array()); $list = array(); foreach ($hashes as $hash) { $list[] = array($hash['type'], $hash['hash']); } $revision->attachHashes($list); } } private function loadReviewers( AphrontDatabaseConnection $conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($revisions, 'getPHID')) ->withEdgeTypes(array($edge_type)) ->needEdgeData(true) ->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST) ->execute(); + $viewer = $this->getViewer(); + $viewer_phid = $viewer->getPHID(); + + // Figure out which of these reviewers the viewer has authority to act as. + if ($this->needReviewerAuthority && $viewer_phid) { + $allow_key = 'differential.allow-self-accept'; + $allow_self = PhabricatorEnv::getEnvConfig($allow_key); + $authority = $this->loadReviewerAuthority( + $revisions, + $edges, + $allow_self); + } + foreach ($revisions as $revision) { $revision_edges = $edges[$revision->getPHID()][$edge_type]; $reviewers = array(); - foreach ($revision_edges as $user_phid => $edge) { - $data = $edge['data']; - $reviewers[] = new DifferentialReviewer( - $user_phid, - idx($data, 'status'), - idx($data, 'diff')); + foreach ($revision_edges as $reviewer_phid => $edge) { + $reviewer = new DifferentialReviewer($reviewer_phid, $edge['data']); + + if ($this->needReviewerAuthority) { + if (!$viewer_phid) { + // Logged-out users never have authority. + $has_authority = false; + } else if ((!$allow_self) && + ($revision->getAuthorPHID() == $viewer_phid)) { + // The author can never have authority unless we allow self-accept. + $has_authority = false; + } else { + // Otherwise, look up whether th viewer has authority. + $has_authority = isset($authority[$reviewer_phid]); + } + + $reviewer->attachAuthority($viewer, $has_authority); + } + + $reviewers[$reviewer_phid] = $reviewer; } $revision->attachReviewerStatus($reviewers); } } public static function splitResponsible(array $revisions, array $user_phids) { $blocking = array(); $active = array(); $waiting = array(); $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; // Bucket revisions into $blocking (revisions where you are blocking // others), $active (revisions you need to do something about) and $waiting // (revisions you're waiting on someone else to do something about). foreach ($revisions as $revision) { $needs_review = ($revision->getStatus() == $status_review); $filter_is_author = in_array($revision->getAuthorPHID(), $user_phids); if (!$revision->getReviewers()) { $needs_review = false; } // If exactly one of "needs review" and "the user is the author" is // true, the user needs to act on it. Otherwise, they're waiting on // it. if ($needs_review ^ $filter_is_author) { if ($needs_review) { array_unshift($blocking, $revision); } else { $active[] = $revision; } } else { $waiting[] = $revision; } } return array($blocking, $active, $waiting); } + private function loadReviewerAuthority( + array $revisions, + array $edges, + $allow_self) { + + $revision_map = mpull($revisions, null, 'getPHID'); + $viewer_phid = $this->getViewer()->getPHID(); + + // Find all the project reviewers which the user may have authority over. + $project_phids = array(); + $project_type = PhabricatorProjectPHIDTypeProject::TYPECONST; + $edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; + foreach ($edges as $src => $types) { + if (!$allow_self) { + if ($revision_map[$src]->getAuthorPHID() == $viewer_phid) { + // If self-review isn't permitted, the user will never have + // authority over projects on revisions they authored because you + // can't accept your own revisions, so we don't need to load any + // data about these reviewers. + continue; + } + } + $edge_data = idx($types, $edge_type, array()); + foreach ($edge_data as $dst => $data) { + if (phid_get_type($dst) == $project_type) { + $project_phids[] = $dst; + } + } + } + + // Now, figure out which of these projects the viewer is actually a + // member of. + $project_authority = array(); + if ($project_phids) { + $project_authority = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($project_phids) + ->withMemberPHIDs(array($viewer_phid)) + ->execute(); + $project_authority = mpull($project_authority, 'getPHID'); + } + + // Finally, the viewer has authority over themselves. + return array( + $viewer_phid => true, + ) + array_fuse($project_authority); + } + + + } diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index e6597ccaf7..62ae27d8f8 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -1,32 +1,49 @@ reviewerPHID = $reviewer_phid; - $this->status = $status; - $this->diffID = $diff_id; + $this->status = idx($edge_data, 'status'); + $this->diffID = idx($edge_data, 'diff'); } public function getReviewerPHID() { return $this->reviewerPHID; } public function getStatus() { return $this->status; } public function getDiffID() { return $this->diffID; } public function isUser() { $user_type = PhabricatorPeoplePHIDTypeUser::TYPECONST; return (phid_get_type($this->getReviewerPHID()) == $user_type); } + public function attachAuthority(PhabricatorUser $user, $has_authority) { + $this->authority[$user->getPHID()] = $has_authority; + return $this; + } + + public function hasAuthority(PhabricatorUser $viewer) { + // It would be nice to use assertAttachedKey() here, but we don't extend + // PhabricatorLiskDAO, and faking that seems sketchy. + + $viewer_phid = $viewer->getPHID(); + if (!array_key_exists($viewer_phid, $this->authority)) { + throw new Exception("You must attachAuthority() first!"); + } + return $this->authority[$viewer_phid]; + } + } diff --git a/src/applications/differential/view/DifferentialReviewersView.php b/src/applications/differential/view/DifferentialReviewersView.php index 851af0afaa..09179e57bf 100644 --- a/src/applications/differential/view/DifferentialReviewersView.php +++ b/src/applications/differential/view/DifferentialReviewersView.php @@ -1,107 +1,98 @@ reviewers = $reviewers; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setActiveDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } - public function setHighlightPHIDs(array $phids) { - $this->highlightPHIDs = $phids; - return $this; - } - public function render() { - $view = new PHUIStatusListView(); - - $highlighted = array_fuse($this->highlightPHIDs); + $viewer = $this->getUser(); + $view = new PHUIStatusListView(); foreach ($this->reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); $handle = $this->handles[$phid]; // If we're missing either the diff or action information for the // reviewer, render information as current. $is_current = (!$this->diff) || (!$reviewer->getDiffID()) || ($this->diff->getID() == $reviewer->getDiffID()); $item = new PHUIStatusItemView(); - if (isset($highlighted[$phid])) { - $item->setHighlighted(true); - } + $item->setHighlighted($reviewer->hasAuthority($viewer)); switch ($reviewer->getStatus()) { case DifferentialReviewerStatus::STATUS_ADDED: $item->setIcon('open-dark', pht('Review Requested')); break; case DifferentialReviewerStatus::STATUS_ACCEPTED: if ($is_current) { $item->setIcon( 'accept-green', pht('Accepted')); } else { $item->setIcon( 'accept-dark', pht('Accepted Prior Diff')); } break; case DifferentialReviewerStatus::STATUS_REJECTED: if ($is_current) { $item->setIcon( 'reject-red', pht('Requested Changes')); } else { $item->setIcon( 'reject-dark', pht('Requested Changes to Prior Diff')); } break; case DifferentialReviewerStatus::STATUS_COMMENTED: if ($is_current) { $item->setIcon( 'info-blue', pht('Commented')); } else { $item->setIcon( 'info-dark', pht('Commented Previously')); } break; default: $item->setIcon('question-dark', pht('%s?', $reviewer->getStatus())); break; } $item->setTarget($handle->renderLink()); $view->addItem($item); } return $view; } }