diff --git a/src/applications/differential/conduit/ConduitAPI_differential_getrevisioncomments_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_getrevisioncomments_Method.php index d2aaf69e57..ee1531add0 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_getrevisioncomments_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_getrevisioncomments_Method.php @@ -1,81 +1,82 @@ 'required list', 'inlines' => 'optional bool', ); } public function defineReturnType() { return 'nonempty list>'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $results = array(); $revision_ids = $request->getValue('ids'); if (!$revision_ids) { return $results; } $comments = id(new DifferentialCommentQuery()) ->withRevisionIDs($revision_ids) ->execute(); $with_inlines = $request->getValue('inlines'); if ($with_inlines) { $inlines = id(new DifferentialInlineCommentQuery()) ->withRevisionIDs($revision_ids) ->execute(); $changesets = array(); if ($inlines) { $changesets = id(new DifferentialChangeset())->loadAllWhere( 'id IN (%Ld)', array_unique(mpull($inlines, 'getChangesetID'))); $inlines = mgroup($inlines, 'getCommentID'); } } foreach ($comments as $comment) { + // TODO: Sort this out in the ID -> PHID change. $revision_id = $comment->getRevisionID(); $result = array( 'revisionID' => $revision_id, 'action' => $comment->getAction(), 'authorPHID' => $comment->getAuthorPHID(), 'dateCreated' => $comment->getDateCreated(), 'content' => $comment->getContent(), ); if ($with_inlines) { $result['inlines'] = array(); foreach (idx($inlines, $comment->getID(), array()) as $inline) { $changeset = idx($changesets, $inline->getChangesetID()); $result['inlines'][] = $this->buildInlineInfoDictionary( $inline, $changeset); } // TODO: Put synthetic inlines without an attached comment somewhere. } $results[$revision_id][] = $result; } return $results; } } diff --git a/src/applications/differential/controller/DifferentialCommentPreviewController.php b/src/applications/differential/controller/DifferentialCommentPreviewController.php index cce6b36fcb..ce3fd356d0 100644 --- a/src/applications/differential/controller/DifferentialCommentPreviewController.php +++ b/src/applications/differential/controller/DifferentialCommentPreviewController.php @@ -1,76 +1,83 @@ id = $data['id']; } public function processRequest() { - $request = $this->getRequest(); + $viewer = $request->getUser(); + + $revision = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$revision) { + return new Aphront404Response(); + } - $author_phid = $request->getUser()->getPHID(); - + $author_phid = $viewer->getPHID(); $action = $request->getStr('action'); - $comment = new DifferentialComment(); $comment->setContent($request->getStr('content')); $comment->setAction($action); $comment->setAuthorPHID($author_phid); $handles = array($author_phid); $reviewers = $request->getStrList('reviewers'); if (DifferentialAction::allowReviewers($action) && $reviewers) { $comment->setMetadata(array( DifferentialComment::METADATA_ADDED_REVIEWERS => $reviewers)); $handles = array_merge($handles, $reviewers); } $ccs = $request->getStrList('ccs'); if ($action == DifferentialAction::ACTION_ADDCCS && $ccs) { $comment->setMetadata(array( DifferentialComment::METADATA_ADDED_CCS => $ccs)); $handles = array_merge($handles, $ccs); } $handles = $this->loadViewerHandles($handles); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($request->getUser()); $engine->addObject($comment, DifferentialComment::MARKUP_FIELD_BODY); $engine->process(); $view = new DifferentialRevisionCommentView(); $view->setUser($request->getUser()); $view->setComment($comment); $view->setHandles($handles); $view->setMarkupEngine($engine); + $view->setRevision($revision); $view->setPreview(true); $view->setTargetDiff(null); $metadata = array( 'reviewers' => $reviewers, 'ccs' => $ccs, ); if ($action != DifferentialAction::ACTION_COMMENT) { $metadata['action'] = $action; } id(new PhabricatorDraft()) ->setAuthorPHID($author_phid) ->setDraftKey('differential-comment-'.$this->id) ->setDraft($comment->getContent()) ->setMetadata($metadata) ->replaceOrDelete(); return id(new AphrontAjaxResponse()) ->setContent($view->render()); } } diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 8e15dd7de3..ef913b7863 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1,928 +1,929 @@ 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( $revision, $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(); $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) { foreach ($comment->getRequiredHandlePHIDs() 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); + $comment_view->setRevision($revision); 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')); // TODO: This just makes the "Z" key work. Generalize this and remove // it at some point. $comment_form = phutil_tag( 'div', array( 'class' => 'differential-add-comment-panel', ), $comment_form); } $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, $diff_history, $warning, $local_view, $toc_view, $other_view, $changeset_view, )); if ($comment_form) { $page_pane->appendChild($comment_form); } 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->addTextCrumb($object_id, '/'.$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 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, ); } $this->requireResource('phabricator-object-selector-css'); $this->requireResource('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()); $status = $revision->getStatus(); $viewer_has_accepted = false; $viewer_has_rejected = false; $status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED; $status_rejected = DifferentialReviewerStatus::STATUS_REJECTED; foreach ($revision->getReviewerStatus() as $reviewer) { if ($reviewer->getReviewerPHID() == $viewer_phid) { if ($reviewer->getStatus() == $status_accepted) { $viewer_has_accepted = true; } if ($reviewer->getStatus() == $status_rejected) { $viewer_has_rejected = true; } break; } } $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_REJECT] = !$viewer_has_rejected; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_ACCEPT] = !$viewer_has_accepted; $actions[DifferentialAction::ACTION_REJECT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; 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( DifferentialRevision $revision, 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(); foreach ($changesets as $changeset) { $changeset->attachHunks($changeset->loadHunks()); } $diff = new DifferentialDiff(); $diff->attachChangesets($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, 'ttl' => (60 * 60 * 24), 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file->attachToObject( $this->getRequest()->getUser(), $revision->getPHID()); unset($unguarded); return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } } diff --git a/src/applications/differential/view/DifferentialRevisionCommentListView.php b/src/applications/differential/view/DifferentialRevisionCommentListView.php index b03cee03db..a6f72854ce 100644 --- a/src/applications/differential/view/DifferentialRevisionCommentListView.php +++ b/src/applications/differential/view/DifferentialRevisionCommentListView.php @@ -1,198 +1,209 @@ revision = $revision; + return $this; + } + + public function getRevision() { + return $this->revision; + } public function setComments(array $comments) { assert_instances_of($comments, 'DifferentialComment'); $this->comments = $comments; return $this; } public function setInlineComments(array $inline_comments) { assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $this->inlines = $inline_comments; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setChangesets(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $this->changesets = $changesets; return $this; } public function setTargetDiff(DifferentialDiff $target) { $this->target = $target; return $this; } public function setVersusDiffID($diff_vs) { $this->versusDiffID = $diff_vs; return $this; } public function getID() { if (!$this->id) { $this->id = celerity_generate_unique_node_id(); } return $this->id; } public function render() { $this->requireResource('differential-revision-comment-list-css'); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($this->user); foreach ($this->comments as $comment) { $comment->giveFacebookSomeArbitraryDiff($this->target); $engine->addObject( $comment, DifferentialComment::MARKUP_FIELD_BODY); } foreach ($this->inlines as $inline) { $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); $inlines = mgroup($this->inlines, 'getCommentID'); $num = 1; $html = array(); foreach ($this->comments as $comment) { $view = new DifferentialRevisionCommentView(); $view->setComment($comment); $view->setUser($this->user); $view->setHandles($this->handles); $view->setMarkupEngine($engine); $view->setInlineComments(idx($inlines, $comment->getID(), array())); $view->setChangesets($this->changesets); $view->setTargetDiff($this->target); + $view->setRevision($this->getRevision()); $view->setVersusDiffID($this->versusDiffID); if ($comment->getAction() == DifferentialAction::ACTION_SUMMARIZE) { $view->setAnchorName('summary'); } else if ($comment->getAction() == DifferentialAction::ACTION_TESTPLAN) { $view->setAnchorName('test-plan'); } else { $view->setAnchorName('comment-'.$num); $num++; } $html[] = $view->render(); } $objs = array_reverse(array_values($this->comments)); $html = array_reverse(array_values($html)); $user = $this->user; $last_comment = null; // Find the most recent comment by the viewer. foreach ($objs as $position => $comment) { if ($user && ($comment->getAuthorPHID() == $user->getPHID())) { if ($last_comment === null) { $last_comment = $position; } else if ($last_comment == $position - 1) { // If the viewer made several comments in a row, show them all. This // is a spaz rule for epriestley. $last_comment = $position; } } } $header = array(); $hidden = array(); if ($last_comment !== null) { foreach ($objs as $position => $comment) { if (!$comment->getID()) { // These are synthetic comments with summary/test plan information. $header[] = $html[$position]; unset($html[$position]); continue; } if ($position <= $last_comment) { // Always show comments after the viewer's last comment. continue; } if ($position < 3) { // Always show the 3 most recent comments. continue; } $hidden[] = $position; } } if (count($hidden) <= 3) { // Don't hide if there's not much to hide. $hidden = array(); } $header = array_reverse($header); $hidden = array_select_keys($html, $hidden); $visible = array_diff_key($html, $hidden); $hidden = array_reverse($hidden); $visible = array_reverse($visible); if ($hidden) { $this->initBehavior( 'differential-show-all-comments', array( 'markup' => implode("\n", $hidden), )); $hidden = javelin_tag( 'div', array( 'sigil' => "differential-all-comments-container", ), phutil_tag( 'div', array( 'class' => 'differential-older-comments-are-hidden', ), array( pht( '%s older comments are hidden.', new PhutilNumber(count($hidden))), ' ', javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-show-all-comments', ), pht('Show all comments.')), ))); } else { $hidden = null; } return javelin_tag( 'div', array( 'class' => 'differential-comment-list', 'id' => $this->getID(), ), array_merge($header, array($hidden), $visible)); } } diff --git a/src/applications/differential/view/DifferentialRevisionCommentView.php b/src/applications/differential/view/DifferentialRevisionCommentView.php index 9ec01df560..d661eb01d9 100644 --- a/src/applications/differential/view/DifferentialRevisionCommentView.php +++ b/src/applications/differential/view/DifferentialRevisionCommentView.php @@ -1,295 +1,305 @@ revision = $revision; + return $this; + } + + public function getRevision() { + return $this->revision; + } public function setComment($comment) { $this->comment = $comment; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) { $this->markupEngine = $markup_engine; return $this; } public function setPreview($preview) { $this->preview = $preview; return $this; } public function setInlineComments(array $inline_comments) { assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $this->inlines = $inline_comments; return $this; } public function setChangesets(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); // Ship these in sorted by getSortKey() and keyed by ID... or else! $this->changesets = $changesets; return $this; } public function setTargetDiff($target) { $this->target = $target; return $this; } public function setVersusDiffID($diff_vs) { $this->versusDiffID = $diff_vs; return $this; } public function setAnchorName($anchor_name) { $this->anchorName = $anchor_name; return $this; } public function render() { if (!$this->user) { throw new Exception("Call setUser() before rendering!"); } $this->requireResource('phabricator-remarkup-css'); $this->requireResource('differential-revision-comment-css'); $comment = $this->comment; $action = $comment->getAction(); $action_class = 'differential-comment-action-'.$action; $info = array(); $content = $comment->getContent(); $hide_comments = true; if (strlen(rtrim($content))) { $hide_comments = false; $content = $this->markupEngine->getOutput( $comment, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); $content = phutil_tag_div('phabricator-remarkup', $content); } $inline_render = $this->renderInlineComments(); if ($inline_render) { $hide_comments = false; } $author = $this->handles[$comment->getAuthorPHID()]; $author_link = $author->renderLink(); $metadata = $comment->getMetadata(); $added_reviewers = idx( $metadata, DifferentialComment::METADATA_ADDED_REVIEWERS, array()); $removed_reviewers = idx( $metadata, DifferentialComment::METADATA_REMOVED_REVIEWERS, array()); $added_ccs = idx( $metadata, DifferentialComment::METADATA_ADDED_CCS, array()); $actions = array(); switch ($comment->getAction()) { case DifferentialAction::ACTION_ADDCCS: $actions[] = pht( "%s added CCs: %s.", $author_link, $this->renderHandleList($added_ccs)); $added_ccs = null; break; case DifferentialAction::ACTION_ADDREVIEWERS: $actions[] = pht( "%s added reviewers: %s.", $author_link, $this->renderHandleList($added_reviewers)); $added_reviewers = null; break; case DifferentialAction::ACTION_UPDATE: $diff_id = idx($metadata, DifferentialComment::METADATA_DIFF_ID); if ($diff_id) { $diff_link = phutil_tag( 'a', array( - 'href' => '/D'.$comment->getRevisionID().'?id='.$diff_id, + 'href' => '/D'.$this->getRevision()->getID().'?id='.$diff_id, ), 'Diff #'.$diff_id); $actions[] = pht( "%s updated this revision to %s.", $author_link, $diff_link); } else { $actions[] = DifferentialAction::getBasicStoryText( $comment->getAction(), $author_link); } break; default: $actions[] = DifferentialAction::getBasicStoryText( $comment->getAction(), $author_link); break; } if ($added_reviewers) { $actions[] = pht( "%s added reviewers: %s.", $author_link, $this->renderHandleList($added_reviewers)); } if ($removed_reviewers) { $actions[] = pht( "%s removed reviewers: %s.", $author_link, $this->renderHandleList($removed_reviewers)); } if ($added_ccs) { $actions[] = pht( "%s added CCs: %s.", $author_link, $this->renderHandleList($added_ccs)); } foreach ($actions as $key => $action) { $actions[$key] = phutil_tag('div', array(), $action); } $xaction_view = id(new PhabricatorTransactionView()) ->setUser($this->user) ->setImageURI($author->getImageURI()) ->setContentSource($comment->getContentSource()) ->addClass($action_class) ->setActions($actions); if ($this->preview) { $xaction_view->setIsPreview($this->preview); } else { $xaction_view->setEpoch($comment->getDateCreated()); if ($this->anchorName) { $anchor_text = - 'D'.$comment->getRevisionID(). + 'D'.$this->getRevision()->getID(). '#'.preg_replace('/^comment-/', '', $this->anchorName); $xaction_view->setAnchor($this->anchorName, $anchor_text); } } if (!$hide_comments) { $xaction_view->appendChild(phutil_tag_div( 'differential-comment-core', array($content, $inline_render))); } return $xaction_view->render(); } private function renderHandleList(array $phids) { $result = array(); foreach ($phids as $phid) { $result[] = $this->handles[$phid]->renderLink(); } return phutil_implode_html(', ', $result); } private function renderInlineComments() { if (!$this->inlines) { return null; } $inlines = $this->inlines; $changesets = $this->changesets; $inlines_by_changeset = mgroup($inlines, 'getChangesetID'); $inlines_by_changeset = array_select_keys( $inlines_by_changeset, array_keys($this->changesets)); $view = new PhabricatorInlineSummaryView(); foreach ($inlines_by_changeset as $changeset_id => $inlines) { $changeset = $changesets[$changeset_id]; $items = array(); foreach ($inlines as $inline) { $on_target = ($this->target) && ($this->target->getID() == $changeset->getDiffID()); $is_visible = false; if ($inline->getIsNewFile()) { // This comment is on the right side of the versus diff, and visible // on the left side of the page. if ($this->versusDiffID) { if ($changeset->getDiffID() == $this->versusDiffID) { $is_visible = true; } } // This comment is on the right side of the target diff, and visible // on the right side of the page. if ($on_target) { $is_visible = true; } } else { // Ths comment is on the left side of the target diff, and visible // on the left side of the page. if (!$this->versusDiffID) { if ($on_target) { $is_visible = true; } } // TODO: We still get one edge case wrong here, when we have a // versus diff and the file didn't exist in the old version. The // comment is visible because we show the left side of the target // diff when there's no corresponding file in the versus diff, but // we incorrectly link it off-page. } $item = array( 'id' => $inline->getID(), 'line' => $inline->getLineNumber(), 'length' => $inline->getLineLength(), 'content' => $this->markupEngine->getOutput( $inline, DifferentialInlineComment::MARKUP_FIELD_BODY), ); if (!$is_visible) { $diff_id = $changeset->getDiffID(); $item['where'] = '(On Diff #'.$diff_id.')'; $item['href'] = - 'D'.$this->comment->getRevisionID(). + 'D'.$this->getRevision()->getID(). '?id='.$diff_id. '#inline-'.$inline->getID(); } $items[] = $item; } $view->addCommentGroup($changeset->getFilename(), $items); } return $view; } }