diff --git a/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php index a158d86d9f..3b4d9a6a63 100644 --- a/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php @@ -1,102 +1,101 @@ 'required id', ); } protected function defineReturnType() { return 'nonempty dict'; } protected function defineErrorTypes() { return array( 'ERR_BAD_REVISION' => pht('No such revision exists.'), ); } protected function execute(ConduitAPIRequest $request) { $diff = null; $revision_id = $request->getValue('revision_id'); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($request->getUser()) - ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); if (!$revision) { throw new ConduitException('ERR_BAD_REVISION'); } - $reviewer_phids = array_values($revision->getReviewers()); + $reviewer_phids = $revision->getReviewerPHIDs(); $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withRevisionIDs(array($revision_id)) ->needChangesets(true) ->execute(); $diff_dicts = mpull($diffs, 'getDiffDict'); $commit_dicts = array(); $commit_phids = $revision->loadCommitPHIDs(); $handles = id(new PhabricatorHandleQuery()) ->setViewer($request->getUser()) ->withPHIDs($commit_phids) ->execute(); foreach ($commit_phids as $commit_phid) { $commit_dicts[] = array( 'fullname' => $handles[$commit_phid]->getFullName(), 'dateCommitted' => $handles[$commit_phid]->getTimestamp(), ); } $field_data = $this->loadCustomFieldsForRevisions( $request->getUser(), array($revision)); $dict = array( 'id' => $revision->getID(), 'phid' => $revision->getPHID(), 'authorPHID' => $revision->getAuthorPHID(), 'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()), 'title' => $revision->getTitle(), 'status' => $revision->getStatus(), 'statusName' => ArcanistDifferentialRevisionStatus::getNameForRevisionStatus( $revision->getStatus()), 'summary' => $revision->getSummary(), 'testPlan' => $revision->getTestPlan(), 'lineCount' => $revision->getLineCount(), 'reviewerPHIDs' => $reviewer_phids, 'diffs' => $diff_dicts, 'commits' => $commit_dicts, 'auxiliary' => idx($field_data, $revision->getPHID(), array()), ); return $dict; } } diff --git a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php index 720361367a..c9f90d0877 100644 --- a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php @@ -1,246 +1,254 @@ formatStringConstants($hash_types); $status_types = array( DifferentialRevisionQuery::STATUS_ANY, DifferentialRevisionQuery::STATUS_OPEN, DifferentialRevisionQuery::STATUS_ACCEPTED, DifferentialRevisionQuery::STATUS_CLOSED, ); $status_const = $this->formatStringConstants($status_types); $order_types = array( DifferentialRevisionQuery::ORDER_MODIFIED, DifferentialRevisionQuery::ORDER_CREATED, ); $order_const = $this->formatStringConstants($order_types); return array( 'authors' => 'optional list', 'ccs' => 'optional list', 'reviewers' => 'optional list', 'paths' => 'optional list>', 'commitHashes' => 'optional list>', 'status' => 'optional '.$status_const, 'order' => 'optional '.$order_const, 'limit' => 'optional uint', 'offset' => 'optional uint', 'ids' => 'optional list', 'phids' => 'optional list', 'subscribers' => 'optional list', 'responsibleUsers' => 'optional list', 'branches' => 'optional list', ); } protected function defineReturnType() { return 'list'; } protected function defineErrorTypes() { return array( 'ERR-INVALID-PARAMETER' => pht('Missing or malformed parameter.'), ); } protected function execute(ConduitAPIRequest $request) { $authors = $request->getValue('authors'); $ccs = $request->getValue('ccs'); $reviewers = $request->getValue('reviewers'); $status = $request->getValue('status'); $order = $request->getValue('order'); $path_pairs = $request->getValue('paths'); $commit_hashes = $request->getValue('commitHashes'); $limit = $request->getValue('limit'); $offset = $request->getValue('offset'); $ids = $request->getValue('ids'); $phids = $request->getValue('phids'); $subscribers = $request->getValue('subscribers'); $responsible_users = $request->getValue('responsibleUsers'); $branches = $request->getValue('branches'); $query = id(new DifferentialRevisionQuery()) ->setViewer($request->getUser()); if ($authors) { $query->withAuthors($authors); } if ($ccs) { $query->withCCs($ccs); } if ($reviewers) { $query->withReviewers($reviewers); } if ($path_pairs) { $paths = array(); foreach ($path_pairs as $pair) { list($callsign, $path) = $pair; $paths[] = $path; } $path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs(); if (count($path_map) != count($paths)) { $unknown_paths = array(); foreach ($paths as $p) { if (!idx($path_map, $p)) { $unknown_paths[] = $p; } } throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription( pht( 'Unknown paths: %s', implode(', ', $unknown_paths))); } $repos = array(); foreach ($path_pairs as $pair) { list($callsign, $path) = $pair; if (!idx($repos, $callsign)) { $repos[$callsign] = id(new PhabricatorRepositoryQuery()) ->setViewer($request->getUser()) ->withCallsigns(array($callsign)) ->executeOne(); if (!$repos[$callsign]) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription( pht( 'Unknown repo callsign: %s', $callsign)); } } $repo = $repos[$callsign]; $query->withPath($repo->getID(), idx($path_map, $path)); } } if ($commit_hashes) { $hash_types = ArcanistDifferentialRevisionHash::getTypes(); foreach ($commit_hashes as $info) { list($type, $hash) = $info; if (empty($type) || !in_array($type, $hash_types) || empty($hash)) { throw new ConduitException('ERR-INVALID-PARAMETER'); } } $query->withCommitHashes($commit_hashes); } if ($status) { $query->withStatus($status); } if ($order) { $query->setOrder($order); } if ($limit) { $query->setLimit($limit); } if ($offset) { $query->setOffset($offset); } if ($ids) { $query->withIDs($ids); } if ($phids) { $query->withPHIDs($phids); } if ($responsible_users) { $query->withResponsibleUsers($responsible_users); } if ($subscribers) { $query->withCCs($subscribers); } if ($branches) { $query->withBranches($branches); } - $query->needRelationships(true); + $query->needReviewerStatus(true); $query->needCommitPHIDs(true); $query->needDiffIDs(true); $query->needActiveDiffs(true); $query->needHashes(true); $revisions = $query->execute(); $field_data = $this->loadCustomFieldsForRevisions( $request->getUser(), $revisions); + if ($revisions) { + $ccs = id(new PhabricatorSubscribersQuery()) + ->withObjectPHIDs(mpull($revisions, 'getPHID')) + ->execute(); + } else { + $ccs = array(); + } + $results = array(); foreach ($revisions as $revision) { $diff = $revision->getActiveDiff(); if (!$diff) { continue; } $id = $revision->getID(); $phid = $revision->getPHID(); $result = array( 'id' => $id, 'phid' => $phid, 'title' => $revision->getTitle(), 'uri' => PhabricatorEnv::getProductionURI('/D'.$id), 'dateCreated' => $revision->getDateCreated(), 'dateModified' => $revision->getDateModified(), 'authorPHID' => $revision->getAuthorPHID(), 'status' => $revision->getStatus(), 'statusName' => ArcanistDifferentialRevisionStatus::getNameForRevisionStatus( $revision->getStatus()), 'properties' => $revision->getProperties(), 'branch' => $diff->getBranch(), 'summary' => $revision->getSummary(), 'testPlan' => $revision->getTestPlan(), 'lineCount' => $revision->getLineCount(), 'activeDiffPHID' => $diff->getPHID(), 'diffs' => $revision->getDiffIDs(), 'commits' => $revision->getCommitPHIDs(), - 'reviewers' => array_values($revision->getReviewers()), - 'ccs' => array_values($revision->getCCPHIDs()), + 'reviewers' => $revision->getReviewerPHIDs(), + 'ccs' => idx($ccs, $phid, array()), 'hashes' => $revision->getHashes(), 'auxiliary' => idx($field_data, $phid, array()), 'repositoryPHID' => $diff->getRepositoryPHID(), ); // TODO: This is a hacky way to put permissions on this field until we // have first-class support, see T838. if ($revision->getAuthorPHID() == $request->getUser()->getPHID()) { $result['sourcePath'] = $diff->getSourcePath(); } $results[] = $result; } return $results; } } diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index d31da8fefc..9b87d1163d 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1,1107 +1,1109 @@ getViewer(); $this->revisionID = $request->getURIData('id'); $viewer_is_anonymous = !$viewer->isLoggedIn(); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) ->setViewer($viewer) - ->needRelationships(true) ->needReviewerStatus(true) ->needReviewerAuthority(true) ->executeOne(); if (!$revision) { return new Aphront404Response(); } $diffs = id(new DifferentialDiffQuery()) ->setViewer($viewer) ->withRevisionIDs(array($this->revisionID)) ->execute(); $diffs = array_reverse($diffs, $preserve_keys = true); if (!$diffs) { throw new Exception( pht('This revision has no diffs. Something has gone quite wrong.')); } $revision->attachActiveDiff(last($diffs)); $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; } $repository = null; $repository_phid = $target->getRepositoryPHID(); if ($repository_phid) { if ($repository_phid == $revision->getRepositoryPHID()) { $repository = $revision->getRepository(); } else { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($repository_phid)) ->executeOne(); } } 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); } $map = $vs_map; if (!$map) { $map = array_fill_keys(array_keys($changesets), 0); } $old_ids = array(); $new_ids = array(); foreach ($map as $id => $vs) { if ($vs <= 0) { $old_ids[] = $id; $new_ids[] = $id; } else { $new_ids[] = $id; $new_ids[] = $vs; } } $this->loadDiffProperties($diffs); $props = $target_manual->getDiffProperties(); + $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( + $revision->getPHID()); + $object_phids = array_merge( - $revision->getReviewers(), - $revision->getCCPHIDs(), + $revision->getReviewerPHIDs(), + $subscriber_phids, $revision->loadCommitPHIDs(), array( $revision->getAuthorPHID(), $viewer->getPHID(), )); foreach ($revision->getAttached() as $type => $phids) { foreach ($phids as $phid => $info) { $object_phids[] = $phid; } } $field_list = PhabricatorCustomField::getObjectFields( $revision, PhabricatorCustomField::ROLE_VIEW); $field_list->setViewer($viewer); $field_list->readFieldsFromStorage($revision); $warning_handle_map = array(); foreach ($field_list->getFields() as $key => $field) { $req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings(); foreach ($req as $phid) { $warning_handle_map[$key][] = $phid; $object_phids[] = $phid; } } $handles = $this->loadViewerHandles($object_phids); $request_uri = $request->getRequestURI(); $limit = 100; $large = $request->getStr('large'); if (count($changesets) > $limit && !$large) { $count = count($changesets); $warning = new PHUIInfoView(); $warning->setTitle(pht('Very Large Diff')); $warning->setSeverity(PHUIInfoView::SEVERITY_WARNING); $warning->appendChild(hsprintf( '%s %s', pht( 'This diff is very large and affects %s files. '. 'You may load each file individually or ', new PhutilNumber($count)), phutil_tag( 'a', array( 'class' => 'button grey', 'href' => $request_uri ->alter('large', 'true') ->setFragment('toc'), ), pht('Show All Files Inline')))); $warning = $warning->render(); $old = array_select_keys($changesets, $old_ids); $new = array_select_keys($changesets, $new_ids); $query = id(new DifferentialInlineCommentQuery()) ->setViewer($viewer) ->needHidden(true) ->withRevisionPHIDs(array($revision->getPHID())); $inlines = $query->execute(); $inlines = $query->adjustInlinesForChangesets( $inlines, $old, $new, $revision); $visible_changesets = array(); foreach ($inlines as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { $visible_changesets[$changeset_id] = $changesets[$changeset_id]; } } } else { $warning = null; $visible_changesets = $changesets; } $commit_hashes = mpull($diffs, 'getSourceControlBaseRevision'); $local_commits = idx($props, 'local:commits', array()); foreach ($local_commits as $local_commit) { $commit_hashes[] = idx($local_commit, 'tree'); $commit_hashes[] = idx($local_commit, 'local'); } $commit_hashes = array_unique(array_filter($commit_hashes)); if ($commit_hashes) { $commits_for_links = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers($commit_hashes) ->execute(); $commits_for_links = mpull( $commits_for_links, null, 'getCommitIdentifier'); } else { $commits_for_links = array(); } $header = $this->buildHeader($revision); $subheader = $this->buildSubheaderView($revision); $details = $this->buildDetails($revision, $field_list); $curtain = $this->buildCurtain($revision); $whitespace = $request->getStr( 'whitespace', DifferentialChangesetParser::WHITESPACE_IGNORE_MOST); $repository = $revision->getRepository(); if ($repository) { $symbol_indexes = $this->buildSymbolIndexes( $repository, $visible_changesets); } else { $symbol_indexes = array(); } $revision_warnings = $this->buildRevisionWarnings( $revision, $field_list, $warning_handle_map, $handles); $info_view = null; if ($revision_warnings) { $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($revision_warnings); } $detail_diffs = array_select_keys( $diffs, array($diff_vs, $target->getID())); $detail_diffs = mpull($detail_diffs, null, 'getPHID'); $this->loadHarbormasterData($detail_diffs); $diff_detail_box = $this->buildDiffDetailView( $detail_diffs, $revision, $field_list); $unit_box = $this->buildUnitMessagesView( $target, $revision); $timeline = $this->buildTransactions( $revision, $diff_vs ? $diffs[$diff_vs] : $target, $target, $old_ids, $new_ids); $timeline->setQuoteRef($revision->getMonogram()); $changeset_view = id(new DifferentialChangesetListView()) ->setChangesets($changesets) ->setVisibleChangesets($visible_changesets) ->setStandaloneURI('/differential/changeset/') ->setRawFileURIs( '/differential/changeset/?view=old', '/differential/changeset/?view=new') ->setUser($viewer) ->setDiff($target) ->setRenderingReferences($rendering_references) ->setVsMap($vs_map) ->setWhitespace($whitespace) ->setSymbolIndexes($symbol_indexes) ->setTitle(pht('Diff %s', $target->getID())) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); if ($repository) { $changeset_view->setRepository($repository); } if (!$viewer_is_anonymous) { $changeset_view->setInlineCommentControllerURI( '/differential/comment/inline/edit/'.$revision->getID().'/'); } $broken_diffs = $this->loadHistoryDiffStatus($diffs); $history = id(new DifferentialRevisionUpdateHistoryView()) ->setUser($viewer) ->setDiffs($diffs) ->setDiffUnitStatuses($broken_diffs) ->setSelectedVersusDiffID($diff_vs) ->setSelectedDiffID($target->getID()) ->setSelectedWhitespace($whitespace) ->setCommitsForLinks($commits_for_links); $local_table = id(new DifferentialLocalCommitsView()) ->setUser($viewer) ->setLocalCommits(idx($props, 'local:commits')) ->setCommitsForLinks($commits_for_links); 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 = $this->buildTableOfContents( $changesets, $visible_changesets, $target->loadCoverageMap($viewer)); $tab_group = id(new PHUITabGroupView()) ->addTab( id(new PHUITabView()) ->setName(pht('Files')) ->setKey('files') ->appendChild($toc_view)) ->addTab( id(new PHUITabView()) ->setName(pht('History')) ->setKey('history') ->appendChild($history)) ->addTab( id(new PHUITabView()) ->setName(pht('Commits')) ->setKey('commits') ->appendChild($local_table)); $stack_graph = id(new DifferentialRevisionGraph()) ->setViewer($viewer) ->setSeedPHID($revision->getPHID()) ->setLoadEntireGraph(true) ->loadGraph(); if (!$stack_graph->isEmpty()) { $stack_table = $stack_graph->newGraphTable(); $parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST; $reachable = $stack_graph->getReachableObjects($parent_type); foreach ($reachable as $key => $reachable_revision) { if ($reachable_revision->isClosed()) { unset($reachable[$key]); } } if ($reachable) { $stack_name = pht('Stack (%s Open)', phutil_count($reachable)); $stack_color = PHUIListItemView::STATUS_FAIL; } else { $stack_name = pht('Stack'); $stack_color = null; } $tab_group->addTab( id(new PHUITabView()) ->setName($stack_name) ->setKey('stack') ->setColor($stack_color) ->appendChild($stack_table)); } if ($other_view) { $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Similar')) ->setKey('similar') ->appendChild($other_view)); } $tab_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Revision Contents')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addTabGroup($tab_group); $signatures = DifferentialRequiredSignaturesField::loadForRevision( $revision); $missing_signatures = false; foreach ($signatures as $phid => $signed) { if (!$signed) { $missing_signatures = true; } } $footer = array(); $signature_message = null; if ($missing_signatures) { $signature_message = id(new PHUIInfoView()) ->setTitle(pht('Content Hidden')) ->appendChild( pht( 'The content of this revision is hidden until the author has '. 'signed all of the required legal agreements.')); } else { $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('toc') ->setNavigationMarker(true); $footer[] = array( $anchor, $warning, $tab_view, $changeset_view, ); } $comment_view = id(new DifferentialRevisionEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($revision); $comment_view->setTransactionTimeline($timeline); $review_warnings = array(); foreach ($field_list->getFields() as $field) { $review_warnings[] = $field->getWarningsForDetailView(); } $review_warnings = array_mergev($review_warnings); if ($review_warnings) { $warnings_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($review_warnings); $comment_view->setInfoView($warnings_view); } $footer[] = $comment_view; $monogram = $revision->getMonogram(); $operations_box = $this->buildOperationsBox($revision); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($monogram); $crumbs->setBorder(true); $filetree_on = $viewer->compareUserSetting( PhabricatorShowFiletreeSetting::SETTINGKEY, PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE); $nav = null; if ($filetree_on) { $collapsed_key = PhabricatorFiletreeVisibleSetting::SETTINGKEY; $collapsed_value = $viewer->getUserSetting($collapsed_key); $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setTitle($monogram) ->setBaseURI(new PhutilURI($revision->getURI())) ->setCollapsed((bool)$collapsed_value) ->build($changesets); } Javelin::initBehavior('differential-user-select'); Javelin::initBehavior('differential-keyboard-navigation'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setCurtain($curtain) ->setMainColumn( array( $operations_box, $info_view, $details, $diff_detail_box, $unit_box, $timeline, $signature_message, )) ->setFooter($footer); $page = $this->newPage() ->setTitle($monogram.' '.$revision->getTitle()) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($revision->getPHID())) ->appendChild($view); if ($nav) { $page->setNavigation($nav); } return $page; } private function buildHeader(DifferentialRevision $revision) { $view = id(new PHUIHeaderView()) ->setHeader($revision->getTitle($revision)) ->setUser($this->getViewer()) ->setPolicyObject($revision) ->setHeaderIcon('fa-cog'); $status = $revision->getStatus(); $status_name = DifferentialRevisionStatus::renderFullDescription($status); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); return $view; } private function buildSubheaderView(DifferentialRevision $revision) { $viewer = $this->getViewer(); $author_phid = $revision->getAuthorPHID(); $author = $viewer->renderHandle($author_phid)->render(); $date = phabricator_datetime($revision->getDateCreated(), $viewer); $author = phutil_tag('strong', array(), $author); $handles = $viewer->loadHandles(array($author_phid)); $image_uri = $handles[$author_phid]->getImageURI(); $image_href = $handles[$author_phid]->getURI(); $content = pht('Authored by %s on %s.', $author, $date); return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } private function buildDetails( DifferentialRevision $revision, $custom_fields) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); if ($custom_fields) { $custom_fields->appendFieldsToPropertyList( $revision, $viewer, $properties); } $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($properties); } private function buildCurtain(DifferentialRevision $revision) { $viewer = $this->getViewer(); $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); $curtain = $this->newCurtainView($revision); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $revision, PhabricatorPolicyCapability::CAN_EDIT); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref("/differential/revision/edit/{$revision_id}/") ->setName(pht('Edit Revision')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-upload') ->setHref("/differential/revision/update/{$revision_id}/") ->setName(pht('Update Diff')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $request_uri = $this->getRequest()->getRequestURI(); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-download') ->setName(pht('Download Raw Diff')) ->setHref($request_uri->alter('download', 'true'))); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $revision); $revision_actions = array( DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY, DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY, ); $revision_submenu = $relationship_list->newActionSubmenu($revision_actions) ->setName(pht('Edit Related Revisions...')) ->setIcon('fa-cog'); $curtain->addAction($revision_submenu); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } return $curtain; } private function loadHistoryDiffStatus(array $diffs) { assert_instances_of($diffs, 'DifferentialDiff'); $diff_phids = mpull($diffs, 'getPHID'); $bad_unit_status = array( ArcanistUnitTestResult::RESULT_FAIL, ArcanistUnitTestResult::RESULT_BROKEN, ); $message = new HarbormasterBuildUnitMessage(); $target = new HarbormasterBuildTarget(); $build = new HarbormasterBuild(); $buildable = new HarbormasterBuildable(); $broken_diffs = queryfx_all( $message->establishConnection('r'), 'SELECT distinct a.buildablePHID FROM %T m JOIN %T t ON m.buildTargetPHID = t.phid JOIN %T b ON t.buildPHID = b.phid JOIN %T a ON b.buildablePHID = a.phid WHERE a.buildablePHID IN (%Ls) AND m.result in (%Ls)', $message->getTableName(), $target->getTableName(), $build->getTableName(), $buildable->getTableName(), $diff_phids, $bad_unit_status); $unit_status = array(); foreach ($broken_diffs as $broken) { $phid = $broken['buildablePHID']; $unit_status[$phid] = DifferentialUnitStatus::UNIT_FAIL; } return $unit_status; } private function loadChangesetsAndVsMap( DifferentialDiff $target, DifferentialDiff $diff_vs = null, PhabricatorRepository $repository = null) { $load_diffs = array($target); if ($diff_vs) { $load_diffs[] = $diff_vs; } $raw_changesets = id(new DifferentialChangesetQuery()) ->setViewer($this->getRequest()->getUser()) ->withDiffs($load_diffs) ->execute(); $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 buildSymbolIndexes( PhabricatorRepository $repository, array $visible_changesets) { assert_instances_of($visible_changesets, 'DifferentialChangeset'); $engine = PhabricatorSyntaxHighlighter::newEngine(); $langs = $repository->getSymbolLanguages(); $langs = nonempty($langs, array()); $sources = $repository->getSymbolSources(); $sources = nonempty($sources, array()); $symbol_indexes = array(); if ($langs && $sources) { $have_symbols = id(new DiffusionSymbolQuery()) ->existsSymbolsInRepository($repository->getPHID()); if (!$have_symbols) { return $symbol_indexes; } } $repository_phids = array_merge( array($repository->getPHID()), $sources); $indexed_langs = array_fill_keys($langs, true); foreach ($visible_changesets as $key => $changeset) { $lang = $engine->getLanguageFromFilename($changeset->getFilename()); if (empty($indexed_langs) || isset($indexed_langs[$lang])) { $symbol_indexes[$key] = array( 'lang' => $lang, 'repositories' => $repository_phids, ); } } return $symbol_indexes; } 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(); } $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); $query = id(new DifferentialRevisionQuery()) ->setViewer($this->getRequest()->getUser()) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) ->needFlags(true) ->needDrafts(true) - ->needRelationships(true); + ->needReviewerStatus(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'); $viewer = $this->getViewer(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Similar Revisions')); $view = id(new DifferentialRevisionListView()) ->setRevisions($revisions) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setNoBox(true) ->setUser($viewer); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return $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->getViewer(); id(new DifferentialHunkQuery()) ->setViewer($viewer) ->withChangesets($changesets) ->needAttachToChangesets(true) ->execute(); $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($revision->getPHID()); unset($unguarded); return $file->getRedirectResponse(); } private function buildTransactions( DifferentialRevision $revision, DifferentialDiff $left_diff, DifferentialDiff $right_diff, array $old_ids, array $new_ids) { $timeline = $this->buildTransactionTimeline( $revision, new DifferentialTransactionQuery(), $engine = null, array( 'left' => $left_diff->getID(), 'right' => $right_diff->getID(), 'old' => implode(',', $old_ids), 'new' => implode(',', $new_ids), )); return $timeline; } private function buildRevisionWarnings( DifferentialRevision $revision, PhabricatorCustomFieldList $field_list, array $warning_handle_map, array $handles) { $warnings = array(); foreach ($field_list->getFields() as $key => $field) { $phids = idx($warning_handle_map, $key, array()); $field_handles = array_select_keys($handles, $phids); $field_warnings = $field->getWarningsForRevisionHeader($field_handles); foreach ($field_warnings as $warning) { $warnings[] = $warning; } } return $warnings; } private function buildDiffDetailView( array $diffs, DifferentialRevision $revision, PhabricatorCustomFieldList $field_list) { $viewer = $this->getViewer(); $fields = array(); foreach ($field_list->getFields() as $field) { if ($field->shouldAppearInDiffPropertyView()) { $fields[] = $field; } } if (!$fields) { return null; } $property_lists = array(); foreach ($this->getDiffTabLabels($diffs) as $tab) { list($label, $diff) = $tab; $property_lists[] = array( $label, $this->buildDiffPropertyList($diff, $revision, $fields), ); } $tab_group = id(new PHUITabGroupView()) ->setHideSingleTab(true); foreach ($property_lists as $key => $property_list) { list($tab_name, $list_view) = $property_list; $tab = id(new PHUITabView()) ->setKey($key) ->setName($tab_name) ->appendChild($list_view); $tab_group->addTab($tab); $tab_group->selectTab($key); } return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Diff Detail')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUser($viewer) ->addTabGroup($tab_group); } private function buildDiffPropertyList( DifferentialDiff $diff, DifferentialRevision $revision, array $fields) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($diff); foreach ($fields as $field) { $label = $field->renderDiffPropertyViewLabel($diff); $value = $field->renderDiffPropertyViewValue($diff); if ($value !== null) { $view->addProperty($label, $value); } } return $view; } private function buildOperationsBox(DifferentialRevision $revision) { $viewer = $this->getViewer(); // Save a query if we can't possibly have pending operations. $repository = $revision->getRepository(); if (!$repository || !$repository->canPerformAutomation()) { return null; } $operations = id(new DrydockRepositoryOperationQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($revision->getPHID())) ->withIsDismissed(false) ->withOperationTypes( array( DrydockLandRepositoryOperation::OPCONST, )) ->execute(); if (!$operations) { return null; } $state_fail = DrydockRepositoryOperation::STATE_FAIL; // We're going to show the oldest operation which hasn't failed, or the // most recent failure if they're all failures. $operations = msort($operations, 'getID'); foreach ($operations as $operation) { if ($operation->getOperationState() != $state_fail) { break; } } // If we found a completed operation, don't render anything. We don't want // to show an older error after the thing worked properly. if ($operation->isDone()) { return null; } $box_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Active Operations')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); return id(new DrydockRepositoryOperationStatusView()) ->setUser($viewer) ->setBoxView($box_view) ->setOperation($operation); } private function buildUnitMessagesView( $diff, DifferentialRevision $revision) { $viewer = $this->getViewer(); if (!$diff->getBuildable()) { return null; } if (!$diff->getUnitMessages()) { return null; } $interesting_messages = array(); foreach ($diff->getUnitMessages() as $message) { switch ($message->getResult()) { case ArcanistUnitTestResult::RESULT_PASS: case ArcanistUnitTestResult::RESULT_SKIP: break; default: $interesting_messages[] = $message; break; } } if (!$interesting_messages) { return null; } $excuse = null; if ($diff->hasDiffProperty('arc:unit-excuse')) { $excuse = $diff->getProperty('arc:unit-excuse'); } return id(new HarbormasterUnitSummaryView()) ->setUser($viewer) ->setExcuse($excuse) ->setBuildable($diff->getBuildable()) ->setUnitMessages($diff->getUnitMessages()) ->setLimit(5) ->setShowViewAll(true); } } diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php index 23ad165ca9..135e9b560d 100644 --- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php +++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php @@ -1,89 +1,90 @@ getFeedStory(); $action = $story->getStoryData()->getValue('action'); return ($action == DifferentialAction::ACTION_CREATE); } public function isStoryAboutObjectClosure($object) { $story = $this->getFeedStory(); $action = $story->getStoryData()->getValue('action'); return ($action == DifferentialAction::ACTION_CLOSE) || ($action == DifferentialAction::ACTION_ABANDON); } public function willPublishStory($object) { return id(new DifferentialRevisionQuery()) ->setViewer($this->getViewer()) ->withIDs(array($object->getID())) - ->needRelationships(true) + ->needReviewerStatus(true) ->executeOne(); } public function getOwnerPHID($object) { return $object->getAuthorPHID(); } public function getActiveUserPHIDs($object) { $status = $object->getStatus(); if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { - return $object->getReviewers(); + return $object->getReviewerPHIDs(); } else { return array(); } } public function getPassiveUserPHIDs($object) { $status = $object->getStatus(); if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { return array(); } else { - return $object->getReviewers(); + return $object->getReviewerPHIDs(); } } public function getCCUserPHIDs($object) { - return $object->getCCPHIDs(); + return PhabricatorSubscribersQuery::loadSubscribersForPHID( + $object->getPHID()); } public function getObjectTitle($object) { $id = $object->getID(); $title = $object->getTitle(); return "D{$id}: {$title}"; } public function getObjectURI($object) { return PhabricatorEnv::getProductionURI('/D'.$object->getID()); } public function getObjectDescription($object) { return $object->getSummary(); } public function isObjectClosed($object) { return $object->isClosed(); } public function getResponsibilityTitle($object) { $prefix = $this->getTitlePrefix($object); return pht('%s Review Request', $prefix); } private function getTitlePrefix(DifferentialRevision $revision) { $prefix_key = 'metamta.differential.subject-prefix'; return PhabricatorEnv::getEnvConfig($prefix_key); } } diff --git a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php index 53fd62fe23..19d15c5c3f 100644 --- a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php @@ -1,166 +1,165 @@ loadActiveDiff()); } protected function initializeNewAdapter() { $this->revision = $this->newObject(); } public function getObject() { return $this->revision; } public function getAdapterContentType() { return 'differential'; } public function getAdapterContentName() { return pht('Differential Revisions'); } public function getAdapterContentDescription() { return pht( "React to revisions being created or updated.\n". "Revision rules can send email, flag revisions, add reviewers, ". "and run build plans."); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return true; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: default: return false; } } public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, HeraldRepetitionPolicyConfig::FIRST, ); } public static function newLegacyAdapter( DifferentialRevision $revision, DifferentialDiff $diff) { $object = new HeraldDifferentialRevisionAdapter(); // Reload the revision to pick up relationship information. $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision->getID())) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); $object->revision = $revision; $object->setDiff($diff); return $object; } public function getHeraldName() { return $this->revision->getTitle(); } protected function loadChangesets() { if ($this->changesets === null) { $this->changesets = $this->getDiff()->loadChangesets(); } return $this->changesets; } protected function loadChangesetsWithHunks() { $changesets = $this->loadChangesets(); if ($changesets && !$this->haveHunks) { $this->haveHunks = true; id(new DifferentialHunkQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withChangesets($changesets) ->needAttachToChangesets(true) ->execute(); } return $changesets; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $this->affectedPackages = array(); $repository = $this->loadRepository(); if ($repository) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $repository, $this->loadAffectedPaths()); $this->affectedPackages = $packages; } } return $this->affectedPackages; } public function loadReviewers() { $reviewers = $this->getObject()->getReviewerStatus(); return mpull($reviewers, 'getReviewerPHID'); } /* -( HarbormasterBuildableAdapterInterface )------------------------------ */ public function getHarbormasterBuildablePHID() { return $this->getDiff()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getObject()->getPHID(); } public function getQueuedHarbormasterBuildRequests() { return $this->buildRequests; } public function queueHarbormasterBuildRequest( HarbormasterBuildRequest $request) { $this->buildRequests[] = $request; } } diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index c3fb98e607..1b60df777a 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -1,1110 +1,1057 @@ 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'; const STATUS_ABANDONED = 'status-abandoned'; private $authors = array(); private $draftAuthors = array(); private $ccs = array(); private $reviewers = array(); private $revIDs = array(); private $commitHashes = array(); private $commitPHIDs = array(); private $phids = array(); private $responsibles = array(); private $branches = array(); private $repositoryPHIDs; private $updatedEpochMin; private $updatedEpochMax; const ORDER_MODIFIED = 'order-modified'; const ORDER_CREATED = 'order-created'; - private $needRelationships = false; private $needActiveDiffs = false; private $needDiffIDs = false; private $needCommitPHIDs = false; private $needHashes = false; private $needReviewerStatus = false; private $needReviewerAuthority; private $needDrafts; private $needFlags; 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 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 that have one of the provided PHIDs as * commits. Calling this function will clear anything set by previous calls * to @{method:withCommitPHIDs}. * * @param array List of PHIDs of commits * @return this * @task config */ public function withCommitPHIDs(array $commit_phids) { $this->commitPHIDs = $commit_phids; 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; } public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } public function withUpdatedEpochBetween($min, $max) { $this->updatedEpochMin = $min; $this->updatedEpochMax = $max; 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; } public function needFlags($need_flags) { $this->needFlags = $need_flags; return $this; } public function needDrafts($need_drafts) { $this->needDrafts = $need_drafts; return $this; } /* -( Query Execution )---------------------------------------------------- */ public function newResultObject() { return new DifferentialRevision(); } /** * Execute the query as configured, returning matching * @{class:DifferentialRevision} objects. * * @return list List of matching DifferentialRevision objects. * @task exec */ protected function loadPage() { $data = $this->loadData(); $table = $this->newResultObject(); return $table->loadAllFromArray($data); } protected 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; } if ($this->getViewer()->isOmnipotent()) { // The viewer is omnipotent. Allow the revision to load even without // 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 || $this->needReviewerAuthority) { $this->loadReviewers($conn_r, $revisions); } return $revisions; } protected function didFilterPage(array $revisions) { $viewer = $this->getViewer(); if ($this->needFlags) { $flags = id(new PhabricatorFlagQuery()) ->setViewer($viewer) ->withOwnerPHIDs(array($viewer->getPHID())) ->withObjectPHIDs(mpull($revisions, 'getPHID')) ->execute(); $flags = mpull($flags, null, 'getObjectPHID'); foreach ($revisions as $revision) { $revision->attachFlag( $viewer, idx($flags, $revision->getPHID())); } } if ($this->needDrafts) { PhabricatorDraftEngine::attachDrafts( $viewer, $revisions); } return $revisions; } private function loadData() { $table = $this->newResultObject(); $conn_r = $table->establishConnection('r'); $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; 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); $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 = $this->buildSelectClause($conn_r); $from = qsprintf( $conn_r, 'FROM %T r', $table->getTableName()); $joins = $this->buildJoinsClause($conn_r); $where = $this->buildWhereClause($conn_r); $group_by = $this->buildGroupClause($conn_r); $having = $this->buildHavingClause($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 %Q %Q)', $select, $from, $joins, $where, $group_by, $having, $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 e_ccs ON e_ccs.src = r.phid '. 'AND e_ccs.type = %s '. 'AND e_ccs.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasSubscriberEdgeType::EDGECONST, $this->ccs); } if ($this->reviewers) { $joins[] = qsprintf( $conn_r, 'JOIN %T reviewer ON reviewer.revisionPHID = r.phid AND reviewer.reviewerStatus != %s AND reviewer.reviewerPHID in (%Ls)', id(new DifferentialReviewer())->getTableName(), DifferentialReviewerStatus::STATUS_RESIGNED, $this->reviewers); } if ($this->draftAuthors) { $joins[] = qsprintf( $conn_r, 'JOIN %T has_draft ON has_draft.srcPHID = r.phid AND has_draft.type = %s AND has_draft.dstPHID IN (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasDraftEdgeType::EDGECONST, $this->draftAuthors); } if ($this->commitPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T commits ON commits.revisionID = r.id', DifferentialRevision::TABLE_COMMIT); } $joins[] = $this->buildJoinClauseParts($conn_r); return $this->formatJoinClause($joins); } /** * @task internal */ protected function buildWhereClause(AphrontDatabaseConnection $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->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->commitPHIDs) { $where[] = qsprintf( $conn_r, 'commits.commitPHID IN (%Ls)', $this->commitPHIDs); } 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->updatedEpochMin !== null) { $where[] = qsprintf( $conn_r, 'r.dateModified >= %d', $this->updatedEpochMin); } if ($this->updatedEpochMax !== null) { $where[] = qsprintf( $conn_r, 'r.dateModified <= %d', $this->updatedEpochMax); } // NOTE: Although the status constants are integers in PHP, the column is a // string column in MySQL, and MySQL won't use keys on string columns if // you put integers in the query. switch ($this->status) { case self::STATUS_ANY: break; case self::STATUS_OPEN: $where[] = qsprintf( $conn_r, 'r.status IN (%Ls)', DifferentialRevisionStatus::getOpenStatuses()); break; case self::STATUS_NEEDS_REVIEW: $where[] = qsprintf( $conn_r, 'r.status IN (%Ls)', array( ArcanistDifferentialRevisionStatus::NEEDS_REVIEW, )); break; case self::STATUS_NEEDS_REVISION: $where[] = qsprintf( $conn_r, 'r.status IN (%Ls)', array( ArcanistDifferentialRevisionStatus::NEEDS_REVISION, )); break; case self::STATUS_ACCEPTED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ls)', array( ArcanistDifferentialRevisionStatus::ACCEPTED, )); break; case self::STATUS_CLOSED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ls)', DifferentialRevisionStatus::getClosedStatuses()); break; case self::STATUS_ABANDONED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ls)', array( ArcanistDifferentialRevisionStatus::ABANDONED, )); break; default: throw new Exception( pht("Unknown revision status filter constant '%s'!", $this->status)); } $where[] = $this->buildWhereClauseParts($conn_r); return $this->formatWhereClause($where); } /** * @task internal */ protected function shouldGroupQueryResultRows() { $join_triggers = array_merge( $this->pathIDs, $this->ccs, $this->reviewers); if (count($join_triggers) > 1) { return true; } return parent::shouldGroupQueryResultRows(); } public function getBuiltinOrders() { $orders = parent::getBuiltinOrders() + array( 'updated' => array( 'vector' => array('updated', 'id'), 'name' => pht('Date Updated (Latest First)'), 'aliases' => array(self::ORDER_MODIFIED), ), 'outdated' => array( 'vector' => array('-updated', '-id'), 'name' => pht('Date Updated (Oldest First)'), ), ); // Alias the "newest" builtin to the historical key for it. $orders['newest']['aliases'][] = self::ORDER_CREATED; return $orders; } protected function getDefaultOrderVector() { return array('updated', 'id'); } public function getOrderableColumns() { $primary = ($this->buildingGlobalOrder ? null : 'r'); return array( 'id' => array( 'table' => $primary, 'column' => 'id', 'type' => 'int', 'unique' => true, ), 'updated' => array( 'table' => $primary, 'column' => 'dateModified', 'type' => 'int', ), ); } protected function getPagingValueMap($cursor, array $keys) { $revision = $this->loadCursorObject($cursor); return array( 'id' => $revision->getID(), 'updated' => $revision->getDateModified(), ); } - private function loadRelationships($conn_r, array $revisions) { - assert_instances_of($revisions, 'DifferentialRevision'); - - $type_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST; - $type_subscriber = PhabricatorObjectHasSubscriberEdgeType::EDGECONST; - - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(mpull($revisions, 'getPHID')) - ->withEdgeTypes(array($type_reviewer, $type_subscriber)) - ->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST) - ->execute(); - - $type_map = array( - DifferentialRevision::RELATION_REVIEWER => $type_reviewer, - DifferentialRevision::RELATION_SUBSCRIBED => $type_subscriber, - ); - - foreach ($revisions as $revision) { - $data = array(); - foreach ($type_map as $rel_type => $edge_type) { - $revision_edges = $edges[$revision->getPHID()][$edge_type]; - foreach ($revision_edges as $dst_phid => $edge_data) { - $data[] = array( - 'relation' => $rel_type, - '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, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $reviewer_table = new DifferentialReviewer(); $reviewer_rows = queryfx_all( $conn, 'SELECT * FROM %T WHERE revisionPHID IN (%Ls) ORDER BY id ASC', $reviewer_table->getTableName(), mpull($revisions, 'getPHID')); $reviewer_list = $reviewer_table->loadAllFromArray($reviewer_rows); $reviewer_map = mgroup($reviewer_list, 'getRevisionPHID'); foreach ($reviewer_map as $key => $reviewers) { $reviewer_map[$key] = mpull($reviewers, null, 'getReviewerPHID'); } $viewer = $this->getViewer(); $viewer_phid = $viewer->getPHID(); $allow_key = 'differential.allow-self-accept'; $allow_self = PhabricatorEnv::getEnvConfig($allow_key); // Figure out which of these reviewers the viewer has authority to act as. if ($this->needReviewerAuthority && $viewer_phid) { $authority = $this->loadReviewerAuthority( $revisions, $reviewer_map, $allow_self); } foreach ($revisions as $revision) { $reviewers = idx($reviewer_map, $revision->getPHID(), array()); foreach ($reviewers as $reviewer_phid => $reviewer) { 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 the viewer has authority. $has_authority = isset($authority[$reviewer_phid]); } $reviewer->attachAuthority($viewer, $has_authority); } $reviewers[$reviewer_phid] = $reviewer; } $revision->attachReviewerStatus($reviewers); } } private function loadReviewerAuthority( array $revisions, array $reviewers, $allow_self) { $revision_map = mpull($revisions, null, 'getPHID'); $viewer_phid = $this->getViewer()->getPHID(); // Find all the project/package reviewers which the user may have authority // over. $project_phids = array(); $package_phids = array(); $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; $package_type = PhabricatorOwnersPackagePHIDType::TYPECONST; $edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST; foreach ($reviewers as $revision_phid => $reviewer_list) { if (!$allow_self) { if ($revision_map[$revision_phid]->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; } } foreach ($reviewer_list as $reviewer_phid => $reviewer) { $phid_type = phid_get_type($reviewer_phid); if ($phid_type == $project_type) { $project_phids[] = $reviewer_phid; } if ($phid_type == $package_type) { $package_phids[] = $reviewer_phid; } } } // The viewer has authority over themselves. $user_authority = array_fuse(array($viewer_phid)); // And over any projects they are 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'); $project_authority = array_fuse($project_authority); } // And over any packages they own. $package_authority = array(); if ($package_phids) { $package_authority = id(new PhabricatorOwnersPackageQuery()) ->setViewer($this->getViewer()) ->withPHIDs($package_phids) ->withAuthorityPHIDs(array($viewer_phid)) ->execute(); $package_authority = mpull($package_authority, 'getPHID'); $package_authority = array_fuse($package_authority); } return $user_authority + $project_authority + $package_authority; } public function getQueryApplicationClass() { return 'PhabricatorDifferentialApplication'; } protected function getPrimaryTableAlias() { return 'r'; } } diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index 9212860292..2c40be4813 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -1,281 +1,280 @@ needFlags(true) ->needDrafts(true) - ->needRelationships(true) ->needReviewerStatus(true); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['responsiblePHIDs']) { $query->withResponsibleUsers($map['responsiblePHIDs']); } if ($map['authorPHIDs']) { $query->withAuthors($map['authorPHIDs']); } if ($map['reviewerPHIDs']) { $query->withReviewers($map['reviewerPHIDs']); } if ($map['repositoryPHIDs']) { $query->withRepositoryPHIDs($map['repositoryPHIDs']); } if ($map['status']) { $query->withStatus($map['status']); } return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Responsible Users')) ->setKey('responsiblePHIDs') ->setAliases(array('responsiblePHID', 'responsibles', 'responsible')) ->setDatasource(new DifferentialResponsibleDatasource()) ->setDescription( pht('Find revisions that a given user is responsible for.')), id(new PhabricatorUsersSearchField()) ->setLabel(pht('Authors')) ->setKey('authorPHIDs') ->setAliases(array('author', 'authors', 'authorPHID')) ->setDescription( pht('Find revisions with specific authors.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Reviewers')) ->setKey('reviewerPHIDs') ->setAliases(array('reviewer', 'reviewers', 'reviewerPHID')) ->setDatasource(new DiffusionAuditorFunctionDatasource()) ->setDescription( pht('Find revisions with specific reviewers.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Repositories')) ->setKey('repositoryPHIDs') ->setAliases(array('repository', 'repositories', 'repositoryPHID')) ->setDatasource(new DiffusionRepositoryFunctionDatasource()) ->setDescription( pht('Find revisions from specific repositories.')), id(new PhabricatorSearchSelectField()) ->setLabel(pht('Status')) ->setKey('status') ->setOptions($this->getStatusOptions()) ->setDescription( pht('Find revisions with particular statuses.')), ); } protected function getURI($path) { return '/differential/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['active'] = pht('Active Revisions'); $names['authored'] = pht('Authored'); } $names['all'] = pht('All Revisions'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer = $this->requireViewer(); switch ($query_key) { case 'active': $bucket_key = DifferentialRevisionRequiredActionResultBucket::BUCKETKEY; return $query ->setParameter('responsiblePHIDs', array($viewer->getPHID())) ->setParameter('status', DifferentialRevisionQuery::STATUS_OPEN) ->setParameter('bucket', $bucket_key); case 'authored': return $query ->setParameter('authorPHIDs', array($viewer->getPHID())); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } private function getStatusOptions() { return array( DifferentialRevisionQuery::STATUS_ANY => pht('All'), DifferentialRevisionQuery::STATUS_OPEN => pht('Open'), DifferentialRevisionQuery::STATUS_ACCEPTED => pht('Accepted'), DifferentialRevisionQuery::STATUS_NEEDS_REVIEW => pht('Needs Review'), DifferentialRevisionQuery::STATUS_NEEDS_REVISION => pht('Needs Revision'), DifferentialRevisionQuery::STATUS_CLOSED => pht('Closed'), DifferentialRevisionQuery::STATUS_ABANDONED => pht('Abandoned'), ); } protected function renderResultList( array $revisions, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($revisions, 'DifferentialRevision'); $viewer = $this->requireViewer(); $template = id(new DifferentialRevisionListView()) ->setUser($viewer) ->setNoBox($this->isPanelContext()); $bucket = $this->getResultBucket($query); $unlanded = $this->loadUnlandedDependencies($revisions); $views = array(); if ($bucket) { $bucket->setViewer($viewer); try { $groups = $bucket->newResultGroups($query, $revisions); foreach ($groups as $group) { $views[] = id(clone $template) ->setHeader($group->getName()) ->setNoDataString($group->getNoDataString()) ->setRevisions($group->getObjects()); } } catch (Exception $ex) { $this->addError($ex->getMessage()); } } else { $views[] = id(clone $template) ->setRevisions($revisions) ->setHandles(array()); } $phids = array_mergev(mpull($views, 'getRequiredHandlePHIDs')); if ($phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); } else { $handles = array(); } foreach ($views as $view) { $view->setHandles($handles); $view->setUnlandedDependencies($unlanded); } if (count($views) == 1) { // Reduce this to a PHUIObjectItemListView so we can get the free // support from ApplicationSearch. $list = head($views)->render(); } else { $list = $views; } $result = new PhabricatorApplicationSearchResultView(); $result->setContent($list); return $result; } protected function getNewUserBody() { $create_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Create a Diff')) ->setHref('/differential/diff/create/') ->setColor(PHUIButtonView::GREEN); $icon = $this->getApplication()->getIcon(); $app_name = $this->getApplication()->getName(); $view = id(new PHUIBigInfoView()) ->setIcon($icon) ->setTitle(pht('Welcome to %s', $app_name)) ->setDescription( pht('Pre-commit code review. Revisions that are waiting on your input '. 'will appear here.')) ->addAction($create_button); return $view; } private function loadUnlandedDependencies(array $revisions) { $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; $phids = array(); foreach ($revisions as $revision) { if ($revision->getStatus() != $status_accepted) { continue; } $phids[] = $revision->getPHID(); } if (!$phids) { return array(); } $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, )); $query->execute(); $revision_phids = $query->getDestinationPHIDs(); if (!$revision_phids) { return array(); } $viewer = $this->requireViewer(); $blocking_revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withPHIDs($revision_phids) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->execute(); $blocking_revisions = mpull($blocking_revisions, null, 'getPHID'); $result = array(); foreach ($revisions as $revision) { $revision_phid = $revision->getPHID(); $blocking_phids = $query->getDestinationPHIDs(array($revision_phid)); $blocking = array_select_keys($blocking_revisions, $blocking_phids); if ($blocking) { $result[$revision_phid] = $blocking; } } return $result; } } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index ff826a36b7..062503451f 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,737 +1,682 @@ setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) - ->attachRelationships(array()) ->attachRepository(null) ->attachActiveDiff(null) ->attachReviewerStatus(array()) ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'originalTitle' => 'text255', 'status' => 'text32', 'summary' => 'text', 'testPlan' => 'text', 'authorPHID' => 'phid?', 'lastReviewerPHID' => 'phid?', 'lineCount' => 'uint32?', 'mailKey' => 'bytes40', 'branchName' => 'text255?', 'repositoryPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'repositoryPHID' => array( 'columns' => array('repositoryPHID'), ), // If you (or a project you are a member of) is reviewing a significant // fraction of the revisions on an install, the result set of open // revisions may be smaller than the result set of revisions where you // are a reviewer. In these cases, this key is better than keys on the // edge table. 'key_status' => array( 'columns' => array('status', 'phid'), ), ), ) + parent::getConfiguration(); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function hasRevisionProperty($key) { return array_key_exists($key, $this->properties); } public function getMonogram() { $id = $this->getID(); return "D{$id}"; } public function getURI() { return '/'.$this->getMonogram(); } public function setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } public function loadIDsByCommitPHIDs($phids) { if (!$phids) { return array(); } $revision_ids = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE commitPHID IN (%Ls)', self::TABLE_COMMIT, $phids); return ipull($revision_ids, 'revisionID', 'commitPHID'); } public function loadCommitPHIDs() { if (!$this->getID()) { return ($this->commits = array()); } $commits = queryfx_all( $this->establishConnection('r'), 'SELECT commitPHID FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $commits = ipull($commits, 'commitPHID'); return ($this->commits = $commits); } public function getCommitPHIDs() { return $this->assertAttached($this->commits); } public function getActiveDiff() { // TODO: Because it's currently technically possible to create a revision // without an associated diff, we allow an attached-but-null active diff. // It would be good to get rid of this once we make diff-attaching // transactional. return $this->assertAttached($this->activeDiff); } public function attachActiveDiff($diff) { $this->activeDiff = $diff; return $this; } public function getDiffIDs() { return $this->assertAttached($this->diffIDs); } public function attachDiffIDs(array $ids) { rsort($ids); $this->diffIDs = array_values($ids); return $this; } public function attachCommitPHIDs(array $phids) { $this->commits = array_values($phids); return $this; } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function setAttachedPHIDs($type, array $phids) { $this->attached[$type] = array_fill_keys($phids, array()); return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialRevisionPHIDType::TYPECONST); } public function loadActiveDiff() { return id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d ORDER BY id DESC LIMIT 1', $this->getID()); } public function save() { if (!$this->getMailKey()) { $this->mailKey = Filesystem::readRandomCharacters(40); } return parent::save(); } - public function loadRelationships() { - if (!$this->getID()) { - $this->relationships = array(); - return; - } - - $data = array(); - - $subscriber_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->getPHID(), - PhabricatorObjectHasSubscriberEdgeType::EDGECONST); - $subscriber_phids = array_reverse($subscriber_phids); - foreach ($subscriber_phids as $phid) { - $data[] = array( - 'relation' => self::RELATION_SUBSCRIBED, - 'objectPHID' => $phid, - 'reasonPHID' => null, - ); - } - - $reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->getPHID(), - DifferentialRevisionHasReviewerEdgeType::EDGECONST); - $reviewer_phids = array_reverse($reviewer_phids); - foreach ($reviewer_phids as $phid) { - $data[] = array( - 'relation' => self::RELATION_REVIEWER, - 'objectPHID' => $phid, - 'reasonPHID' => null, - ); - } - - return $this->attachRelationships($data); - } - - public function attachRelationships(array $relationships) { - $this->relationships = igroup($relationships, 'relation'); - return $this; - } - - public function getReviewers() { - return $this->getRelatedPHIDs(self::RELATION_REVIEWER); - } - - public function getCCPHIDs() { - return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED); - } - - private function getRelatedPHIDs($relation) { - $this->assertAttached($this->relationships); - - return ipull($this->getRawRelations($relation), 'objectPHID'); - } - - public function getRawRelations($relation) { - return idx($this->relationships, $relation, array()); - } - public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A revision's author (which effectively means "owner" after we added // commandeering) can always view and edit it. $author_phid = $this->getAuthorPHID(); if ($author_phid) { if ($user->getPHID() == $author_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { $description = array( pht('The owner of a revision can always view and edit it.'), ); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $description[] = pht("A revision's reviewers can always view it."); $description[] = pht( 'If a revision belongs to a repository, other users must be able '. 'to view the repository in order to view the revision.'); break; } return $description; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $repository_phid = $this->getRepositoryPHID(); $repository = $this->getRepository(); // Try to use the object if we have it, since it will save us some // data fetching later on. In some cases, we might not have it. $repository_ref = nonempty($repository, $repository_phid); if ($repository_ref) { $extended[] = array( $repository_ref, PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } public function getReviewerStatus() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewerStatus(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); $this->reviewerStatus = $reviewers; return $this; } + public function getReviewerPHIDs() { + $reviewers = $this->getReviewerStatus(); + return mpull($reviewers, 'getReviewerPHID'); + } + public function getReviewerPHIDsForEdit() { $reviewers = $this->getReviewerStatus(); $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; $value = array(); foreach ($reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); if ($reviewer->getStatus() == $status_blocking) { $value[] = 'blocking('.$phid.')'; } else { $value[] = $phid; } } return $value; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } public function isClosed() { return DifferentialRevisionStatus::isClosedStatus($this->getStatus()); } public function isAbandoned() { $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; return ($this->getStatus() == $status_abandoned); } public function isAccepted() { $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; return ($this->getStatus() == $status_accepted); } public function getStatusIcon() { $map = array( ArcanistDifferentialRevisionStatus::NEEDS_REVIEW => 'fa-code grey', ArcanistDifferentialRevisionStatus::NEEDS_REVISION => 'fa-refresh red', ArcanistDifferentialRevisionStatus::CHANGES_PLANNED => 'fa-headphones red', ArcanistDifferentialRevisionStatus::ACCEPTED => 'fa-check green', ArcanistDifferentialRevisionStatus::CLOSED => 'fa-check-square-o black', ArcanistDifferentialRevisionStatus::ABANDONED => 'fa-plane black', ); return idx($map, $this->getStatus()); } public function getStatusDisplayName() { $status = $this->getStatus(); return ArcanistDifferentialRevisionStatus::getNameForRevisionStatus( $status); } public function getFlag(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->flags, $viewer->getPHID()); } public function attachFlag( PhabricatorUser $viewer, PhabricatorFlag $flag = null) { $this->flags[$viewer->getPHID()] = $flag; return $this; } public function getHasDraft(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment()); } public function attachHasDraft(PhabricatorUser $viewer, $has_draft) { $this->drafts[$viewer->getCacheFragment()] = $has_draft; return $this; } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterContainerPHID(); } public function getHarbormasterBuildablePHID() { return $this->loadActiveDiff()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getPHID(); } public function getHarbormasterPublishablePHID() { return $this->getPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { if ($phid == $this->getAuthorPHID()) { return true; } // TODO: This only happens when adding or removing CCs, and is safe from a // policy perspective, but the subscription pathway should have some // opportunity to load this data properly. For now, this is the only case // where implicit subscription is not an intrinsic property of the object. if ($this->reviewerStatus == self::ATTACHABLE) { $reviewers = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->needReviewerStatus(true) ->executeOne() ->getReviewerStatus(); } else { $reviewers = $this->getReviewerStatus(); } foreach ($reviewers as $reviewer) { if ($reviewer->getReviewerPHID() == $phid) { return true; } } return false; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('differential.fields'); } public function getCustomFieldBaseClass() { return 'DifferentialCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DifferentialTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { $viewer = $request->getViewer(); $render_data = $timeline->getRenderData(); $left = $request->getInt('left', idx($render_data, 'left')); $right = $request->getInt('right', idx($render_data, 'right')); $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withIDs(array($left, $right)) ->execute(); $diffs = mpull($diffs, null, 'getID'); $left_diff = $diffs[$left]; $right_diff = $diffs[$right]; $old_ids = $request->getStr('old', idx($render_data, 'old')); $new_ids = $request->getStr('new', idx($render_data, 'new')); $old_ids = array_filter(explode(',', $old_ids)); $new_ids = array_filter(explode(',', $new_ids)); $type_inline = DifferentialTransaction::TYPE_INLINE; $changeset_ids = array_merge($old_ids, $new_ids); $inlines = array(); foreach ($timeline->getTransactions() as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction->getComment(); $changeset_ids[] = $xaction->getComment()->getChangesetID(); } } if ($changeset_ids) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($request->getUser()) ->withIDs($changeset_ids) ->execute(); $changesets = mpull($changesets, null, 'getID'); } else { $changesets = array(); } foreach ($inlines as $key => $inline) { $inlines[$key] = DifferentialInlineComment::newFromModernComment( $inline); } $query = id(new DifferentialInlineCommentQuery()) ->needHidden(true) ->setViewer($viewer); // NOTE: This is a bit sketchy: this method adjusts the inlines as a // side effect, which means it will ultimately adjust the transaction // comments and affect timeline rendering. $query->adjustInlinesForChangesets( $inlines, array_select_keys($changesets, $old_ids), array_select_keys($changesets, $new_ids), $this); return $timeline ->setChangesets($changesets) ->setRevision($this) ->setLeftDiff($left_diff) ->setRightDiff($right_diff); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $diffs = id(new DifferentialDiffQuery()) ->setViewer($engine->getViewer()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($diffs as $diff) { $engine->destroyObject($diff); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); // we have to do paths a little differentally as they do not have // an id or phid column for delete() to act on $dummy_path = new DifferentialAffectedPath(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $dummy_path->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DifferentialRevisionFulltextEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The revision title.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Revision author PHID.')), ); } public function getFieldValuesForConduit() { return array( 'title' => $this->getTitle(), 'authorPHID' => $this->getAuthorPHID(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DifferentialRevisionDraftEngine(); } } diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index 811e74fd65..5aacafbb69 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -1,180 +1,176 @@ unlandedDependencies = $unlanded_dependencies; return $this; } public function getUnlandedDependencies() { return $this->unlandedDependencies; } public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; } public function setHeader($header) { $this->header = $header; return $this; } public function setRevisions(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $this->revisions = $revisions; return $this; } public function setNoBox($box) { $this->noBox = $box; return $this; } public function setBackground($background) { $this->background = $background; return $this; } public function getRequiredHandlePHIDs() { $phids = array(); foreach ($this->revisions as $revision) { $phids[] = array($revision->getAuthorPHID()); - - // TODO: Switch to getReviewerStatus(), but not all callers pass us - // revisions with this data loaded. - $phids[] = $revision->getReviewers(); + $phids[] = $revision->getReviewerPHIDs(); } return array_mergev($phids); } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function render() { $viewer = $this->getViewer(); $this->initBehavior('phabricator-tooltips', array()); $this->requireResource('aphront-tooltip-css'); $list = new PHUIObjectItemListView(); foreach ($this->revisions as $revision) { $item = id(new PHUIObjectItemView()) ->setUser($viewer); $icons = array(); $phid = $revision->getPHID(); $flag = $revision->getFlag($viewer); if ($flag) { $flag_class = PhabricatorFlagColor::getCSSClass($flag->getColor()); $icons['flag'] = phutil_tag( 'div', array( 'class' => 'phabricator-flag-icon '.$flag_class, ), ''); } if ($revision->getHasDraft($viewer)) { $icons['draft'] = true; } $modified = $revision->getDateModified(); if (isset($icons['flag'])) { $item->addHeadIcon($icons['flag']); } $item->setObjectName('D'.$revision->getID()); $item->setHeader($revision->getTitle()); $item->setHref('/D'.$revision->getID()); if (isset($icons['draft'])) { $draft = id(new PHUIIconView()) ->setIcon('fa-comment yellow') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Unsubmitted Comments'), )); $item->addAttribute($draft); } // Author $author_handle = $this->handles[$revision->getAuthorPHID()]; $item->addByline(pht('Author: %s', $author_handle->renderLink())); $unlanded = idx($this->unlandedDependencies, $phid); if ($unlanded) { $item->addAttribute( array( id(new PHUIIconView())->setIcon('fa-chain-broken', 'red'), ' ', pht('Open Dependencies'), )); } $reviewers = array(); - // TODO: As above, this should be based on `getReviewerStatus()`. - foreach ($revision->getReviewers() as $reviewer) { + foreach ($revision->getReviewerPHIDs() as $reviewer) { $reviewers[] = $this->handles[$reviewer]->renderLink(); } if (!$reviewers) { $reviewers = phutil_tag('em', array(), pht('None')); } else { $reviewers = phutil_implode_html(', ', $reviewers); } $item->addAttribute(pht('Reviewers: %s', $reviewers)); $item->setEpoch($revision->getDateModified()); if ($revision->isClosed()) { $item->setDisabled(true); } $item->setStatusIcon( $revision->getStatusIcon(), $revision->getStatusDisplayName()); $list->addItem($item); } $list->setNoDataString($this->noDataString); if ($this->header && !$this->noBox) { $list->setFlush(true); $list = id(new PHUIObjectBoxView()) ->setBackground($this->background) ->setObjectList($list); if ($this->header instanceof PHUIHeaderView) { $list->setHeader($this->header); } else { $list->setHeaderText($this->header); } } else { $list->setHeader($this->header); } return $list; } } diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index aa8e370e25..dc0131356d 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1,2022 +1,2022 @@ loadDiffusionContext(); if ($response) { return $response; } $drequest = $this->getDiffusionRequest(); // Figure out if we're browsing a directory, a file, or a search result // list. $grep = $request->getStr('grep'); $find = $request->getStr('find'); if (strlen($grep) || strlen($find)) { return $this->browseSearch(); } $pager = id(new PHUIPagerView()) ->readFromRequest($request); $results = DiffusionBrowseResultSet::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.browsequery', array( 'path' => $drequest->getPath(), 'commit' => $drequest->getStableCommit(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, ))); $reason = $results->getReasonForEmptyResultSet(); $is_file = ($reason == DiffusionBrowseResultSet::REASON_IS_FILE); if ($is_file) { return $this->browseFile(); } else { $paths = $results->getPaths(); $paths = $pager->sliceResults($paths); $results->setPaths($paths); return $this->browseDirectory($results, $pager); } } private function browseSearch() { $drequest = $this->getDiffusionRequest(); $header = $this->buildHeaderView($drequest); $search_form = $this->renderSearchForm(); $search_results = $this->renderSearchResults(); $search_form = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Search')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($search_form); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter( array( $search_form, $search_results, )); return $this->newPage() ->setTitle( array( nonempty(basename($drequest->getPath()), '/'), $drequest->getRepository()->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild($view); } private function browseFile() { $viewer = $this->getViewer(); $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $before = $request->getStr('before'); if ($before) { return $this->buildBeforeResponse($before); } $path = $drequest->getPath(); $blame_key = PhabricatorDiffusionBlameSetting::SETTINGKEY; $show_blame = $request->getBool( 'blame', $viewer->getUserSetting($blame_key)); $view = $request->getStr('view'); if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) { $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer); $editor = id(new PhabricatorUserPreferencesEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xactions = array(); $xactions[] = $preferences->newTransaction($blame_key, $show_blame); $editor->applyTransactions($preferences, $xactions); $uri = $request->getRequestURI() ->alter('blame', null); return id(new AphrontRedirectResponse())->setURI($uri); } // We need the blame information if blame is on and this is an Ajax request. // If blame is on and this is a colorized request, we don't show blame at // first (we ajax it in afterward) so we don't need to query for it. $needs_blame = ($show_blame && $request->isAjax()); $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), ); $byte_limit = null; if ($view !== 'raw') { $byte_limit = PhabricatorFileStorageEngine::getChunkThreshold(); $time_limit = 10; $params += array( 'timeout' => $time_limit, 'byteLimit' => $byte_limit, ); } $response = $this->callConduitWithDiffusionRequest( 'diffusion.filecontentquery', $params); $hit_byte_limit = $response['tooHuge']; $hit_time_limit = $response['tooSlow']; $file_phid = $response['filePHID']; if ($hit_byte_limit) { $corpus = $this->buildErrorCorpus( pht( 'This file is larger than %s byte(s), and too large to display '. 'in the web UI.', phutil_format_bytes($byte_limit))); } else if ($hit_time_limit) { $corpus = $this->buildErrorCorpus( pht( 'This file took too long to load from the repository (more than '. '%s second(s)).', new PhutilNumber($time_limit))); } else { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception(pht('Failed to load content file!')); } if ($view === 'raw') { return $file->getRedirectResponse(); } $data = $file->loadFileData(); $lfs_ref = $this->getGitLFSRef($repository, $data); if ($lfs_ref) { if ($view == 'git-lfs') { $file = $this->loadGitLFSFile($lfs_ref); // Rename the file locally so we generate a better vanity URI for // it. In storage, it just has a name like "lfs-13f9a94c0923...", // since we don't get any hints about possible human-readable names // at upload time. $basename = basename($drequest->getPath()); $file->makeEphemeral(); $file->setName($basename); return $file->getRedirectResponse(); } else { $corpus = $this->buildGitLFSCorpus($lfs_ref); } } else if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) { $file_uri = $file->getBestURI(); if ($file->isViewableImage()) { $corpus = $this->buildImageCorpus($file_uri); } else { $corpus = $this->buildBinaryCorpus($file_uri, $data); } } else { $this->loadLintMessages(); $this->coverage = $drequest->loadCoverage(); // Build the content of the file. $corpus = $this->buildCorpus( $show_blame, $data, $needs_blame, $drequest, $path, $data); } } if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($corpus); } require_celerity_resource('diffusion-source-css'); // Render the page. $view = $this->buildCurtain($drequest); $curtain = $this->enrichCurtain( $view, $drequest, $show_blame); $properties = $this->buildPropertyView($drequest); $header = $this->buildHeaderView($drequest); $header->setHeaderIcon('fa-file-code-o'); $content = array(); $follow = $request->getStr('follow'); if ($follow) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_WARNING); $notice->setTitle(pht('Unable to Continue')); switch ($follow) { case 'first': $notice->appendChild( pht( 'Unable to continue tracing the history of this file because '. 'this commit is the first commit in the repository.')); break; case 'created': $notice->appendChild( pht( 'Unable to continue tracing the history of this file because '. 'this commit created the file.')); break; } $content[] = $notice; } $renamed = $request->getStr('renamed'); if ($renamed) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $notice->setTitle(pht('File Renamed')); $notice->appendChild( pht( 'File history passes through a rename from "%s" to "%s".', $drequest->getPath(), $renamed)); $content[] = $notice; } $content[] = $corpus; $content[] = $this->buildOpenRevisions(); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $crumbs->setBorder(true); $basename = basename($this->getDiffusionRequest()->getPath()); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $content, )); if ($properties) { $view->addPropertySection(pht('Details'), $properties); } $title = array($basename, $repository->getDisplayName()); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } public function browseDirectory( DiffusionBrowseResultSet $results, PHUIPagerView $pager) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $reason = $results->getReasonForEmptyResultSet(); $curtain = $this->buildCurtain($drequest); $details = $this->buildPropertyView($drequest); $header = $this->buildHeaderView($drequest); $header->setHeaderIcon('fa-folder-open'); $search_form = $this->renderSearchForm(); $empty_result = null; $browse_panel = null; $branch_panel = null; if (!$results->isValidResults()) { $empty_result = new DiffusionEmptyResultView(); $empty_result->setDiffusionRequest($drequest); $empty_result->setDiffusionBrowseResultSet($results); $empty_result->setView($request->getStr('view')); } else { $phids = array(); foreach ($results->getPaths() as $result) { $data = $result->getLastCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } } } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $browse_table = id(new DiffusionBrowseTableView()) ->setDiffusionRequest($drequest) ->setHandles($handles) ->setPaths($results->getPaths()) ->setUser($request->getUser()); $browse_header = id(new PHUIHeaderView()) ->setHeader(nonempty(basename($drequest->getPath()), '/')) ->setHeaderIcon('fa-folder-open'); $browse_panel = id(new PHUIObjectBoxView()) ->setHeader($browse_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($browse_table); $browse_panel->setShowHide( array(pht('Show Search')), pht('Hide Search'), $search_form, '#'); $path = $drequest->getPath(); $is_branch = (!strlen($path) && $repository->supportsBranchComparison()); if ($is_branch) { $branch_panel = $this->buildBranchTable(); } } $open_revisions = $this->buildOpenRevisions(); $readme = $this->renderDirectoryReadme($results); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $pager_box = $this->renderTablePagerBox($pager); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $branch_panel, $empty_result, $browse_panel, )) ->setFooter( array( $open_revisions, $readme, $pager_box, )); if ($details) { $view->addPropertySection(pht('Details'), $details); } return $this->newPage() ->setTitle(array( nonempty(basename($drequest->getPath()), '/'), $repository->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function renderSearchResults() { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $results = array(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $search_mode = null; switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $results = array(); break; default: if (strlen($this->getRequest()->getStr('grep'))) { $search_mode = 'grep'; $query_string = $request->getStr('grep'); $results = $this->callConduitWithDiffusionRequest( 'diffusion.searchquery', array( 'grep' => $query_string, 'commit' => $drequest->getStableCommit(), 'path' => $drequest->getPath(), 'limit' => $pager->getPageSize() + 1, 'offset' => $pager->getOffset(), )); } else { // Filename search. $search_mode = 'find'; $query_string = $request->getStr('find'); $results = $this->callConduitWithDiffusionRequest( 'diffusion.querypaths', array( 'pattern' => $query_string, 'commit' => $drequest->getStableCommit(), 'path' => $drequest->getPath(), 'limit' => $pager->getPageSize() + 1, 'offset' => $pager->getOffset(), )); } break; } $results = $pager->sliceResults($results); if ($search_mode == 'grep') { $table = $this->renderGrepResults($results, $query_string); $header = pht( 'File content matching "%s" under "%s"', $query_string, nonempty($drequest->getPath(), '/')); } else { $table = $this->renderFindResults($results); $header = pht( 'Paths matching "%s" under "%s"', $query_string, nonempty($drequest->getPath(), '/')); } $box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); $pager_box = $this->renderTablePagerBox($pager); return array($box, $pager_box); } private function renderGrepResults(array $results, $pattern) { $drequest = $this->getDiffusionRequest(); require_celerity_resource('phabricator-search-results-css'); $rows = array(); foreach ($results as $result) { list($path, $line, $string) = $result; $href = $drequest->generateURI(array( 'action' => 'browse', 'path' => $path, 'line' => $line, )); $matches = null; $count = @preg_match_all( '('.$pattern.')u', $string, $matches, PREG_OFFSET_CAPTURE); if (!$count) { $output = ltrim($string); } else { $output = array(); $cursor = 0; $length = strlen($string); foreach ($matches[0] as $match) { $offset = $match[1]; if ($cursor != $offset) { $output[] = array( 'text' => substr($string, $cursor, $offset), 'highlight' => false, ); } $output[] = array( 'text' => $match[0], 'highlight' => true, ); $cursor = $offset + strlen($match[0]); } if ($cursor != $length) { $output[] = array( 'text' => substr($string, $cursor), 'highlight' => false, ); } if ($output) { $output[0]['text'] = ltrim($output[0]['text']); } foreach ($output as $key => $segment) { if ($segment['highlight']) { $output[$key] = phutil_tag('strong', array(), $segment['text']); } else { $output[$key] = $segment['text']; } } } $string = phutil_tag( 'pre', array('class' => 'PhabricatorMonospaced phui-source-fragment'), $output); $path = Filesystem::readablePath($path, $drequest->getPath()); $rows[] = array( phutil_tag('a', array('href' => $href), $path), $line, $string, ); } $table = id(new AphrontTableView($rows)) ->setClassName('remarkup-code') ->setHeaders(array(pht('Path'), pht('Line'), pht('String'))) ->setColumnClasses(array('', 'n', 'wide')) ->setNoDataString( pht( 'The pattern you searched for was not found in the content of any '. 'files.')); return $table; } private function renderFindResults(array $results) { $drequest = $this->getDiffusionRequest(); $rows = array(); foreach ($results as $result) { $href = $drequest->generateURI(array( 'action' => 'browse', 'path' => $result, )); $readable = Filesystem::readablePath($result, $drequest->getPath()); $rows[] = array( phutil_tag('a', array('href' => $href), $readable), ); } $table = id(new AphrontTableView($rows)) ->setHeaders(array(pht('Path'))) ->setColumnClasses(array('wide')) ->setNoDataString( pht( 'The pattern you searched for did not match the names of any '. 'files.')); return $table; } private function loadLintMessages() { $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); if (!$branch || !$branch->getLintCommit()) { return; } $this->lintCommit = $branch->getLintCommit(); $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = ''; if ($drequest->getLint()) { $where = qsprintf( $conn, 'AND code = %s', $drequest->getLint()); } $this->lintMessages = queryfx_all( $conn, 'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s', PhabricatorRepository::TABLE_LINTMESSAGE, $branch->getID(), $where, '/'.$drequest->getPath()); } private function buildCorpus( $show_blame, $file_corpus, $needs_blame, DiffusionRequest $drequest, $path, $data) { $viewer = $this->getViewer(); $blame_timeout = 15; $blame_failed = false; $highlight_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; $blame_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; $can_highlight = (strlen($file_corpus) <= $highlight_limit); $can_blame = (strlen($file_corpus) <= $blame_limit); if ($needs_blame && $can_blame) { $blame = $this->loadBlame($path, $drequest->getCommit(), $blame_timeout); list($blame_list, $blame_commits) = $blame; if ($blame_list === null) { $blame_failed = true; $blame_list = array(); } } else { $blame_list = array(); $blame_commits = array(); } require_celerity_resource('syntax-highlighting-css'); if ($can_highlight) { $highlighted = PhabricatorSyntaxHighlighter::highlightWithFilename( $path, $file_corpus); } else { // Highlight as plain text to escape the content properly. $highlighted = PhabricatorSyntaxHighlighter::highlightWithLanguage( 'txt', $file_corpus); } $lines = phutil_split_lines($highlighted); $rows = $this->buildDisplayRows( $lines, $blame_list, $blame_commits, $show_blame); $corpus_table = javelin_tag( 'table', array( 'class' => 'diffusion-source remarkup-code PhabricatorMonospaced', 'sigil' => 'phabricator-source', ), $rows); if ($this->getRequest()->isAjax()) { return $corpus_table; } $id = celerity_generate_unique_node_id(); $repo = $drequest->getRepository(); $symbol_repos = nonempty($repo->getSymbolSources(), array()); $symbol_repos[] = $repo->getPHID(); $lang = last(explode('.', $drequest->getPath())); $repo_languages = $repo->getSymbolLanguages(); $repo_languages = nonempty($repo_languages, array()); $repo_languages = array_fill_keys($repo_languages, true); $needs_symbols = true; if ($repo_languages && $symbol_repos) { $have_symbols = id(new DiffusionSymbolQuery()) ->existsSymbolsInRepository($repo->getPHID()); if (!$have_symbols) { $needs_symbols = false; } } if ($needs_symbols && $repo_languages) { $needs_symbols = isset($repo_languages[$lang]); } if ($needs_symbols) { Javelin::initBehavior( 'repository-crossreference', array( 'container' => $id, 'lang' => $lang, 'repositories' => $symbol_repos, )); } $corpus = phutil_tag( 'div', array( 'id' => $id, ), $corpus_table); Javelin::initBehavior('load-blame', array('id' => $id)); $edit = $this->renderEditButton(); $file = $this->renderFileButton(); $header = id(new PHUIHeaderView()) ->setHeader(basename($this->getDiffusionRequest()->getPath())) ->setHeaderIcon('fa-file-code-o') ->addActionLink($edit) ->addActionLink($file); $corpus = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($corpus) ->setCollapsed(true); $messages = array(); if (!$can_highlight) { $messages[] = pht( 'This file is larger than %s, so syntax highlighting is disabled '. 'by default.', phutil_format_bytes($highlight_limit)); } if ($show_blame && !$can_blame) { $messages[] = pht( 'This file is larger than %s, so blame is disabled.', phutil_format_bytes($blame_limit)); } if ($blame_failed) { $messages[] = pht( 'Failed to load blame information for this file in %s second(s).', new PhutilNumber($blame_timeout)); } if ($messages) { $corpus->setInfoView( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($messages)); } return $corpus; } private function enrichCurtain( PHUICurtainView $curtain, DiffusionRequest $drequest, $show_blame) { $viewer = $this->getViewer(); $base_uri = $this->getRequest()->getRequestURI(); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Last Change')) ->setHref( $drequest->generateURI( array( 'action' => 'change', ))) ->setIcon('fa-backward')); if ($show_blame) { $blame_text = pht('Disable Blame'); $blame_icon = 'fa-exclamation-circle lightgreytext'; $blame_value = 0; } else { $blame_text = pht('Enable Blame'); $blame_icon = 'fa-exclamation-circle'; $blame_value = 1; } $curtain->addAction( id(new PhabricatorActionView()) ->setName($blame_text) ->setHref($base_uri->alter('blame', $blame_value)) ->setIcon($blame_icon) ->setUser($viewer) ->setRenderAsForm($viewer->isLoggedIn())); $href = null; if ($this->getRequest()->getStr('lint') !== null) { $lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages)); $href = $base_uri->alter('lint', null); } else if ($this->lintCommit === null) { $lint_text = pht('Lint not Available'); } else { $lint_text = pht( 'Show %d Lint Message(s)', count($this->lintMessages)); $href = $this->getDiffusionRequest()->generateURI(array( 'action' => 'browse', 'commit' => $this->lintCommit, ))->alter('lint', ''); } $curtain->addAction( id(new PhabricatorActionView()) ->setName($lint_text) ->setHref($href) ->setIcon('fa-exclamation-triangle') ->setDisabled(!$href)); $repository = $drequest->getRepository(); $owners = 'PhabricatorOwnersApplication'; if (PhabricatorApplication::isClassInstalled($owners)) { $package_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withControl( $repository->getPHID(), array( $drequest->getPath(), )); $package_query->execute(); $packages = $package_query->getControllingPackagesForPath( $repository->getPHID(), $drequest->getPath()); if ($packages) { $ownership = id(new PHUIStatusListView()) ->setUser($viewer); foreach ($packages as $package) { $icon = 'fa-list-alt'; $color = 'grey'; $item = id(new PHUIStatusItemView()) ->setIcon($icon, $color) ->setTarget($viewer->renderHandle($package->getPHID())); $ownership->addItem($item); } } else { $ownership = phutil_tag('em', array(), pht('None')); } $curtain->newPanel() ->setHeaderText(pht('Owners')) ->appendChild($ownership); } return $curtain; } private function renderEditButton() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $line = nonempty((int)$drequest->getLine(), 1); $editor_link = $user->loadEditorLink($path, $line, $repository); $template = $user->loadEditorLink($path, '%l', $repository); $button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Open in Editor')) ->setHref($editor_link) ->setIcon('fa-pencil') ->setID('editor_link') ->setMetadata(array('link_template' => $template)) ->setDisabled(!$editor_link); return $button; } private function renderFileButton($file_uri = null, $label = null) { $base_uri = $this->getRequest()->getRequestURI(); if ($file_uri) { $text = pht('Download Raw File'); $href = $file_uri; $icon = 'fa-download'; } else { $text = pht('View Raw File'); $href = $base_uri->alter('view', 'raw'); $icon = 'fa-file-text'; } if ($label !== null) { $text = $label; } $button = id(new PHUIButtonView()) ->setTag('a') ->setText($text) ->setHref($href) ->setIcon($icon); return $button; } private function renderGitLFSButton() { $viewer = $this->getViewer(); $uri = $this->getRequest()->getRequestURI(); $href = $uri->alter('view', 'git-lfs'); $text = pht('Download from Git LFS'); $icon = 'fa-download'; return id(new PHUIButtonView()) ->setTag('a') ->setText($text) ->setHref($href) ->setIcon($icon); } private function buildDisplayRows( array $lines, array $blame_list, array $blame_commits, $show_blame) { $request = $this->getRequest(); $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $revision_map = array(); $revisions = array(); if ($blame_commits) { $commit_map = mpull($blame_commits, 'getCommitIdentifier', 'getPHID'); $revision_ids = id(new DifferentialRevision()) ->loadIDsByCommitPHIDs(array_keys($commit_map)); if ($revision_ids) { $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs($revision_ids) ->execute(); $revisions = mpull($revisions, null, 'getID'); } foreach ($revision_ids as $commit_phid => $revision_id) { $revision_map[$commit_map[$commit_phid]] = $revision_id; } } $phids = array(); foreach ($blame_commits as $commit) { $author_phid = $commit->getAuthorPHID(); if ($author_phid === null) { continue; } $phids[$author_phid] = $author_phid; } foreach ($revisions as $revision) { $author_phid = $revision->getAuthorPHID(); if ($author_phid === null) { continue; } $phids[$author_phid] = $author_phid; } $handles = $viewer->loadHandles($phids); $colors = array(); if ($blame_commits) { $epochs = array(); foreach ($blame_commits as $identifier => $commit) { $epochs[$identifier] = $commit->getEpoch(); } $epoch_list = array_filter($epochs); $epoch_list = array_unique($epoch_list); $epoch_list = array_values($epoch_list); $epoch_min = min($epoch_list); $epoch_max = max($epoch_list); $epoch_range = ($epoch_max - $epoch_min) + 1; foreach ($blame_commits as $identifier => $commit) { $epoch = $epochs[$identifier]; if (!$epoch) { $color = '#ffffdd'; // Warning color, missing data. } else { $color_ratio = ($epoch - $epoch_min) / $epoch_range; $color_value = 0xE6 * (1.0 - $color_ratio); $color = sprintf( '#%02x%02x%02x', $color_value, 0xF6, $color_value); } $colors[$identifier] = $color; } } $display = array(); $last_identifier = null; $last_color = null; foreach ($lines as $line_index => $line) { $color = '#f6f6f6'; $duplicate = false; if (isset($blame_list[$line_index])) { $identifier = $blame_list[$line_index]; if (isset($colors[$identifier])) { $color = $colors[$identifier]; } if ($identifier === $last_identifier) { $duplicate = true; } else { $last_identifier = $identifier; } } $display[$line_index] = array( 'data' => $line, 'target' => false, 'highlighted' => false, 'color' => $color, 'duplicate' => $duplicate, ); } $line_arr = array(); $line_str = $drequest->getLine(); $ranges = explode(',', $line_str); foreach ($ranges as $range) { if (strpos($range, '-') !== false) { list($min, $max) = explode('-', $range, 2); $line_arr[] = array( 'min' => min($min, $max), 'max' => max($min, $max), ); } else if (strlen($range)) { $line_arr[] = array( 'min' => $range, 'max' => $range, ); } } // Mark the first highlighted line as the target line. if ($line_arr) { $target_line = $line_arr[0]['min']; if (isset($display[$target_line - 1])) { $display[$target_line - 1]['target'] = true; } } // Mark all other highlighted lines as highlighted. foreach ($line_arr as $range) { for ($ii = $range['min']; $ii <= $range['max']; $ii++) { if (isset($display[$ii - 1])) { $display[$ii - 1]['highlighted'] = true; } } } $engine = null; $inlines = array(); if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) { $engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); foreach ($this->lintMessages as $message) { $inline = id(new PhabricatorAuditInlineComment()) ->setSyntheticAuthor( ArcanistLintSeverity::getStringForSeverity($message['severity']). ' '.$message['code'].' ('.$message['name'].')') ->setLineNumber($message['line']) ->setContent($message['description']); $inlines[$message['line']][] = $inline; $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); require_celerity_resource('differential-changeset-view-css'); } $rows = $this->renderInlines( idx($inlines, 0, array()), $show_blame, (bool)$this->coverage, $engine); // NOTE: We're doing this manually because rendering is otherwise // dominated by URI generation for very large files. $line_base = (string)$drequest->generateURI( array( 'action' => 'browse', 'stable' => true, )); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-oncopy'); Javelin::initBehavior('phabricator-tooltips'); Javelin::initBehavior('phabricator-line-linker'); // Render these once, since they tend to get repeated many times in large // blame outputs. $commit_links = $this->renderCommitLinks($blame_commits, $handles); $revision_links = $this->renderRevisionLinks($revisions, $handles); if ($this->coverage) { require_celerity_resource('differential-changeset-view-css'); Javelin::initBehavior( 'diffusion-browse-file', array( 'labels' => array( 'cov-C' => pht('Covered'), 'cov-N' => pht('Not Covered'), 'cov-U' => pht('Not Executable'), ), )); } $skip_text = pht('Skip Past This Commit'); foreach ($display as $line_index => $line) { $row = array(); $line_number = $line_index + 1; $line_href = $line_base.'$'.$line_number; if (isset($blame_list[$line_index])) { $identifier = $blame_list[$line_index]; } else { $identifier = null; } $revision_link = null; $commit_link = null; $before_link = null; $style = 'background: '.$line['color'].';'; if ($identifier && !$line['duplicate']) { if (isset($commit_links[$identifier])) { $commit_link = $commit_links[$identifier]; } if (isset($revision_map[$identifier])) { $revision_id = $revision_map[$identifier]; if (isset($revision_links[$revision_id])) { $revision_link = $revision_links[$revision_id]; } } $skip_href = $line_href.'?before='.$identifier.'&view=blame'; $before_link = javelin_tag( 'a', array( 'href' => $skip_href, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $skip_text, 'align' => 'E', 'size' => 300, ), ), "\xC2\xAB"); } if ($show_blame) { $row[] = phutil_tag( 'th', array( 'class' => 'diffusion-blame-link', ), $before_link); $object_links = array(); $object_links[] = $commit_link; if ($revision_link) { $object_links[] = phutil_tag('span', array(), '/'); $object_links[] = $revision_link; } $row[] = phutil_tag( 'th', array( 'class' => 'diffusion-rev-link', ), $object_links); } $line_link = phutil_tag( 'a', array( 'href' => $line_href, 'style' => $style, ), $line_number); $row[] = javelin_tag( 'th', array( 'class' => 'diffusion-line-link', 'sigil' => 'phabricator-source-line', 'style' => $style, ), $line_link); if ($line['target']) { Javelin::initBehavior( 'diffusion-jump-to', array( 'target' => 'scroll_target', )); $anchor_text = phutil_tag( 'a', array( 'id' => 'scroll_target', ), ''); } else { $anchor_text = null; } $row[] = phutil_tag( 'td', array( ), array( $anchor_text, // NOTE: See phabricator-oncopy behavior. "\xE2\x80\x8B", // TODO: [HTML] Not ideal. phutil_safe_html(str_replace("\t", ' ', $line['data'])), )); if ($this->coverage) { $cov_index = $line_index; if (isset($this->coverage[$cov_index])) { $cov_class = $this->coverage[$cov_index]; } else { $cov_class = 'N'; } $row[] = phutil_tag( 'td', array( 'class' => 'cov cov-'.$cov_class, ), ''); } $rows[] = phutil_tag( 'tr', array( 'class' => ($line['highlighted'] ? 'phabricator-source-highlight' : null), ), $row); $cur_inlines = $this->renderInlines( idx($inlines, $line_number, array()), $show_blame, $this->coverage, $engine); foreach ($cur_inlines as $cur_inline) { $rows[] = $cur_inline; } } return $rows; } private function renderInlines( array $inlines, $show_blame, $has_coverage, $engine) { $rows = array(); foreach ($inlines as $inline) { // TODO: This should use modern scaffolding code. $inline_view = id(new PHUIDiffInlineCommentDetailView()) ->setUser($this->getViewer()) ->setMarkupEngine($engine) ->setInlineComment($inline) ->render(); $row = array_fill(0, ($show_blame ? 3 : 1), phutil_tag('th')); $row[] = phutil_tag('td', array(), $inline_view); if ($has_coverage) { $row[] = phutil_tag( 'td', array( 'class' => 'cov cov-I', )); } $rows[] = phutil_tag('tr', array('class' => 'inline'), $row); } return $rows; } private function buildImageCorpus($file_uri) { $properties = new PHUIPropertyListView(); $properties->addImageContent( phutil_tag( 'img', array( 'src' => $file_uri, ))); $file = $this->renderFileButton($file_uri); $header = id(new PHUIHeaderView()) ->setHeader(basename($this->getDiffusionRequest()->getPath())) ->addActionLink($file) ->setHeaderIcon('fa-file-image-o'); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); } private function buildBinaryCorpus($file_uri, $data) { $size = new PhutilNumber(strlen($data)); $text = pht('This is a binary file. It is %s byte(s) in length.', $size); $text = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($text); $file = $this->renderFileButton($file_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')) ->addActionLink($file); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($text); return $box; } private function buildErrorCorpus($message) { $text = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($message); $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($text); return $box; } private function buildBeforeResponse($before) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); // NOTE: We need to get the grandparent so we can capture filename changes // in the parent. $parent = $this->loadParentCommitOf($before); $old_filename = null; $was_created = false; if ($parent) { $grandparent = $this->loadParentCommitOf($parent); if ($grandparent) { $rename_query = new DiffusionRenameHistoryQuery(); $rename_query->setRequest($drequest); $rename_query->setOldCommit($grandparent); $rename_query->setViewer($request->getUser()); $old_filename = $rename_query->loadOldFilename(); $was_created = $rename_query->getWasCreated(); } } $follow = null; if ($was_created) { // If the file was created in history, that means older commits won't // have it. Since we know it existed at 'before', it must have been // created then; jump there. $target_commit = $before; $follow = 'created'; } else if ($parent) { // If we found a parent, jump to it. This is the normal case. $target_commit = $parent; } else { // If there's no parent, this was probably created in the initial commit? // And the "was_created" check will fail because we can't identify the // grandparent. Keep the user at 'before'. $target_commit = $before; $follow = 'first'; } $path = $drequest->getPath(); $renamed = null; if ($old_filename !== null && $old_filename !== '/'.$path) { $renamed = $path; $path = $old_filename; } $line = null; // If there's a follow error, drop the line so the user sees the message. if (!$follow) { $line = $this->getBeforeLineNumber($target_commit); } $before_uri = $drequest->generateURI( array( 'action' => 'browse', 'commit' => $target_commit, 'line' => $line, 'path' => $path, )); $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); $before_uri = $before_uri->alter('before', null); $before_uri = $before_uri->alter('renamed', $renamed); $before_uri = $before_uri->alter('follow', $follow); return id(new AphrontRedirectResponse())->setURI($before_uri); } private function getBeforeLineNumber($target_commit) { $drequest = $this->getDiffusionRequest(); $viewer = $this->getViewer(); $line = $drequest->getLine(); if (!$line) { return null; } $diff_info = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'againstCommit' => $target_commit, )); $file_phid = $diff_info['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file ("%s") returned by "%s".', $file_phid, 'diffusion.rawdiffquery.')); } $raw_diff = $file->loadFileData(); $old_line = 0; $new_line = 0; foreach (explode("\n", $raw_diff) as $text) { if ($text[0] == '-' || $text[0] == ' ') { $old_line++; } if ($text[0] == '+' || $text[0] == ' ') { $new_line++; } if ($new_line == $line) { return $old_line; } } // We didn't find the target line. return $line; } private function loadParentCommitOf($commit) { $drequest = $this->getDiffusionRequest(); $user = $this->getRequest()->getUser(); $before_req = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $drequest->getRepository(), 'commit' => $commit, )); $parents = DiffusionQuery::callConduitWithDiffusionRequest( $user, $before_req, 'diffusion.commitparentsquery', array( 'commit' => $commit, )); return head($parents); } private function renderRevisionTooltip( DifferentialRevision $revision, $handles) { $viewer = $this->getRequest()->getUser(); $date = phabricator_date($revision->getDateModified(), $viewer); $id = $revision->getID(); $title = $revision->getTitle(); $header = "D{$id} {$title}"; $author = $handles[$revision->getAuthorPHID()]->getName(); return "{$header}\n{$date} \xC2\xB7 {$author}"; } private function renderCommitTooltip( PhabricatorRepositoryCommit $commit, $author) { $viewer = $this->getRequest()->getUser(); $date = phabricator_date($commit->getEpoch(), $viewer); $summary = trim($commit->getSummary()); return "{$summary}\n{$date} \xC2\xB7 {$author}"; } protected function renderSearchForm() { $drequest = $this->getDiffusionRequest(); $forms = array(); $form = id(new AphrontFormView()) ->setUser($this->getViewer()) ->setMethod('GET'); switch ($drequest->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $forms[] = id(clone $form) ->appendChild(pht('Search is not available in Subversion.')); break; default: $forms[] = id(clone $form) ->appendChild( id(new AphrontFormTextWithSubmitControl()) ->setLabel(pht('File Name')) ->setSubmitLabel(pht('Search File Names')) ->setName('find') ->setValue($this->getRequest()->getStr('find'))); $forms[] = id(clone $form) ->appendChild( id(new AphrontFormTextWithSubmitControl()) ->setLabel(pht('Pattern')) ->setSubmitLabel(pht('Grep File Content')) ->setName('grep') ->setValue($this->getRequest()->getStr('grep'))); break; } require_celerity_resource('diffusion-icons-css'); $form_box = phutil_tag_div('diffusion-search-boxen', $forms); return $form_box; } protected function markupText($text) { $engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine(); $engine->setConfig('viewer', $this->getRequest()->getUser()); $text = $engine->markupText($text); $text = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $text); return $text; } protected function buildHeaderView(DiffusionRequest $drequest) { $viewer = $this->getViewer(); $tag = $this->renderCommitHashTag($drequest); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($this->renderPathLinks($drequest, $mode = 'browse')) ->addTag($tag); return $header; } protected function buildCurtain(DiffusionRequest $drequest) { $viewer = $this->getViewer(); $repository = $drequest->getRepository(); $curtain = $this->newCurtainView($drequest); $history_uri = $drequest->generateURI( array( 'action' => 'history', )); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($history_uri) ->setIcon('fa-list')); $behind_head = $drequest->getSymbolicCommit(); if ($repository->supportsBranchComparison()) { $compare_uri = $drequest->generateURI( array( 'action' => 'compare', )); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Compare Against...')) ->setIcon('fa-code-fork') ->setWorkflow(true) ->setHref($compare_uri)); } $head_uri = $drequest->generateURI( array( 'commit' => '', 'action' => 'browse', )); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Jump to HEAD')) ->setHref($head_uri) ->setIcon('fa-home') ->setDisabled(!$behind_head)); return $curtain; } protected function buildPropertyView( DiffusionRequest $drequest) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); if ($drequest->getSymbolicType() == 'tag') { $symbolic = $drequest->getSymbolicCommit(); $view->addProperty(pht('Tag'), $symbolic); $tags = $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', array( 'names' => array($symbolic), 'needMessages' => true, )); $tags = DiffusionRepositoryTag::newFromConduit($tags); $tags = mpull($tags, null, 'getName'); $tag = idx($tags, $symbolic); if ($tag && strlen($tag->getMessage())) { $view->addSectionHeader( pht('Tag Content'), 'fa-tag'); $view->addTextContent($this->markupText($tag->getMessage())); } } if ($view->hasAnyProperties()) { return $view; } return null; } private function buildOpenRevisions() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs(); $path_id = idx($path_map, $path); if (!$path_id) { return null; } $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withPath($repository->getID(), $path_id) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) - ->needRelationships(true) + ->needReviewerStatus(true) ->needFlags(true) ->needDrafts(true) ->execute(); if (!$revisions) { return null; } $header = id(new PHUIHeaderView()) ->setHeader(pht('Open Revisions')) ->setSubheader( pht('Recently updated open revisions affecting this file.')); $view = id(new DifferentialRevisionListView()) ->setHeader($header) ->setRevisions($revisions) ->setUser($viewer); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return $view; } private function loadBlame($path, $commit, $timeout) { $blame = $this->callConduitWithDiffusionRequest( 'diffusion.blame', array( 'commit' => $commit, 'paths' => array($path), 'timeout' => $timeout, )); $identifiers = idx($blame, $path, null); if ($identifiers) { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers($identifiers) // TODO: We only fetch this to improve author display behavior, but // shouldn't really need to? ->needCommitData(true) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); } else { $commits = array(); } return array($identifiers, $commits); } private function renderCommitLinks(array $commits, $handles) { $links = array(); foreach ($commits as $identifier => $commit) { $tooltip = $this->renderCommitTooltip( $commit, $commit->renderAuthorShortName($handles)); $commit_link = javelin_tag( 'a', array( 'href' => $commit->getURI(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), $commit->getLocalName()); $links[$identifier] = $commit_link; } return $links; } private function renderRevisionLinks(array $revisions, $handles) { $links = array(); foreach ($revisions as $revision) { $revision_id = $revision->getID(); $tooltip = $this->renderRevisionTooltip($revision, $handles); $revision_link = javelin_tag( 'a', array( 'href' => '/'.$revision->getMonogram(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), $revision->getMonogram()); $links[$revision_id] = $revision_link; } return $links; } private function getGitLFSRef(PhabricatorRepository $repository, $data) { if (!$repository->canUseGitLFS()) { return null; } $lfs_pattern = '(^version https://git-lfs\\.github\\.com/spec/v1[\r\n])'; if (!preg_match($lfs_pattern, $data)) { return null; } $matches = null; if (!preg_match('(^oid sha256:(.*)$)m', $data, $matches)) { return null; } $hash = $matches[1]; $hash = trim($hash); return id(new PhabricatorRepositoryGitLFSRefQuery()) ->setViewer($this->getViewer()) ->withRepositoryPHIDs(array($repository->getPHID())) ->withObjectHashes(array($hash)) ->executeOne(); } private function buildGitLFSCorpus(PhabricatorRepositoryGitLFSRef $ref) { // TODO: We should probably test if we can load the file PHID here and // show the user an error if we can't, rather than making them click // through to hit an error. $header = id(new PHUIHeaderView()) ->setHeader(basename($this->getDiffusionRequest()->getPath())) ->setHeaderIcon('fa-archive'); $severity = PHUIInfoView::SEVERITY_NOTICE; $messages = array(); $messages[] = pht( 'This %s file is stored in Git Large File Storage.', phutil_format_bytes($ref->getByteSize())); try { $file = $this->loadGitLFSFile($ref); $data = $this->renderGitLFSButton(); $header->addActionLink($data); } catch (Exception $ex) { $severity = PHUIInfoView::SEVERITY_ERROR; $messages[] = pht('The data for this file could not be loaded.'); } $raw = $this->renderFileButton(null, pht('View Raw LFS Pointer')); $header->addActionLink($raw); $corpus = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setCollapsed(true); if ($messages) { $corpus->setInfoView( id(new PHUIInfoView()) ->setSeverity($severity) ->setErrors($messages)); } return $corpus; } private function loadGitLFSFile(PhabricatorRepositoryGitLFSRef $ref) { $viewer = $this->getViewer(); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($ref->getFilePHID())) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file object for Git LFS ref "%s"!', $ref->getObjectHash())); } return $file; } private function buildBranchTable() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $branch = $drequest->getBranch(); $default_branch = $repository->getDefaultBranch(); if ($branch === $default_branch) { return null; } $pager = id(new PHUIPagerView()) ->setPageSize(10); try { $results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', array( 'commit' => $branch, 'against' => $default_branch, 'path' => $drequest->getPath(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, )); } catch (Exception $ex) { return null; } $history = DiffusionPathChange::newFromConduit($results['pathChanges']); $history = $pager->sliceResults($history); if (!$history) { return null; } $history_table = id(new DiffusionHistoryTableView()) ->setViewer($viewer) ->setDiffusionRequest($drequest) ->setHistory($history); $history_table->loadRevisions(); $history_table ->setParents($results['parents']) ->setFilterParents(true) ->setIsHead(true) ->setIsTail(!$pager->getHasMorePages()); $header = id(new PHUIHeaderView()) ->setHeader(pht('%s vs %s', $branch, $default_branch)); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($history_table); } } diff --git a/src/applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php index 684f5d75d6..50900ede78 100644 --- a/src/applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php @@ -1,34 +1,34 @@ getAdapter()->loadDifferentialRevision(); if (!$revision) { return array(); } - return $revision->getReviewers(); + return $revision->getReviewerPHIDs(); } protected function getHeraldFieldStandardType() { return self::STANDARD_PHID_LIST; } protected function getDatasource() { return new PhabricatorProjectOrUserDatasource(); } } diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php index bce6b5b517..936126ba89 100644 --- a/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php @@ -1,34 +1,34 @@ getAdapter()->getRevision(); if (!$revision) { return array(); } - return $revision->getReviewers(); - } + return $revision->getReviewerPHIDs(); + } protected function getHeraldFieldStandardType() { return self::STANDARD_PHID_LIST; } protected function getDatasource() { return new PhabricatorProjectOrUserDatasource(); } } diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index 9d63b9db3d..3465924f92 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -1,367 +1,366 @@ setViewer($viewer) ->withPHIDs(array($object->getPHID())) ->needCommitData(true) ->executeOne(); if (!$object) { throw new Exception( pht( 'Failed to reload commit ("%s") to fetch commit data.', $object->getPHID())); } return id(clone $this) ->setObject($object); } protected function initializeNewAdapter() { $this->commit = $this->newObject(); } public function setObject($object) { $this->commit = $object; return $this; } public function getObject() { return $this->commit; } public function getAdapterContentType() { return 'commit'; } public function getAdapterContentName() { return pht('Commits'); } public function getAdapterContentDescription() { return pht( "React to new commits appearing in tracked repositories.\n". "Commit rules can send email, flag commits, trigger audits, ". "and run build plans."); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return true; default: return false; } } public function canTriggerOnObject($object) { if ($object instanceof PhabricatorRepository) { return true; } if ($object instanceof PhabricatorProject) { return true; } return false; } public function getTriggerObjectPHIDs() { $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $repository_phid = $this->getRepository()->getPHID(); $commit_phid = $this->getObject()->getPHID(); $phids = array(); $phids[] = $commit_phid; $phids[] = $repository_phid; // NOTE: This is projects for the repository, not for the commit. When // Herald evalutes, commits normally can not have any project tags yet. $repository_project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $repository_phid, $project_type); foreach ($repository_project_phids as $phid) { $phids[] = $phid; } $phids = array_unique($phids); $phids = array_values($phids); return $phids; } public function explainValidTriggerObjects() { return pht('This rule can trigger for **repositories** and **projects**.'); } public function getHeraldName() { return $this->commit->getMonogram(); } public function loadAffectedPaths() { if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->getRepository(), $this->commit, PhabricatorUser::getOmnipotentUser()); $this->affectedPaths = $result; } return $this->affectedPaths; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $this->getRepository(), $this->loadAffectedPaths()); $this->affectedPackages = $packages; } return $this->affectedPackages; } public function loadAuditNeededPackages() { if ($this->auditNeededPackages === null) { $status_arr = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::CONCERNED, ); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( 'commitPHID = %s AND auditStatus IN (%Ls)', $this->commit->getPHID(), $status_arr); $this->auditNeededPackages = $requests; } return $this->auditNeededPackages; } public function loadDifferentialRevision() { if ($this->affectedRevision === null) { $this->affectedRevision = false; $commit = $this->getObject(); $data = $commit->getCommitData(); $revision_id = $data->getCommitDetail('differential.revisionID'); if ($revision_id) { // NOTE: The Herald rule owner might not actually have access to // the revision, and can control which revision a commit is // associated with by putting text in the commit message. However, // the rules they can write against revisions don't actually expose // anything interesting, so it seems reasonable to load unconditionally // here. $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); if ($revision) { $this->affectedRevision = $revision; } } } return $this->affectedRevision; } public static function getEnormousByteLimit() { return 1024 * 1024 * 1024; // 1GB } public static function getEnormousTimeLimit() { return 60 * 15; // 15 Minutes } private function loadCommitDiff() { $viewer = PhabricatorUser::getOmnipotentUser(); $byte_limit = self::getEnormousByteLimit(); $time_limit = self::getEnormousTimeLimit(); $diff_info = $this->callConduit( 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), 'timeout' => $time_limit, 'byteLimit' => $byte_limit, 'linesOfContext' => 0, )); if ($diff_info['tooHuge']) { throw new Exception( pht( 'The raw text of this change is enormous (larger than %s byte(s)). '. 'Herald can not process it.', new PhutilNumber($byte_limit))); } if ($diff_info['tooSlow']) { throw new Exception( pht( 'The raw text of this change took too long to process (longer '. 'than %s second(s)). Herald can not process it.', new PhutilNumber($time_limit))); } $file_phid = $diff_info['filePHID']; $diff_file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$diff_file) { throw new Exception( pht( 'Failed to load diff ("%s") for this change.', $file_phid)); } $raw = $diff_file->loadFileData(); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); $diff = DifferentialDiff::newEphemeralFromRawChanges( $changes); return $diff; } public function isDiffEnormous() { $this->loadDiffContent('*'); return ($this->commitDiff instanceof Exception); } public function loadDiffContent($type) { if ($this->commitDiff === null) { try { $this->commitDiff = $this->loadCommitDiff(); } catch (Exception $ex) { $this->commitDiff = $ex; phlog($ex); } } if ($this->commitDiff instanceof Exception) { $ex = $this->commitDiff; $ex_class = get_class($ex); $ex_message = pht('Failed to load changes: %s', $ex->getMessage()); return array( '<'.$ex_class.'>' => $ex_message, ); } $changes = $this->commitDiff->getChangesets(); $result = array(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { switch ($type) { case '-': $lines[] = $hunk->makeOldFile(); break; case '+': $lines[] = $hunk->makeNewFile(); break; case '*': $lines[] = $hunk->makeChanges(); break; default: throw new Exception(pht("Unknown content selection '%s'!", $type)); } } $result[$change->getFilename()] = implode("\n", $lines); } return $result; } public function loadIsMergeCommit() { $parents = $this->callConduit( 'diffusion.commitparentsquery', array( 'commit' => $this->getObject()->getCommitIdentifier(), )); return (count($parents) > 1); } private function callConduit($method, array $params) { $viewer = PhabricatorUser::getOmnipotentUser(); $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $viewer, 'repository' => $this->getRepository(), 'commit' => $this->commit->getCommitIdentifier(), )); return DiffusionQuery::callConduitWithDiffusionRequest( $viewer, $drequest, $method, $params); } private function getRepository() { return $this->getObject()->getRepository(); } /* -( HarbormasterBuildableAdapterInterface )------------------------------ */ public function getHarbormasterBuildablePHID() { return $this->getObject()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getObject()->getRepository()->getPHID(); } public function getQueuedHarbormasterBuildRequests() { return $this->buildRequests; } public function queueHarbormasterBuildRequest( HarbormasterBuildRequest $request) { $this->buildRequests[] = $request; } } diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php index 13c2695fed..5fbc970dfa 100644 --- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php +++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php @@ -1,226 +1,226 @@ getDiffContent('*'); return ($this->changesets instanceof Exception); } public function getDiffContent($type) { if ($this->changesets === null) { try { $this->changesets = $this->getHookEngine()->loadChangesetsForCommit( $this->getObject()->getRefNew()); } catch (Exception $ex) { $this->changesets = $ex; } } if ($this->changesets instanceof Exception) { $ex_class = get_class($this->changesets); $ex_message = $this->changesets->getMessage(); if ($type === 'name') { return array("<{$ex_class}: {$ex_message}>"); } else { return array("<{$ex_class}>" => $ex_message); } } $result = array(); if ($type === 'name') { foreach ($this->changesets as $change) { $result[] = $change->getFilename(); } } else { foreach ($this->changesets as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { switch ($type) { case '-': $lines[] = $hunk->makeOldFile(); break; case '+': $lines[] = $hunk->makeNewFile(); break; case '*': default: $lines[] = $hunk->makeChanges(); break; } } $result[$change->getFilename()] = implode('', $lines); } } return $result; } public function getCommitRef() { if ($this->commitRef === null) { $this->commitRef = $this->getHookEngine()->loadCommitRefForCommit( $this->getObject()->getRefNew()); } return $this->commitRef; } public function getAuthorPHID() { $repository = $this->getHookEngine()->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $ref = $this->getCommitRef(); $author = $ref->getAuthor(); if (!strlen($author)) { return null; } return $this->lookupUser($author); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // In Subversion, the pusher is always the author. return $this->getHookEngine()->getViewer()->getPHID(); } } public function getCommitterPHID() { $repository = $this->getHookEngine()->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // If there's no committer information, we're going to return the // author instead. However, if there's committer information and we // can't resolve it, return `null`. $ref = $this->getCommitRef(); $committer = $ref->getCommitter(); if (!strlen($committer)) { return $this->getAuthorPHID(); } return $this->lookupUser($committer); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // In Subversion, the pusher is always the committer. return $this->getHookEngine()->getViewer()->getPHID(); } } public function getAuthorRaw() { $repository = $this->getHookEngine()->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $ref = $this->getCommitRef(); return $ref->getAuthor(); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // In Subversion, the pusher is always the author. return $this->getHookEngine()->getViewer()->getUsername(); } } public function getCommitterRaw() { $repository = $this->getHookEngine()->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // Here, if there's no committer, we're going to return the author // instead. $ref = $this->getCommitRef(); $committer = $ref->getCommitter(); if (strlen($committer)) { return $committer; } return $ref->getAuthor(); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // In Subversion, the pusher is always the committer. return $this->getHookEngine()->getViewer()->getUsername(); } } private function lookupUser($author) { return id(new DiffusionResolveUserQuery()) ->withName($author) ->execute(); } private function getCommitFields() { if ($this->fields === null) { $this->fields = id(new DiffusionLowLevelCommitFieldsQuery()) ->setRepository($this->getHookEngine()->getRepository()) ->withCommitRef($this->getCommitRef()) ->execute(); } return $this->fields; } public function getRevision() { if ($this->revision === false) { $fields = $this->getCommitFields(); $revision_id = idx($fields, 'revisionID'); if (!$revision_id) { $this->revision = null; } else { $this->revision = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($revision_id)) - ->needRelationships(true) + ->needReviewerStatus(true) ->executeOne(); } } return $this->revision; } public function getIsMergeCommit() { $repository = $this->getHookEngine()->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $parents = id(new DiffusionLowLevelParentsQuery()) ->setRepository($repository) ->withIdentifier($this->getObject()->getRefNew()) ->execute(); return (count($parents) > 1); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // NOTE: For now, we ignore "svn:mergeinfo" at all levels. We might // change this some day, but it's not nearly as clear a signal as // ancestry is in Git/Mercurial. return false; } } public function getBranches() { return $this->getHookEngine()->loadBranches( $this->getObject()->getRefNew()); } }