diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 4cd8f276fc..8d52b7facf 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -1,1172 +1,1172 @@ loadDiffusionContext(); if ($response) { return $response; } $drequest = $this->getDiffusionRequest(); $viewer = $request->getUser(); $repository = $drequest->getRepository(); $commit_identifier = $drequest->getCommit(); // If this page is being accessed via "/source/xyz/commit/...", redirect // to the canonical URI. $has_callsign = strlen($request->getURIData('repositoryCallsign')); $has_id = strlen($request->getURIData('repositoryID')); if (!$has_callsign && !$has_id) { $canonical_uri = $repository->getCommitURI($commit_identifier); return id(new AphrontRedirectResponse()) ->setURI($canonical_uri); } if ($request->getStr('diff')) { return $this->buildRawDiffResponse($drequest); } $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(array($commit_identifier)) ->needCommitData(true) ->needAuditRequests(true) ->needAuditAuthority(array($viewer)) ->setLimit(100) ->needIdentities(true) ->execute(); $multiple_results = count($commits) > 1; $crumbs = $this->buildCrumbs(array( 'commit' => !$multiple_results, )); $crumbs->setBorder(true); if (!$commits) { if (!$this->getCommitExists()) { return new Aphront404Response(); } $error = id(new PHUIInfoView()) ->setTitle(pht('Commit Still Parsing')) ->appendChild( pht( 'Failed to load the commit because the commit has not been '. 'parsed yet.')); $title = pht('Commit Still Parsing'); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($error); } else if ($multiple_results) { $warning_message = pht( 'The identifier %s is ambiguous and matches more than one commit.', phutil_tag( 'strong', array(), $commit_identifier)); $error = id(new PHUIInfoView()) ->setTitle(pht('Ambiguous Commit')) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->appendChild($warning_message); $list = id(new DiffusionCommitGraphView()) ->setViewer($viewer) ->setCommits($commits); $crumbs->addTextCrumb(pht('Ambiguous Commit')); $matched_commits = id(new PHUITwoColumnView()) ->setFooter(array( $error, $list, )); return $this->newPage() ->setTitle(pht('Ambiguous Commit')) ->setCrumbs($crumbs) ->appendChild($matched_commits); } else { $commit = head($commits); } $audit_requests = $commit->getAudits(); $commit_data = $commit->getCommitData(); $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); $error_panel = null; $unpublished_panel = null; $hard_limit = 1000; if ($commit->isImported()) { $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $change_query->setLimit($hard_limit + 1); $changes = $change_query->loadChanges(); } else { $changes = array(); } $was_limited = (count($changes) > $hard_limit); if ($was_limited) { $changes = array_slice($changes, 0, $hard_limit); } $count = count($changes); $is_unreadable = false; $hint = null; if (!$count || $commit->isUnreachable()) { $hint = id(new DiffusionCommitHintQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withOldCommitIdentifiers(array($commit->getCommitIdentifier())) ->executeOne(); if ($hint) { $is_unreadable = $hint->isUnreadable(); } } if ($is_foreign) { $subpath = $commit_data->getCommitDetail('svn-subpath'); $error_panel = new PHUIInfoView(); $error_panel->setTitle(pht('Commit Not Tracked')); $error_panel->setSeverity(PHUIInfoView::SEVERITY_WARNING); $error_panel->appendChild( pht( "This Diffusion repository is configured to track only one ". "subdirectory of the entire Subversion repository, and this commit ". "didn't affect the tracked subdirectory ('%s'), so no ". "information is available.", $subpath)); } else { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $viewer); $commit_tag = $this->renderCommitHashTag($drequest); $header = id(new PHUIHeaderView()) ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))) ->setHeaderIcon('fa-code-fork') ->addTag($commit_tag); if (!$commit->isAuditStatusNoAudit()) { $status = $commit->getAuditStatusObject(); $icon = $status->getIcon(); $color = $status->getColor(); $status = $status->getName(); $header->setStatus($icon, $color, $status); } $curtain = $this->buildCurtain($commit, $repository); $details = $this->buildPropertyListView( $commit, $commit_data, $audit_requests); $message = $commit_data->getCommitMessage(); $revision = $commit->getCommitIdentifier(); $message = $this->linkBugtraq($message); $message = $engine->markupText($message); $detail_list = new PHUIPropertyListView(); $detail_list->addTextContent( phutil_tag( 'div', array( 'class' => 'diffusion-commit-message phabricator-remarkup', ), $message)); if ($commit->isUnreachable()) { $did_rewrite = false; if ($hint) { if ($hint->isRewritten()) { $rewritten = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(array($hint->getNewCommitIdentifier())) ->executeOne(); if ($rewritten) { $did_rewrite = true; $rewritten_uri = $rewritten->getURI(); $rewritten_name = $rewritten->getLocalName(); $rewritten_link = phutil_tag( 'a', array( 'href' => $rewritten_uri, ), $rewritten_name); $this->commitErrors[] = pht( 'This commit was rewritten after it was published, which '. 'changed the commit hash. This old version of the commit is '. 'no longer reachable from any branch, tag or ref. The new '. 'version of this commit is %s.', $rewritten_link); } } } if (!$did_rewrite) { $this->commitErrors[] = pht( 'This commit has been deleted in the repository: it is no longer '. 'reachable from any branch, tag, or ref.'); } } if (!$commit->isPermanentCommit()) { $nonpermanent_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setName(pht('Unpublished')) ->setColor(PHUITagView::COLOR_ORANGE); $header->addTag($nonpermanent_tag); $holds = $commit_data->newPublisherHoldReasons(); $reasons = array(); foreach ($holds as $hold) { $reasons[] = array( phutil_tag('strong', array(), pht('%s:', $hold->getName())), ' ', $hold->getSummary(), ); } if (!$holds) { $reasons[] = pht('No further details are available.'); } $doc_href = PhabricatorEnv::getDoclink( 'Diffusion User Guide: Permanent Refs'); $doc_link = phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Learn More')); $title = array( pht('Unpublished Commit'), pht(" \xC2\xB7 "), $doc_link, ); $unpublished_panel = id(new PHUIInfoView()) ->setTitle($title) ->setErrors($reasons) ->setSeverity(PHUIInfoView::SEVERITY_WARNING); } if ($this->getCommitErrors()) { $error_panel = id(new PHUIInfoView()) ->appendChild($this->getCommitErrors()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING); } } $timeline = $this->buildComments($commit); $merge_table = $this->buildMergesTable($commit); $show_changesets = false; $info_panel = null; $change_list = null; $change_table = null; if ($is_unreadable) { $info_panel = $this->renderStatusMessage( pht('Unreadable Commit'), pht( 'This commit has been marked as unreadable by an administrator. '. 'It may have been corrupted or created improperly by an external '. 'tool.')); } else if ($is_foreign) { // Don't render anything else. } else if (!$commit->isImported()) { $info_panel = $this->renderStatusMessage( pht('Still Importing...'), pht( 'This commit is still importing. Changes will be visible once '. 'the import finishes.')); } else if (!count($changes)) { $info_panel = $this->renderStatusMessage( pht('Empty Commit'), pht( 'This commit is empty and does not affect any paths.')); } else if ($was_limited) { $info_panel = $this->renderStatusMessage( pht('Very Large Commit'), pht( 'This commit is very large, and affects more than %d files. '. 'Changes are not shown.', $hard_limit)); } else if (!$this->getCommitExists()) { $info_panel = $this->renderStatusMessage( pht('Commit No Longer Exists'), pht('This commit no longer exists in the repository.')); } else { $show_changesets = true; // The user has clicked "Show All Changes", and we should show all the // changes inline even if there are more than the soft limit. $show_all_details = $request->getBool('show_all'); $change_header = id(new PHUIHeaderView()) ->setHeader(pht('Changes (%s)', new PhutilNumber($count))); $warning_view = null; if ($count > self::CHANGES_LIMIT && !$show_all_details) { $button = id(new PHUIButtonView()) ->setText(pht('Show All Changes')) ->setHref('?show_all=true') ->setTag('a') ->setIcon('fa-files-o'); $warning_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle(pht('Very Large Commit')) ->appendChild( pht('This commit is very large. Load each file individually.')); $change_header->addActionLink($button); } $changesets = DiffusionPathChange::convertToDifferentialChangesets( $viewer, $changes); // TODO: This table and panel shouldn't really be separate, but we need // to clean up the "Load All Files" interaction first. $change_table = $this->buildTableOfContents( $changesets, $change_header, $warning_view); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $vcs_supports_directory_changes = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $vcs_supports_directory_changes = false; break; default: throw new Exception(pht('Unknown VCS.')); } $references = array(); foreach ($changesets as $key => $changeset) { $file_type = $changeset->getFileType(); if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { if (!$vcs_supports_directory_changes) { unset($changesets[$key]); continue; } } $references[$key] = $drequest->generateURI( array( 'action' => 'rendering-ref', 'path' => $changeset->getFilename(), )); } // TODO: Some parts of the views still rely on properties of the // DifferentialChangeset. Make the objects ephemeral to make sure we don't // accidentally save them, and then set their ID to the appropriate ID for // this application (the path IDs). $path_ids = array_flip(mpull($changes, 'getPath')); foreach ($changesets as $changeset) { $changeset->makeEphemeral(); $changeset->setID($path_ids[$changeset->getFilename()]); } if ($count <= self::CHANGES_LIMIT || $show_all_details) { $visible_changesets = $changesets; } else { $visible_changesets = array(); $inlines = id(new DiffusionDiffInlineCommentQuery()) ->setViewer($viewer) ->withCommitPHIDs(array($commit->getPHID())) ->withPublishedComments(true) ->withPublishableComments(true) ->execute(); $inlines = mpull($inlines, 'newInlineCommentObject'); $path_ids = mpull($inlines, null, 'getPathID'); foreach ($changesets as $key => $changeset) { if (array_key_exists($changeset->getID(), $path_ids)) { $visible_changesets[$key] = $changeset; } } } $change_list_title = $commit->getDisplayName(); $change_list = new DifferentialChangesetListView(); $change_list->setTitle($change_list_title); $change_list->setChangesets($changesets); $change_list->setVisibleChangesets($visible_changesets); $change_list->setRenderingReferences($references); $change_list->setRenderURI($repository->getPathURI('diff/')); $change_list->setRepository($repository); $change_list->setUser($viewer); $change_list->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); // TODO: Try to setBranch() to something reasonable here? $change_list->setStandaloneURI( $repository->getPathURI('diff/')); $change_list->setRawFileURIs( // TODO: Implement this, somewhat tricky if there's an octopus merge // or whatever? null, $repository->getPathURI('diff/?view=r')); $change_list->setInlineCommentControllerURI( '/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/'); } $add_comment = $this->renderAddCommentPanel( $commit, $timeline); $filetree = id(new DifferentialFileTreeEngine()) ->setViewer($viewer) ->setDisabled(!$show_changesets); if ($show_changesets) { $filetree->setChangesets($changesets); } $description_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Description')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($detail_list); $detail_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Details')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($details); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $unpublished_panel, $error_panel, $description_box, $detail_box, $timeline, $merge_table, $info_panel, )) ->setFooter( array( $change_table, $change_list, $add_comment, )); $main_content = array( $crumbs, $view, ); $main_content = $filetree->newView($main_content); if (!$filetree->getDisabled()) { $change_list->setFormationView($main_content); } $page = $this->newPage() ->setTitle($commit->getDisplayName()) ->setPageObjectPHIDS(array($commit->getPHID())) ->appendChild($main_content); return $page; } private function buildPropertyListView( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $audit_requests) { $viewer = $this->getViewer(); $commit_phid = $commit->getPHID(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $view = id(new PHUIPropertyListView()) ->setUser($this->getRequest()->getUser()) ->setObject($commit); $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($commit_phid)) ->withEdgeTypes(array( DiffusionCommitHasTaskEdgeType::EDGECONST, DiffusionCommitHasRevisionEdgeType::EDGECONST, DiffusionCommitRevertsCommitEdgeType::EDGECONST, DiffusionCommitRevertedByCommitEdgeType::EDGECONST, )); $edges = $edge_query->execute(); $task_phids = array_keys( $edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]); $revision_phid = key( $edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]); $reverts_phids = array_keys( $edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]); $reverted_by_phids = array_keys( $edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]); $phids = $edge_query->getDestinationPHIDs(array($commit_phid)); if ($data->getCommitDetail('reviewerPHID')) { $phids[] = $data->getCommitDetail('reviewerPHID'); } $phids[] = $commit->getCommitterDisplayPHID(); $phids[] = $commit->getAuthorDisplayPHID(); // NOTE: We should never normally have more than a single push log, but // it can occur naturally if a commit is pushed, then the branch it was // on is deleted, then the commit is pushed again (or through other similar // chains of events). This should be rare, but does not indicate a bug // or data issue. // NOTE: We never query push logs in SVN because the committer is always // the pusher and the commit time is always the push time; the push log // is redundant and we save a query by skipping it. $push_logs = array(); if ($repository->isHosted() && !$repository->isSVN()) { $push_logs = id(new PhabricatorRepositoryPushLogQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withNewRefs(array($commit->getCommitIdentifier())) ->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)) ->execute(); foreach ($push_logs as $log) { $phids[] = $log->getPusherPHID(); } } $handles = array(); if ($phids) { $handles = $this->loadViewerHandles($phids); } $props = array(); if ($audit_requests) { $user_requests = array(); $other_requests = array(); foreach ($audit_requests as $audit_request) { if (!$audit_request->isInteresting()) { continue; } if ($audit_request->isUser()) { $user_requests[] = $audit_request; } else { $other_requests[] = $audit_request; } } if ($user_requests) { $view->addProperty( pht('Auditors'), $this->renderAuditStatusView($commit, $user_requests)); } if ($other_requests) { $view->addProperty( pht('Group Auditors'), $this->renderAuditStatusView($commit, $other_requests)); } } $provenance_list = new PHUIStatusListView(); $author_view = $commit->newCommitAuthorView($viewer); if ($author_view) { $author_date = $data->getCommitDetail('authorEpoch'); $author_date = phabricator_datetime($author_date, $viewer); $provenance_list->addItem( id(new PHUIStatusItemView()) ->setTarget($author_view) ->setNote(pht('Authored on %s', $author_date))); } if (!$commit->isAuthorSameAsCommitter()) { $committer_view = $commit->newCommitCommitterView($viewer); if ($committer_view) { $committer_date = $commit->getEpoch(); $committer_date = phabricator_datetime($committer_date, $viewer); $provenance_list->addItem( id(new PHUIStatusItemView()) ->setTarget($committer_view) ->setNote(pht('Committed on %s', $committer_date))); } } if ($push_logs) { $pushed_list = new PHUIStatusListView(); foreach ($push_logs as $push_log) { $pusher_date = $push_log->getEpoch(); $pusher_date = phabricator_datetime($pusher_date, $viewer); $pusher_view = $handles[$push_log->getPusherPHID()]->renderLink(); $provenance_list->addItem( id(new PHUIStatusItemView()) ->setTarget($pusher_view) ->setNote(pht('Pushed on %s', $pusher_date))); } } $view->addProperty(pht('Provenance'), $provenance_list); $reviewer_phid = $data->getCommitDetail('reviewerPHID'); if ($reviewer_phid) { $view->addProperty( pht('Reviewer'), $handles[$reviewer_phid]->renderLink()); } if ($revision_phid) { $view->addProperty( pht('Differential Revision'), $handles[$revision_phid]->renderLink()); } $parents = $this->getCommitParents(); if ($parents) { $view->addProperty( pht('Parents'), $viewer->renderHandleList(mpull($parents, 'getPHID'))); } if ($this->getCommitExists()) { $view->addProperty( pht('Branches'), phutil_tag( 'span', array( 'id' => 'commit-branches', ), pht('Unknown'))); $view->addProperty( pht('Tags'), phutil_tag( 'span', array( 'id' => 'commit-tags', ), pht('Unknown'))); $identifier = $commit->getCommitIdentifier(); $root = $repository->getPathURI("commit/{$identifier}"); Javelin::initBehavior( 'diffusion-commit-branches', array( $root.'/branches/' => 'commit-branches', $root.'/tags/' => 'commit-tags', )); } $refs = $this->getCommitRefs(); if ($refs) { $ref_links = array(); foreach ($refs as $ref_data) { $ref_links[] = phutil_tag( 'a', array( 'href' => $ref_data['href'], ), $ref_data['ref']); } $view->addProperty( pht('References'), phutil_implode_html(', ', $ref_links)); } if ($reverts_phids) { $view->addProperty( pht('Reverts'), $viewer->renderHandleList($reverts_phids)); } if ($reverted_by_phids) { $view->addProperty( pht('Reverted By'), $viewer->renderHandleList($reverted_by_phids)); } if ($task_phids) { $task_list = array(); foreach ($task_phids as $phid) { $task_list[] = $handles[$phid]->renderLink(); } $task_list = phutil_implode_html(phutil_tag('br'), $task_list); $view->addProperty( pht('Tasks'), $task_list); } return $view; } private function buildComments(PhabricatorRepositoryCommit $commit) { $timeline = $this->buildTransactionTimeline( $commit, new PhabricatorAuditTransactionQuery()); $timeline->setQuoteRef($commit->getMonogram()); return $timeline; } private function renderAddCommentPanel( PhabricatorRepositoryCommit $commit, $timeline) { $request = $this->getRequest(); $viewer = $request->getUser(); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $comment_view = id(new DiffusionCommitEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($commit); $comment_view->setTransactionTimeline($timeline); return $comment_view; } private function buildMergesTable(PhabricatorRepositoryCommit $commit) { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $merges = $this->getCommitMerges(); if (!$merges) { return null; } $limit = $this->getMergeDisplayLimit(); $caption = null; if (count($merges) > $limit) { $merges = array_slice($merges, 0, $limit); $caption = new PHUIInfoView(); $caption->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $caption->appendChild( pht( 'This commit merges a very large number of changes. '. 'Only the first %s are shown.', new PhutilNumber($limit))); } - $history_table = id(new DiffusionHistoryTableView()) - ->setUser($viewer) + $commit_list = id(new DiffusionCommitGraphView()) + ->setViewer($viewer) ->setDiffusionRequest($drequest) ->setHistory($merges); $panel = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Merged Changes')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($history_table); + ->setObjectList($commit_list->newObjectItemListView()); if ($caption) { $panel->setInfoView($caption); } return $panel; } private function buildCurtain( PhabricatorRepositoryCommit $commit, PhabricatorRepository $repository) { $request = $this->getRequest(); $viewer = $this->getViewer(); $curtain = $this->newCurtainView($commit); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $commit, PhabricatorPolicyCapability::CAN_EDIT); $id = $commit->getID(); $edit_uri = $this->getApplicationURI("/commit/edit/{$id}/"); $action = id(new PhabricatorActionView()) ->setName(pht('Edit Commit')) ->setHref($edit_uri) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $curtain->addAction($action); $action = id(new PhabricatorActionView()) ->setName(pht('Download Raw Diff')) ->setHref($request->getRequestURI()->alter('diff', true)) ->setIcon('fa-download'); $curtain->addAction($action); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $commit); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } return $curtain; } private function buildRawDiffResponse(DiffusionRequest $drequest) { $diff_info = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), )); $file_phid = $diff_info['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file ("%s") returned by "%s".', $file_phid, 'diffusion.rawdiffquery')); } return $file->getRedirectResponse(); } private function renderAuditStatusView( PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $viewer = $this->getViewer(); $view = new PHUIStatusListView(); foreach ($audit_requests as $request) { $code = $request->getAuditStatus(); $item = new PHUIStatusItemView(); $item->setIcon( PhabricatorAuditStatusConstants::getStatusIcon($code), PhabricatorAuditStatusConstants::getStatusColor($code), PhabricatorAuditStatusConstants::getStatusName($code)); $auditor_phid = $request->getAuditorPHID(); $target = $viewer->renderHandle($auditor_phid); $item->setTarget($target); if ($commit->hasAuditAuthority($viewer, $request)) { $item->setHighlighted(true); } $view->addItem($item); } return $view; } private function linkBugtraq($corpus) { $url = PhabricatorEnv::getEnvConfig('bugtraq.url'); if (!strlen($url)) { return $corpus; } $regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex'); if (!$regexes) { return $corpus; } $parser = id(new PhutilBugtraqParser()) ->setBugtraqPattern("[[ {$url} | %BUGID% ]]") ->setBugtraqCaptureExpression(array_shift($regexes)); $select = array_shift($regexes); if ($select) { $parser->setBugtraqSelectExpression($select); } return $parser->processCorpus($corpus); } private function buildTableOfContents( array $changesets, $header, $info_view) { $drequest = $this->getDiffusionRequest(); $viewer = $this->getViewer(); $toc_view = id(new PHUIDiffTableOfContentsListView()) ->setUser($viewer) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); if ($info_view) { $toc_view->setInfoView($info_view); } // TODO: This is hacky, we just want access to the linkX() methods on // DiffusionView. $diffusion_view = id(new DiffusionEmptyResultView()) ->setDiffusionRequest($drequest); $have_owners = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorOwnersApplication', $viewer); if (!$changesets) { $have_owners = false; } if ($have_owners) { if ($viewer->getPHID()) { $packages = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withAuthorityPHIDs(array($viewer->getPHID())) ->execute(); $toc_view->setAuthorityPackages($packages); } $repository = $drequest->getRepository(); $repository_phid = $repository->getPHID(); $control_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withControl($repository_phid, mpull($changesets, 'getFilename')); $control_query->execute(); } foreach ($changesets as $changeset_id => $changeset) { $path = $changeset->getFilename(); $anchor = $changeset->getAnchorName(); $history_link = $diffusion_view->linkHistory($path); $browse_link = $diffusion_view->linkBrowse( $path, array( 'type' => $changeset->getFileType(), )); $item = id(new PHUIDiffTableOfContentsItemView()) ->setChangeset($changeset) ->setAnchor($anchor) ->setContext( array( $history_link, ' ', $browse_link, )); if ($have_owners) { $packages = $control_query->getControllingPackagesForPath( $repository_phid, $changeset->getFilename()); $item->setPackages($packages); } $toc_view->addItem($item); } return $toc_view; } private function loadCommitState() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commit = $drequest->getCommit(); // TODO: We could use futures here and resolve these calls in parallel. $exceptions = array(); try { $parent_refs = $this->callConduitWithDiffusionRequest( 'diffusion.commitparentsquery', array( 'commit' => $commit, )); if ($parent_refs) { $parents = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers($parent_refs) ->execute(); } else { $parents = array(); } $this->commitParents = $parents; } catch (Exception $ex) { $this->commitParents = false; $exceptions[] = $ex; } $merge_limit = $this->getMergeDisplayLimit(); try { if ($repository->isSVN()) { $this->commitMerges = array(); } else { $merges = $this->callConduitWithDiffusionRequest( 'diffusion.mergedcommitsquery', array( 'commit' => $commit, 'limit' => $merge_limit + 1, )); $this->commitMerges = DiffusionPathChange::newFromConduit($merges); } } catch (Exception $ex) { $this->commitMerges = false; $exceptions[] = $ex; } try { if ($repository->isGit()) { $refs = $this->callConduitWithDiffusionRequest( 'diffusion.refsquery', array( 'commit' => $commit, )); } else { $refs = array(); } $this->commitRefs = $refs; } catch (Exception $ex) { $this->commitRefs = false; $exceptions[] = $ex; } if ($exceptions) { $exists = $this->callConduitWithDiffusionRequest( 'diffusion.existsquery', array( 'commit' => $commit, )); if ($exists) { $this->commitExists = true; foreach ($exceptions as $exception) { $this->commitErrors[] = $exception->getMessage(); } } else { $this->commitExists = false; $this->commitErrors[] = pht( 'This commit no longer exists in the repository. It may have '. 'been part of a branch which was deleted.'); } } else { $this->commitExists = true; $this->commitErrors = array(); } } private function getMergeDisplayLimit() { return 50; } private function getCommitExists() { if ($this->commitExists === null) { $this->loadCommitState(); } return $this->commitExists; } private function getCommitParents() { if ($this->commitParents === null) { $this->loadCommitState(); } return $this->commitParents; } private function getCommitRefs() { if ($this->commitRefs === null) { $this->loadCommitState(); } return $this->commitRefs; } private function getCommitMerges() { if ($this->commitMerges === null) { $this->loadCommitState(); } return $this->commitMerges; } private function getCommitErrors() { if ($this->commitErrors === null) { $this->loadCommitState(); } return $this->commitErrors; } } diff --git a/src/applications/diffusion/controller/DiffusionHistoryController.php b/src/applications/diffusion/controller/DiffusionHistoryController.php index f4cd57d09e..fb35d9b6ad 100644 --- a/src/applications/diffusion/controller/DiffusionHistoryController.php +++ b/src/applications/diffusion/controller/DiffusionHistoryController.php @@ -1,145 +1,127 @@ loadDiffusionContext(); if ($response) { return $response; } require_celerity_resource('diffusion-css'); $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, ); $history_results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', $params); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); $history = $pager->sliceResults($history); - $identifiers = array(); - foreach ($history as $item) { - $identifiers[] = $item->getCommitIdentifier(); - } - - if ($identifiers) { - $commits = id(new DiffusionCommitQuery()) - ->setViewer($viewer) - ->withRepositoryPHIDs(array($repository->getPHID())) - ->withIdentifiers($identifiers) - ->needCommitData(true) - ->needIdentities(true) - ->execute(); - } else { - $commits = array(); - } - $history_list = id(new DiffusionCommitGraphView()) ->setViewer($viewer) ->setDiffusionRequest($drequest) - ->setHistory($history) - ->setCommits($commits); + ->setHistory($history); // NOTE: If we have a path (like "src/"), many nodes in the graph are // likely to be missing (since the path wasn't touched by those commits). // If we draw the graph, commits will often appear to be unrelated because // intermediate nodes are omitted. Just drop the graph. // The ideal behavior would be to load the entire graph and then connect // ancestors appropriately, but this would currrently be prohibitively // expensive in the general case. $show_graph = !strlen($drequest->getPath()); if ($show_graph) { $history_list ->setParents($history_results['parents']) ->setIsHead(!$pager->getOffset()) ->setIsTail(!$pager->getHasMorePages()); } $header = $this->buildHeader($drequest); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'history', )); $crumbs->setBorder(true); $title = array( pht('History'), $repository->getDisplayName(), ); $pager = id(new PHUIBoxView()) ->addClass('mlb') ->appendChild($pager); $tabs = $this->buildTabsView('history'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setTabs($tabs) ->setFooter(array( $history_list, $pager, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view) ->addClass('diffusion-history-view'); } private function buildHeader(DiffusionRequest $drequest) { $viewer = $this->getViewer(); $repository = $drequest->getRepository(); $no_path = !strlen($drequest->getPath()); if ($no_path) { $header_text = pht('History'); } else { $header_text = $this->renderPathLinks($drequest, $mode = 'history'); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($header_text) ->setHeaderIcon('fa-clock-o'); if (!$repository->isSVN()) { $branch_tag = $this->renderBranchTag($drequest); $header->addTag($branch_tag); } if ($drequest->getSymbolicCommit()) { $symbolic_tag = $this->renderSymbolicCommit($drequest); $header->addTag($symbolic_tag); } return $header; } } diff --git a/src/applications/diffusion/view/DiffusionCommitGraphView.php b/src/applications/diffusion/view/DiffusionCommitGraphView.php index 1c8e88f164..66cf8c2a18 100644 --- a/src/applications/diffusion/view/DiffusionCommitGraphView.php +++ b/src/applications/diffusion/view/DiffusionCommitGraphView.php @@ -1,434 +1,520 @@ history = $history; return $this; } public function getHistory() { return $this->history; } public function setCommits(array $commits) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $this->commits = $commits; - $this->commitMap = mpull($commits, null, 'getCommitIdentifier'); return $this; } public function getCommits() { return $this->commits; } public function setParents(array $parents) { $this->parents = $parents; return $this; } public function getParents() { return $this->parents; } public function setIsHead($is_head) { $this->isHead = $is_head; return $this; } public function getIsHead() { return $this->isHead; } public function setIsTail($is_tail) { $this->isTail = $is_tail; return $this; } public function getIsTail() { return $this->isTail; } public function setFilterParents($filter_parents) { $this->filterParents = $filter_parents; return $this; } public function getFilterParents() { return $this->filterParents; } private function getRepository() { $drequest = $this->getDiffusionRequest(); if (!$drequest) { return null; } return $drequest->getRepository(); } - public function render() { - $viewer = $this->getViewer(); + public function newObjectItemListView() { + $list_view = id(new PHUIObjectItemListView()); + + $item_views = $this->newObjectItemViews(); + foreach ($item_views as $item_view) { + $list_view->addItem($item_view); + } + + return $list_view; + } + private function newObjectItemViews() { require_celerity_resource('diffusion-css'); $show_builds = $this->shouldShowBuilds(); $show_revisions = $this->shouldShowRevisions(); - $items = $this->newHistoryItems(); + $views = array(); - $rows = array(); - $last_date = null; - foreach ($items as $item) { + $items = $this->newHistoryItems(); + foreach ($items as $hash => $item) { $content = array(); - $item_epoch = $item['epoch']; - $item_hash = $item['hash']; $commit = $item['commit']; - $item_date = phabricator_date($item_epoch, $viewer); - if ($item_date !== $last_date) { - $last_date = $item_date; - $content[] = phutil_tag( - 'div', - array( - 'class' => 'diffusion-commit-graph-date-header', - ), - $item_date); - } - $commit_description = $this->getCommitDescription($commit); - $commit_link = $this->getCommitURI($item_hash); + $commit_link = $this->getCommitURI($hash); - $short_hash = $this->getCommitObjectName($item_hash); + $short_hash = $this->getCommitObjectName($hash); $is_disabled = $this->getCommitIsDisabled($commit); $author_view = $this->getCommitAuthorView($commit); $item_view = id(new PHUIObjectItemView()) ->setHeader($commit_description) ->setObjectName($short_hash) ->setHref($commit_link) ->setDisabled($is_disabled); if ($author_view !== null) { $item_view->addAttribute($author_view); } - $browse_button = $this->newBrowseButton($item_hash); + $browse_button = $this->newBrowseButton($hash); $build_view = null; if ($show_builds) { - $build_view = $this->newBuildView($item_hash); + $build_view = $this->newBuildView($hash); } $item_view->setSideColumn( array( $build_view, $browse_button, )); $revision_view = null; if ($show_revisions) { - $revision_view = $this->newRevisionView($item_hash); + $revision_view = $this->newRevisionView($hash); } if ($revision_view !== null) { $item_view->addAttribute($revision_view); } - $view = id(new PHUIObjectItemListView()) - ->setFlush(true) - ->addItem($item_view); + $views[$hash] = $item_view; + } + + return $views; + } + + private function newObjectItemRows() { + $viewer = $this->getViewer(); - $content[] = $view; + $items = $this->newHistoryItems(); + $views = $this->newObjectItemViews(); - $rows[] = $content; + $last_date = null; + $rows = array(); + foreach ($items as $hash => $item) { + $item_epoch = $item['epoch']; + $item_date = phabricator_date($item_epoch, $viewer); + if ($item_date !== $last_date) { + $last_date = $item_date; + $date_view = phutil_tag( + 'div', + array( + 'class' => 'diffusion-commit-graph-date-header', + ), + $item_date); + } else { + $date_view = null; + } + + $item_view = idx($views, $hash); + if ($item_view) { + $list_view = id(new PHUIObjectItemListView()) + ->setFlush(true) + ->addItem($item_view); + } else { + $list_view = null; + } + + $rows[] = array( + $date_view, + $list_view, + ); } + return $rows; + } + + public function render() { + $rows = $this->newObjectItemRows(); + $graph = $this->newGraphView(); foreach ($rows as $idx => $row) { $cells = array(); if ($graph) { $cells[] = phutil_tag( 'td', array( 'class' => 'diffusion-commit-graph-path-cell', ), $graph[$idx]); } $cells[] = phutil_tag( 'td', array( 'class' => 'diffusion-commit-graph-content-cell', ), $row); $rows[$idx] = phutil_tag('tr', array(), $cells); } $table = phutil_tag( 'table', array( 'class' => 'diffusion-commit-graph-table', ), $rows); return $table; } private function newGraphView() { if (!$this->getParents()) { return null; } $parents = $this->getParents(); // If we're filtering parents, remove relationships which point to // commits that are not part of the visible graph. Otherwise, we get // a big tree of nonsense when viewing release branches like "stable" // versus "master". if ($this->getFilterParents()) { foreach ($parents as $key => $nodes) { foreach ($nodes as $nkey => $node) { if (empty($parents[$node])) { unset($parents[$key][$nkey]); } } } } return id(new PHUIDiffGraphView()) ->setIsHead($this->getIsHead()) ->setIsTail($this->getIsTail()) ->renderGraph($parents); } private function shouldShowBuilds() { $viewer = $this->getViewer(); $show_builds = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorHarbormasterApplication', $this->getUser()); return $show_builds; } private function shouldShowRevisions() { $viewer = $this->getViewer(); $show_revisions = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDifferentialApplication', $viewer); return $show_revisions; } private function newHistoryItems() { $items = array(); - $commits = $this->getCommits(); - $commit_map = mpull($commits, null, 'getCommitIdentifier'); - $history = $this->getHistory(); if ($history !== null) { foreach ($history as $history_item) { $commit_hash = $history_item->getCommitIdentifier(); - $items[] = array( + $items[$commit_hash] = array( 'epoch' => $history_item->getEpoch(), 'hash' => $commit_hash, - 'commit' => idx($commit_map, $commit_hash), + 'commit' => $this->getCommit($commit_hash), ); } } else { + $commits = $this->getCommitMap(); foreach ($commits as $commit) { - $items[] = array( + $commit_hash = $commit->getCommitIdentifier(); + + $items[$commit_hash] = array( 'epoch' => $commit->getEpoch(), - 'hash' => $commit->getCommitIdentifier(), + 'hash' => $commit_hash, 'commit' => $commit, ); } } return $items; } private function getCommitDescription($commit) { if (!$commit) { return phutil_tag('em', array(), pht("Discovering\xE2\x80\xA6")); } // We can show details once the message and change have been imported. $partial_import = PhabricatorRepositoryCommit::IMPORTED_MESSAGE | PhabricatorRepositoryCommit::IMPORTED_CHANGE; if (!$commit->isPartiallyImported($partial_import)) { return phutil_tag('em', array(), pht("Importing\xE2\x80\xA6")); } return $commit->getCommitData()->getSummary(); } private function getCommitURI($hash) { $repository = $this->getRepository(); if ($repository) { return $repository->getCommitURI($hash); } $commit = $this->getCommit($hash); - return $commit->getURI(); + if ($commit) { + return $commit->getURI(); + } + + return null; } private function getCommitObjectName($hash) { $repository = $this->getRepository(); if ($repository) { return $repository->formatCommitName( $hash, $is_local = true); } $commit = $this->getCommit($hash); - return $commit->getDisplayName(); + if ($commit) { + return $commit->getDisplayName(); + } + + return null; } private function getCommitIsDisabled($commit) { if (!$commit) { return true; } if ($commit->isUnreachable()) { return true; } return false; } private function getCommitAuthorView($commit) { if (!$commit) { return null; } $viewer = $this->getViewer(); return $commit->newCommitAuthorView($viewer); } private function newBrowseButton($hash) { $repository = $this->getRepository(); if ($repository) { $drequest = $this->getDiffusionRequest(); return $this->linkBrowse( $drequest->getPath(), array( 'commit' => $hash, 'branch' => $drequest->getBranch(), ), $as_button = true); } return null; } private function getCommit($hash) { $commit_map = $this->getCommitMap(); return idx($commit_map, $hash); } private function getCommitMap() { + if ($this->commitMap === null) { + $commit_list = $this->newCommitList(); + $this->commitMap = mpull($commit_list, null, 'getCommitIdentifier'); + } + return $this->commitMap; } private function newBuildView($hash) { $commit = $this->getCommit($hash); if (!$commit) { return null; } $buildable = $this->getBuildable($commit); if (!$buildable) { return null; } return $this->renderBuildable($buildable, 'button'); } private function getBuildable(PhabricatorRepositoryCommit $commit) { $buildable_map = $this->getBuildableMap(); return idx($buildable_map, $commit->getPHID()); } private function getBuildableMap() { if ($this->buildableMap === null) { - $commits = $this->getCommits(); + $commits = $this->getCommitMap(); $buildables = $this->loadBuildables($commits); $this->buildableMap = $buildables; } return $this->buildableMap; } private function newRevisionView($hash) { $commit = $this->getCommit($hash); if (!$commit) { return null; } $revisions = $this->getRevisions($commit); if (!$revisions) { return null; } $revision = head($revisions); return id(new PHUITagView()) ->setName($revision->getMonogram()) ->setType(PHUITagView::TYPE_SHADE) ->setColor(PHUITagView::COLOR_BLUE) ->setHref($revision->getURI()) ->setBorder(PHUITagView::BORDER_NONE) ->setSlimShady(true); } private function getRevisions(PhabricatorRepositoryCommit $commit) { $revision_map = $this->getRevisionMap(); return idx($revision_map, $commit->getPHID(), array()); } private function getRevisionMap() { if ($this->revisionMap === null) { $this->revisionMap = $this->newRevisionMap(); } return $this->revisionMap; } private function newRevisionMap() { $viewer = $this->getViewer(); - $commits = $this->getCommits(); + $commits = $this->getCommitMap(); return DiffusionCommitRevisionQuery::loadRevisionMapForCommits( $viewer, $commits); } + private function newCommitList() { + $commits = $this->getCommits(); + if ($commits !== null) { + return $commits; + } + + $repository = $this->getRepository(); + if (!$repository) { + return array(); + } + + $history = $this->getHistory(); + if ($history === null) { + return array(); + } + + $identifiers = array(); + foreach ($history as $item) { + $identifiers[] = $item->getCommitIdentifier(); + } + + if (!$identifiers) { + return array(); + } + + $viewer = $this->getViewer(); + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepositoryPHIDs(array($repository->getPHID())) + ->withIdentifiers($identifiers) + ->needCommitData(true) + ->needIdentities(true) + ->execute(); + + return $commits; + } + }