Path: blob/master/src/applications/differential/controller/DifferentialRevisionViewController.php
12256 views
<?php12final class DifferentialRevisionViewController3extends DifferentialController {45private $revisionID;6private $changesetCount;7private $hiddenChangesets;8private $warnings = array();910public function shouldAllowPublic() {11return true;12}1314public function isLargeDiff() {15return ($this->getChangesetCount() > $this->getLargeDiffLimit());16}1718public function isVeryLargeDiff() {19return ($this->getChangesetCount() > $this->getVeryLargeDiffLimit());20}2122public function getLargeDiffLimit() {23return 200;24}2526public function getVeryLargeDiffLimit() {27return 2000;28}2930public function getChangesetCount() {31if ($this->changesetCount === null) {32throw new PhutilInvalidStateException('setChangesetCount');33}34return $this->changesetCount;35}3637public function setChangesetCount($count) {38$this->changesetCount = $count;39return $this;40}4142public function handleRequest(AphrontRequest $request) {43$viewer = $this->getViewer();44$this->revisionID = $request->getURIData('id');4546$viewer_is_anonymous = !$viewer->isLoggedIn();4748$revision = id(new DifferentialRevisionQuery())49->withIDs(array($this->revisionID))50->setViewer($viewer)51->needReviewers(true)52->needReviewerAuthority(true)53->needCommitPHIDs(true)54->executeOne();55if (!$revision) {56return new Aphront404Response();57}5859$diffs = id(new DifferentialDiffQuery())60->setViewer($viewer)61->withRevisionIDs(array($this->revisionID))62->execute();63$diffs = array_reverse($diffs, $preserve_keys = true);6465if (!$diffs) {66throw new Exception(67pht('This revision has no diffs. Something has gone quite wrong.'));68}6970$revision->attachActiveDiff(last($diffs));7172$diff_vs = $this->getOldDiffID($revision, $diffs);73if ($diff_vs instanceof AphrontResponse) {74return $diff_vs;75}7677$target_id = $this->getNewDiffID($revision, $diffs);78if ($target_id instanceof AphrontResponse) {79return $target_id;80}8182$target = $diffs[$target_id];8384$target_manual = $target;85if (!$target_id) {86foreach ($diffs as $diff) {87if ($diff->getCreationMethod() != 'commit') {88$target_manual = $diff;89}90}91}9293$repository = null;94$repository_phid = $target->getRepositoryPHID();95if ($repository_phid) {96if ($repository_phid == $revision->getRepositoryPHID()) {97$repository = $revision->getRepository();98} else {99$repository = id(new PhabricatorRepositoryQuery())100->setViewer($viewer)101->withPHIDs(array($repository_phid))102->executeOne();103}104}105106list($changesets, $vs_map, $vs_changesets, $rendering_references) =107$this->loadChangesetsAndVsMap(108$target,109idx($diffs, $diff_vs),110$repository);111112$this->setChangesetCount(count($rendering_references));113114if ($request->getExists('download')) {115return $this->buildRawDiffResponse(116$revision,117$changesets,118$vs_changesets,119$vs_map,120$repository);121}122123$map = $vs_map;124if (!$map) {125$map = array_fill_keys(array_keys($changesets), 0);126}127128$old_ids = array();129$new_ids = array();130foreach ($map as $id => $vs) {131if ($vs <= 0) {132$old_ids[] = $id;133$new_ids[] = $id;134} else {135$new_ids[] = $id;136$new_ids[] = $vs;137}138}139140$this->loadDiffProperties($diffs);141$props = $target_manual->getDiffProperties();142143$subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(144$revision->getPHID());145146$object_phids = array_merge(147$revision->getReviewerPHIDs(),148$subscriber_phids,149$revision->getCommitPHIDs(),150array(151$revision->getAuthorPHID(),152$viewer->getPHID(),153));154155foreach ($revision->getAttached() as $type => $phids) {156foreach ($phids as $phid => $info) {157$object_phids[] = $phid;158}159}160161$field_list = PhabricatorCustomField::getObjectFields(162$revision,163PhabricatorCustomField::ROLE_VIEW);164165$field_list->setViewer($viewer);166$field_list->readFieldsFromStorage($revision);167168$warning_handle_map = array();169foreach ($field_list->getFields() as $key => $field) {170$req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings();171foreach ($req as $phid) {172$warning_handle_map[$key][] = $phid;173$object_phids[] = $phid;174}175}176177$handles = $this->loadViewerHandles($object_phids);178$warnings = $this->warnings;179180$request_uri = $request->getRequestURI();181182$large = $request->getStr('large');183184$large_warning =185($this->isLargeDiff()) &&186(!$this->isVeryLargeDiff()) &&187(!$large);188189if ($large_warning) {190$count = $this->getChangesetCount();191192$expand_uri = $request_uri193->alter('large', 'true')194->setFragment('toc');195196$message = array(197pht(198'This large diff affects %s files. Files without inline '.199'comments have been collapsed.',200new PhutilNumber($count)),201' ',202phutil_tag(203'strong',204array(),205phutil_tag(206'a',207array(208'href' => $expand_uri,209),210pht('Expand All Files'))),211);212213$warnings[] = id(new PHUIInfoView())214->setTitle(pht('Large Diff'))215->setSeverity(PHUIInfoView::SEVERITY_WARNING)216->appendChild($message);217218$folded_changesets = $changesets;219} else {220$folded_changesets = array();221}222223// Don't hide or fold changesets which have inline comments.224$hidden_changesets = $this->hiddenChangesets;225if ($hidden_changesets || $folded_changesets) {226$old = array_select_keys($changesets, $old_ids);227$new = array_select_keys($changesets, $new_ids);228229$inlines = id(new DifferentialDiffInlineCommentQuery())230->setViewer($viewer)231->withRevisionPHIDs(array($revision->getPHID()))232->withPublishableComments(true)233->withPublishedComments(true)234->execute();235236$inlines = mpull($inlines, 'newInlineCommentObject');237238$inlines = id(new PhabricatorInlineCommentAdjustmentEngine())239->setViewer($viewer)240->setRevision($revision)241->setOldChangesets($old)242->setNewChangesets($new)243->setInlines($inlines)244->execute();245246foreach ($inlines as $inline) {247$changeset_id = $inline->getChangesetID();248if (!isset($changesets[$changeset_id])) {249continue;250}251252unset($hidden_changesets[$changeset_id]);253unset($folded_changesets[$changeset_id]);254}255}256257// If we would hide only one changeset, don't hide anything. The notice258// we'd render about it is about the same size as the changeset.259if (count($hidden_changesets) < 2) {260$hidden_changesets = array();261}262263// Update the set of hidden changesets, since we may have just un-hidden264// some of them.265if ($hidden_changesets) {266$warnings[] = id(new PHUIInfoView())267->setTitle(pht('Showing Only Differences'))268->setSeverity(PHUIInfoView::SEVERITY_NOTICE)269->appendChild(270pht(271'This revision modifies %s more files that are hidden because '.272'they were not modified between selected diffs and they have no '.273'inline comments.',274phutil_count($hidden_changesets)));275}276277// Compute the unfolded changesets. By default, everything is unfolded.278$unfolded_changesets = $changesets;279foreach ($folded_changesets as $changeset_id => $changeset) {280unset($unfolded_changesets[$changeset_id]);281}282283// Throw away any hidden changesets.284foreach ($hidden_changesets as $changeset_id => $changeset) {285unset($changesets[$changeset_id]);286unset($unfolded_changesets[$changeset_id]);287}288289$commit_hashes = mpull($diffs, 'getSourceControlBaseRevision');290$local_commits = idx($props, 'local:commits', array());291foreach ($local_commits as $local_commit) {292$commit_hashes[] = idx($local_commit, 'tree');293$commit_hashes[] = idx($local_commit, 'local');294}295$commit_hashes = array_unique(array_filter($commit_hashes));296if ($commit_hashes) {297$commits_for_links = id(new DiffusionCommitQuery())298->setViewer($viewer)299->withIdentifiers($commit_hashes)300->execute();301$commits_for_links = mpull(302$commits_for_links,303null,304'getCommitIdentifier');305} else {306$commits_for_links = array();307}308309$header = $this->buildHeader($revision);310$subheader = $this->buildSubheaderView($revision);311$details = $this->buildDetails($revision, $field_list);312$curtain = $this->buildCurtain($revision);313314$repository = $revision->getRepository();315if ($repository) {316$symbol_indexes = $this->buildSymbolIndexes(317$repository,318$unfolded_changesets);319} else {320$symbol_indexes = array();321}322323$revision_warnings = $this->buildRevisionWarnings(324$revision,325$field_list,326$warning_handle_map,327$handles);328$info_view = null;329if ($revision_warnings) {330$info_view = id(new PHUIInfoView())331->setSeverity(PHUIInfoView::SEVERITY_WARNING)332->setErrors($revision_warnings);333}334335$detail_diffs = array_select_keys(336$diffs,337array($diff_vs, $target->getID()));338$detail_diffs = mpull($detail_diffs, null, 'getPHID');339340$this->loadHarbormasterData($detail_diffs);341342$diff_detail_box = $this->buildDiffDetailView(343$detail_diffs,344$revision,345$field_list);346347$unit_box = $this->buildUnitMessagesView(348$target,349$revision);350351$timeline = $this->buildTransactions(352$revision,353$diff_vs ? $diffs[$diff_vs] : $target,354$target,355$old_ids,356$new_ids);357358$timeline->setQuoteRef($revision->getMonogram());359360if ($this->isVeryLargeDiff()) {361$messages = array();362363$messages[] = pht(364'This very large diff affects more than %s files. Use the %s to '.365'browse changes.',366new PhutilNumber($this->getVeryLargeDiffLimit()),367phutil_tag(368'a',369array(370'href' => '/differential/diff/'.$target->getID().'/changesets/',371),372phutil_tag('strong', array(), pht('Changeset List'))));373374$changeset_view = id(new PHUIInfoView())375->setErrors($messages);376} else {377$changeset_view = id(new DifferentialChangesetListView())378->setChangesets($changesets)379->setVisibleChangesets($unfolded_changesets)380->setStandaloneURI('/differential/changeset/')381->setRawFileURIs(382'/differential/changeset/?view=old',383'/differential/changeset/?view=new')384->setUser($viewer)385->setDiff($target)386->setRenderingReferences($rendering_references)387->setVsMap($vs_map)388->setSymbolIndexes($symbol_indexes)389->setTitle(pht('Diff %s', $target->getID()))390->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);391392$revision_id = $revision->getID();393$inline_list_uri = "/revision/inlines/{$revision_id}/";394$inline_list_uri = $this->getApplicationURI($inline_list_uri);395$changeset_view->setInlineListURI($inline_list_uri);396397if ($repository) {398$changeset_view->setRepository($repository);399}400401if (!$viewer_is_anonymous) {402$changeset_view->setInlineCommentControllerURI(403'/differential/comment/inline/edit/'.$revision->getID().'/');404}405}406407$broken_diffs = $this->loadHistoryDiffStatus($diffs);408409$history = id(new DifferentialRevisionUpdateHistoryView())410->setUser($viewer)411->setDiffs($diffs)412->setDiffUnitStatuses($broken_diffs)413->setSelectedVersusDiffID($diff_vs)414->setSelectedDiffID($target->getID())415->setCommitsForLinks($commits_for_links);416417$local_table = id(new DifferentialLocalCommitsView())418->setUser($viewer)419->setLocalCommits(idx($props, 'local:commits'))420->setCommitsForLinks($commits_for_links);421422if ($repository && !$this->isVeryLargeDiff()) {423$other_revisions = $this->loadOtherRevisions(424$changesets,425$target,426$repository);427} else {428$other_revisions = array();429}430431$other_view = null;432if ($other_revisions) {433$other_view = $this->renderOtherRevisions($other_revisions);434}435436if ($this->isVeryLargeDiff()) {437$toc_view = null;438439// When rendering a "very large" diff, we skip computation of owners440// that own no files because it is significantly expensive and not very441// valuable.442foreach ($revision->getReviewers() as $reviewer) {443// Give each reviewer a dummy nonempty value so the UI does not render444// the "(Owns No Changed Paths)" note. If that behavior becomes more445// sophisticated in the future, this behavior might also need to.446$reviewer->attachChangesets($changesets);447}448} else {449$this->buildPackageMaps($changesets);450451$toc_view = $this->buildTableOfContents(452$changesets,453$unfolded_changesets,454$target->loadCoverageMap($viewer));455456// Attach changesets to each reviewer so we can show which Owners package457// reviewers own no files.458foreach ($revision->getReviewers() as $reviewer) {459$reviewer_phid = $reviewer->getReviewerPHID();460$reviewer_changesets = $this->getPackageChangesets($reviewer_phid);461$reviewer->attachChangesets($reviewer_changesets);462}463464$authority_packages = $this->getAuthorityPackages();465foreach ($changesets as $changeset) {466$changeset_packages = $this->getChangesetPackages($changeset);467468$changeset469->setAuthorityPackages($authority_packages)470->setChangesetPackages($changeset_packages);471}472}473474$tab_group = new PHUITabGroupView();475476if ($toc_view) {477$tab_group->addTab(478id(new PHUITabView())479->setName(pht('Files'))480->setKey('files')481->appendChild($toc_view));482}483484$tab_group->addTab(485id(new PHUITabView())486->setName(pht('History'))487->setKey('history')488->appendChild($history));489490$filetree = id(new DifferentialFileTreeEngine())491->setViewer($viewer);492$filetree_collapsed = !$filetree->getIsVisible();493494// See PHI811. If the viewer has the file tree on, the files tab with the495// table of contents is redundant, so default to the "History" tab instead.496if (!$filetree_collapsed) {497$tab_group->selectTab('history');498}499500$tab_group->addTab(501id(new PHUITabView())502->setName(pht('Commits'))503->setKey('commits')504->appendChild($local_table));505506$stack_graph = id(new DifferentialRevisionGraph())507->setViewer($viewer)508->setSeedPHID($revision->getPHID())509->setLoadEntireGraph(true)510->loadGraph();511if (!$stack_graph->isEmpty()) {512// See PHI1900. The graph UI element now tries to figure out the correct513// height automatically, but currently can't in this case because the514// element is not visible when the page loads. Set an explicit height.515$stack_graph->setHeight(34);516517$stack_table = $stack_graph->newGraphTable();518519$parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;520$reachable = $stack_graph->getReachableObjects($parent_type);521522foreach ($reachable as $key => $reachable_revision) {523if ($reachable_revision->isClosed()) {524unset($reachable[$key]);525}526}527528if ($reachable) {529$stack_name = pht('Stack (%s Open)', phutil_count($reachable));530$stack_color = PHUIListItemView::STATUS_FAIL;531} else {532$stack_name = pht('Stack');533$stack_color = null;534}535536$tab_group->addTab(537id(new PHUITabView())538->setName($stack_name)539->setKey('stack')540->setColor($stack_color)541->appendChild($stack_table));542}543544if ($other_view) {545$tab_group->addTab(546id(new PHUITabView())547->setName(pht('Similar'))548->setKey('similar')549->appendChild($other_view));550}551552$view_button = id(new PHUIButtonView())553->setTag('a')554->setText(pht('Changeset List'))555->setHref('/differential/diff/'.$target->getID().'/changesets/')556->setIcon('fa-align-left');557558$tab_header = id(new PHUIHeaderView())559->setHeader(pht('Revision Contents'))560->addActionLink($view_button);561562$tab_view = id(new PHUIObjectBoxView())563->setHeader($tab_header)564->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)565->addTabGroup($tab_group);566567$signatures = DifferentialRequiredSignaturesField::loadForRevision(568$revision);569$missing_signatures = false;570foreach ($signatures as $phid => $signed) {571if (!$signed) {572$missing_signatures = true;573}574}575576$footer = array();577$signature_message = null;578if ($missing_signatures) {579$signature_message = id(new PHUIInfoView())580->setTitle(pht('Content Hidden'))581->appendChild(582pht(583'The content of this revision is hidden until the author has '.584'signed all of the required legal agreements.'));585} else {586$anchor = id(new PhabricatorAnchorView())587->setAnchorName('toc')588->setNavigationMarker(true);589590$footer[] = array(591$anchor,592$warnings,593$tab_view,594$changeset_view,595);596}597598$comment_view = id(new DifferentialRevisionEditEngine())599->setViewer($viewer)600->buildEditEngineCommentView($revision);601602$comment_view->setTransactionTimeline($timeline);603604$review_warnings = array();605foreach ($field_list->getFields() as $field) {606$review_warnings[] = $field->getWarningsForDetailView();607}608$review_warnings = array_mergev($review_warnings);609610if ($review_warnings) {611$warnings_view = id(new PHUIInfoView())612->setSeverity(PHUIInfoView::SEVERITY_WARNING)613->setErrors($review_warnings);614615$comment_view->setInfoView($warnings_view);616}617618$footer[] = $comment_view;619620$monogram = $revision->getMonogram();621$operations_box = $this->buildOperationsBox($revision);622623$crumbs = $this->buildApplicationCrumbs();624$crumbs->addTextCrumb($monogram);625$crumbs->setBorder(true);626627$filetree628->setChangesets($changesets)629->setDisabled($this->isVeryLargeDiff());630631$view = id(new PHUITwoColumnView())632->setHeader($header)633->setSubheader($subheader)634->setCurtain($curtain)635->setMainColumn(636array(637$operations_box,638$info_view,639$details,640$diff_detail_box,641$unit_box,642$timeline,643$signature_message,644))645->setFooter($footer);646647$main_content = array(648$crumbs,649$view,650);651652$main_content = $filetree->newView($main_content);653654if (!$filetree->getDisabled()) {655$changeset_view->setFormationView($main_content);656}657658$page = $this->newPage()659->setTitle($monogram.' '.$revision->getTitle())660->setPageObjectPHIDs(array($revision->getPHID()))661->appendChild($main_content);662663return $page;664}665666private function buildHeader(DifferentialRevision $revision) {667$view = id(new PHUIHeaderView())668->setHeader($revision->getTitle($revision))669->setUser($this->getViewer())670->setPolicyObject($revision)671->setHeaderIcon('fa-cog');672673$status_tag = id(new PHUITagView())674->setName($revision->getStatusDisplayName())675->setIcon($revision->getStatusIcon())676->setColor($revision->getStatusTagColor())677->setType(PHUITagView::TYPE_SHADE);678679$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_tag);680681// If the revision is in a status other than "Draft", but not broadcasting,682// add an additional "Draft" tag to the header to make it clear that this683// revision hasn't promoted yet.684if (!$revision->getShouldBroadcast() && !$revision->isDraft()) {685$draft_status = DifferentialRevisionStatus::newForStatus(686DifferentialRevisionStatus::DRAFT);687688$draft_tag = id(new PHUITagView())689->setName($draft_status->getDisplayName())690->setIcon($draft_status->getIcon())691->setColor($draft_status->getTagColor())692->setType(PHUITagView::TYPE_SHADE);693694$view->addTag($draft_tag);695}696697return $view;698}699700private function buildSubheaderView(DifferentialRevision $revision) {701$viewer = $this->getViewer();702703$author_phid = $revision->getAuthorPHID();704705$author = $viewer->renderHandle($author_phid)->render();706$date = phabricator_datetime($revision->getDateCreated(), $viewer);707$author = phutil_tag('strong', array(), $author);708709$handles = $viewer->loadHandles(array($author_phid));710$image_uri = $handles[$author_phid]->getImageURI();711$image_href = $handles[$author_phid]->getURI();712713$content = pht('Authored by %s on %s.', $author, $date);714715return id(new PHUIHeadThingView())716->setImage($image_uri)717->setImageHref($image_href)718->setContent($content);719}720721private function buildDetails(722DifferentialRevision $revision,723$custom_fields) {724$viewer = $this->getViewer();725$properties = id(new PHUIPropertyListView())726->setUser($viewer);727728if ($custom_fields) {729$custom_fields->appendFieldsToPropertyList(730$revision,731$viewer,732$properties);733}734735$header = id(new PHUIHeaderView())736->setHeader(pht('Details'));737738return id(new PHUIObjectBoxView())739->setHeader($header)740->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)741->appendChild($properties);742}743744private function buildCurtain(DifferentialRevision $revision) {745$viewer = $this->getViewer();746$revision_id = $revision->getID();747$revision_phid = $revision->getPHID();748$curtain = $this->newCurtainView($revision);749750$can_edit = PhabricatorPolicyFilter::hasCapability(751$viewer,752$revision,753PhabricatorPolicyCapability::CAN_EDIT);754755$curtain->addAction(756id(new PhabricatorActionView())757->setIcon('fa-pencil')758->setHref("/differential/revision/edit/{$revision_id}/")759->setName(pht('Edit Revision'))760->setDisabled(!$can_edit)761->setWorkflow(!$can_edit));762763$curtain->addAction(764id(new PhabricatorActionView())765->setIcon('fa-upload')766->setHref("/differential/revision/update/{$revision_id}/")767->setName(pht('Update Diff'))768->setDisabled(!$can_edit)769->setWorkflow(!$can_edit));770771$request_uri = $this->getRequest()->getRequestURI();772$curtain->addAction(773id(new PhabricatorActionView())774->setIcon('fa-download')775->setName(pht('Download Raw Diff'))776->setHref($request_uri->alter('download', 'true')));777778$relationship_list = PhabricatorObjectRelationshipList::newForObject(779$viewer,780$revision);781782$revision_actions = array(783DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY,784DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY,785);786787$revision_submenu = $relationship_list->newActionSubmenu($revision_actions)788->setName(pht('Edit Related Revisions...'))789->setIcon('fa-cog');790791$curtain->addAction($revision_submenu);792793$relationship_submenu = $relationship_list->newActionMenu();794if ($relationship_submenu) {795$curtain->addAction($relationship_submenu);796}797798$repository = $revision->getRepository();799if ($repository && $repository->canPerformAutomation()) {800$revision_id = $revision->getID();801802$op = new DrydockLandRepositoryOperation();803$barrier = $op->getBarrierToLanding($viewer, $revision);804805if ($barrier) {806$can_land = false;807} else {808$can_land = true;809}810811$action = id(new PhabricatorActionView())812->setName(pht('Land Revision'))813->setIcon('fa-fighter-jet')814->setHref("/differential/revision/operation/{$revision_id}/")815->setWorkflow(true)816->setDisabled(!$can_land);817818$curtain->addAction($action);819}820821return $curtain;822}823824private function loadHistoryDiffStatus(array $diffs) {825assert_instances_of($diffs, 'DifferentialDiff');826827$diff_phids = mpull($diffs, 'getPHID');828$bad_unit_status = array(829ArcanistUnitTestResult::RESULT_FAIL,830ArcanistUnitTestResult::RESULT_BROKEN,831);832833$message = new HarbormasterBuildUnitMessage();834$target = new HarbormasterBuildTarget();835$build = new HarbormasterBuild();836$buildable = new HarbormasterBuildable();837838$broken_diffs = queryfx_all(839$message->establishConnection('r'),840'SELECT distinct a.buildablePHID841FROM %T m842JOIN %T t ON m.buildTargetPHID = t.phid843JOIN %T b ON t.buildPHID = b.phid844JOIN %T a ON b.buildablePHID = a.phid845WHERE a.buildablePHID IN (%Ls)846AND m.result in (%Ls)',847$message->getTableName(),848$target->getTableName(),849$build->getTableName(),850$buildable->getTableName(),851$diff_phids,852$bad_unit_status);853854$unit_status = array();855foreach ($broken_diffs as $broken) {856$phid = $broken['buildablePHID'];857$unit_status[$phid] = DifferentialUnitStatus::UNIT_FAIL;858}859860return $unit_status;861}862863private function loadChangesetsAndVsMap(864DifferentialDiff $target,865DifferentialDiff $diff_vs = null,866PhabricatorRepository $repository = null) {867$viewer = $this->getViewer();868869$load_diffs = array($target);870if ($diff_vs) {871$load_diffs[] = $diff_vs;872}873874$raw_changesets = id(new DifferentialChangesetQuery())875->setViewer($viewer)876->withDiffs($load_diffs)877->execute();878$changeset_groups = mgroup($raw_changesets, 'getDiffID');879880$changesets = idx($changeset_groups, $target->getID(), array());881$changesets = mpull($changesets, null, 'getID');882883$refs = array();884$vs_map = array();885$vs_changesets = array();886$must_compare = array();887if ($diff_vs) {888$vs_id = $diff_vs->getID();889$vs_changesets_path_map = array();890foreach (idx($changeset_groups, $vs_id, array()) as $changeset) {891$path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs);892$vs_changesets_path_map[$path] = $changeset;893$vs_changesets[$changeset->getID()] = $changeset;894}895896foreach ($changesets as $key => $changeset) {897$path = $changeset->getAbsoluteRepositoryPath($repository, $target);898if (isset($vs_changesets_path_map[$path])) {899$vs_map[$changeset->getID()] =900$vs_changesets_path_map[$path]->getID();901$refs[$changeset->getID()] =902$changeset->getID().'/'.$vs_changesets_path_map[$path]->getID();903unset($vs_changesets_path_map[$path]);904905$must_compare[] = $changeset->getID();906907} else {908$refs[$changeset->getID()] = $changeset->getID();909}910}911912foreach ($vs_changesets_path_map as $path => $changeset) {913$changesets[$changeset->getID()] = $changeset;914$vs_map[$changeset->getID()] = -1;915$refs[$changeset->getID()] = $changeset->getID().'/-1';916}917918} else {919foreach ($changesets as $changeset) {920$refs[$changeset->getID()] = $changeset->getID();921}922}923924$changesets = msort($changesets, 'getSortKey');925926// See T13137. When displaying the diff between two updates, hide any927// changesets which haven't actually changed.928$this->hiddenChangesets = array();929foreach ($must_compare as $changeset_id) {930$changeset = $changesets[$changeset_id];931$vs_changeset = $vs_changesets[$vs_map[$changeset_id]];932933if ($changeset->hasSameEffectAs($vs_changeset)) {934$this->hiddenChangesets[$changeset_id] = $changesets[$changeset_id];935}936}937938return array($changesets, $vs_map, $vs_changesets, $refs);939}940941private function buildSymbolIndexes(942PhabricatorRepository $repository,943array $unfolded_changesets) {944assert_instances_of($unfolded_changesets, 'DifferentialChangeset');945946$engine = PhabricatorSyntaxHighlighter::newEngine();947948$langs = $repository->getSymbolLanguages();949$langs = nonempty($langs, array());950951$sources = $repository->getSymbolSources();952$sources = nonempty($sources, array());953954$symbol_indexes = array();955956if ($langs && $sources) {957$have_symbols = id(new DiffusionSymbolQuery())958->existsSymbolsInRepository($repository->getPHID());959if (!$have_symbols) {960return $symbol_indexes;961}962}963964$repository_phids = array_merge(965array($repository->getPHID()),966$sources);967968$indexed_langs = array_fill_keys($langs, true);969foreach ($unfolded_changesets as $key => $changeset) {970$lang = $engine->getLanguageFromFilename($changeset->getFilename());971if (empty($indexed_langs) || isset($indexed_langs[$lang])) {972$symbol_indexes[$key] = array(973'lang' => $lang,974'repositories' => $repository_phids,975);976}977}978979return $symbol_indexes;980}981982private function loadOtherRevisions(983array $changesets,984DifferentialDiff $target,985PhabricatorRepository $repository) {986assert_instances_of($changesets, 'DifferentialChangeset');987988$viewer = $this->getViewer();989990$paths = array();991foreach ($changesets as $changeset) {992$paths[] = $changeset->getAbsoluteRepositoryPath(993$repository,994$target);995}996997if (!$paths) {998return array();999}10001001$recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds'));10021003$query = id(new DifferentialRevisionQuery())1004->setViewer($viewer)1005->withIsOpen(true)1006->withUpdatedEpochBetween($recent, null)1007->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED)1008->setLimit(10)1009->needFlags(true)1010->needDrafts(true)1011->needReviewers(true)1012->withRepositoryPHIDs(1013array(1014$repository->getPHID(),1015))1016->withPaths($paths);10171018$results = $query->execute();10191020// Strip out *this* revision.1021foreach ($results as $key => $result) {1022if ($result->getID() == $this->revisionID) {1023unset($results[$key]);1024break;1025}1026}10271028return $results;1029}10301031private function renderOtherRevisions(array $revisions) {1032assert_instances_of($revisions, 'DifferentialRevision');1033$viewer = $this->getViewer();10341035$header = id(new PHUIHeaderView())1036->setHeader(pht('Recent Similar Revisions'));10371038return id(new DifferentialRevisionListView())1039->setViewer($viewer)1040->setRevisions($revisions)1041->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)1042->setNoBox(true);1043}104410451046private function buildRawDiffResponse(1047DifferentialRevision $revision,1048array $changesets,1049array $vs_changesets,1050array $vs_map,1051PhabricatorRepository $repository = null) {10521053assert_instances_of($changesets, 'DifferentialChangeset');1054assert_instances_of($vs_changesets, 'DifferentialChangeset');10551056$viewer = $this->getViewer();10571058id(new DifferentialHunkQuery())1059->setViewer($viewer)1060->withChangesets($changesets)1061->needAttachToChangesets(true)1062->execute();10631064$diff = new DifferentialDiff();1065$diff->attachChangesets($changesets);1066$raw_changes = $diff->buildChangesList();1067$changes = array();1068foreach ($raw_changes as $changedict) {1069$changes[] = ArcanistDiffChange::newFromDictionary($changedict);1070}10711072$loader = id(new PhabricatorFileBundleLoader())1073->setViewer($viewer);10741075$bundle = ArcanistBundle::newFromChanges($changes);1076$bundle->setLoadFileDataCallback(array($loader, 'loadFileData'));10771078$vcs = $repository ? $repository->getVersionControlSystem() : null;1079switch ($vcs) {1080case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:1081case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:1082$raw_diff = $bundle->toGitPatch();1083break;1084case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:1085default:1086$raw_diff = $bundle->toUnifiedDiff();1087break;1088}10891090$request_uri = $this->getRequest()->getRequestURI();10911092// this ends up being something like1093// D123.diff1094// or the verbose1095// D123.vs123.id123.highlightjs.diff1096// lame but nice to include these options1097$file_name = ltrim($request_uri->getPath(), '/').'.';1098foreach ($request_uri->getQueryParamsAsPairList() as $pair) {1099list($key, $value) = $pair;1100if ($key == 'download') {1101continue;1102}1103$file_name .= $key.$value.'.';1104}1105$file_name .= 'diff';11061107$iterator = new ArrayIterator(array($raw_diff));11081109$source = id(new PhabricatorIteratorFileUploadSource())1110->setName($file_name)1111->setMIMEType('text/plain')1112->setRelativeTTL(phutil_units('24 hours in seconds'))1113->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)1114->setIterator($iterator);11151116$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();1117$file = $source->uploadFile();1118$file->attachToObject($revision->getPHID());1119unset($unguarded);11201121return $file->getRedirectResponse();1122}11231124private function buildTransactions(1125DifferentialRevision $revision,1126DifferentialDiff $left_diff,1127DifferentialDiff $right_diff,1128array $old_ids,1129array $new_ids) {11301131$timeline = $this->buildTransactionTimeline(1132$revision,1133new DifferentialTransactionQuery(),1134$engine = null,1135array(1136'left' => $left_diff->getID(),1137'right' => $right_diff->getID(),1138'old' => implode(',', $old_ids),1139'new' => implode(',', $new_ids),1140));11411142return $timeline;1143}11441145private function buildRevisionWarnings(1146DifferentialRevision $revision,1147PhabricatorCustomFieldList $field_list,1148array $warning_handle_map,1149array $handles) {11501151$warnings = array();1152foreach ($field_list->getFields() as $key => $field) {1153$phids = idx($warning_handle_map, $key, array());1154$field_handles = array_select_keys($handles, $phids);1155$field_warnings = $field->getWarningsForRevisionHeader($field_handles);1156foreach ($field_warnings as $warning) {1157$warnings[] = $warning;1158}1159}11601161return $warnings;1162}11631164private function buildDiffDetailView(1165array $diffs,1166DifferentialRevision $revision,1167PhabricatorCustomFieldList $field_list) {1168$viewer = $this->getViewer();11691170$fields = array();1171foreach ($field_list->getFields() as $field) {1172if ($field->shouldAppearInDiffPropertyView()) {1173$fields[] = $field;1174}1175}11761177if (!$fields) {1178return null;1179}11801181$property_lists = array();1182foreach ($this->getDiffTabLabels($diffs) as $tab) {1183list($label, $diff) = $tab;11841185$property_lists[] = array(1186$label,1187$this->buildDiffPropertyList($diff, $revision, $fields),1188);1189}11901191$tab_group = id(new PHUITabGroupView())1192->setHideSingleTab(true);11931194foreach ($property_lists as $key => $property_list) {1195list($tab_name, $list_view) = $property_list;11961197$tab = id(new PHUITabView())1198->setKey($key)1199->setName($tab_name)1200->appendChild($list_view);12011202$tab_group->addTab($tab);1203$tab_group->selectTab($key);1204}12051206return id(new PHUIObjectBoxView())1207->setHeaderText(pht('Diff Detail'))1208->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)1209->setUser($viewer)1210->addTabGroup($tab_group);1211}12121213private function buildDiffPropertyList(1214DifferentialDiff $diff,1215DifferentialRevision $revision,1216array $fields) {1217$viewer = $this->getViewer();12181219$view = id(new PHUIPropertyListView())1220->setUser($viewer)1221->setObject($diff);12221223foreach ($fields as $field) {1224$label = $field->renderDiffPropertyViewLabel($diff);1225$value = $field->renderDiffPropertyViewValue($diff);1226if ($value !== null) {1227$view->addProperty($label, $value);1228}1229}12301231return $view;1232}12331234private function buildOperationsBox(DifferentialRevision $revision) {1235$viewer = $this->getViewer();12361237// Save a query if we can't possibly have pending operations.1238$repository = $revision->getRepository();1239if (!$repository || !$repository->canPerformAutomation()) {1240return null;1241}12421243$operations = id(new DrydockRepositoryOperationQuery())1244->setViewer($viewer)1245->withObjectPHIDs(array($revision->getPHID()))1246->withIsDismissed(false)1247->withOperationTypes(1248array(1249DrydockLandRepositoryOperation::OPCONST,1250))1251->execute();1252if (!$operations) {1253return null;1254}12551256$state_fail = DrydockRepositoryOperation::STATE_FAIL;12571258// We're going to show the oldest operation which hasn't failed, or the1259// most recent failure if they're all failures.1260$operations = msort($operations, 'getID');1261foreach ($operations as $operation) {1262if ($operation->getOperationState() != $state_fail) {1263break;1264}1265}12661267// If we found a completed operation, don't render anything. We don't want1268// to show an older error after the thing worked properly.1269if ($operation->isDone()) {1270return null;1271}12721273$box_view = id(new PHUIObjectBoxView())1274->setHeaderText(pht('Active Operations'))1275->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);12761277return id(new DrydockRepositoryOperationStatusView())1278->setUser($viewer)1279->setBoxView($box_view)1280->setOperation($operation);1281}12821283private function buildUnitMessagesView(1284DifferentialDiff $diff,1285DifferentialRevision $revision) {1286$viewer = $this->getViewer();12871288if (!$diff->getBuildable()) {1289return null;1290}12911292if (!$diff->getUnitMessages()) {1293return null;1294}12951296$interesting_messages = array();1297foreach ($diff->getUnitMessages() as $message) {1298switch ($message->getResult()) {1299case ArcanistUnitTestResult::RESULT_PASS:1300case ArcanistUnitTestResult::RESULT_SKIP:1301break;1302default:1303$interesting_messages[] = $message;1304break;1305}1306}13071308if (!$interesting_messages) {1309return null;1310}13111312return id(new HarbormasterUnitSummaryView())1313->setViewer($viewer)1314->setBuildable($diff->getBuildable())1315->setUnitMessages($diff->getUnitMessages())1316->setLimit(5)1317->setShowViewAll(true);1318}13191320private function getOldDiffID(DifferentialRevision $revision, array $diffs) {1321assert_instances_of($diffs, 'DifferentialDiff');1322$request = $this->getRequest();13231324$diffs = mpull($diffs, null, 'getID');13251326$is_new = ($request->getURIData('filter') === 'new');1327$old_id = $request->getInt('vs');13281329// This is ambiguous, so just 404 rather than trying to figure out what1330// the user expects.1331if ($is_new && $old_id) {1332return new Aphront404Response();1333}13341335if ($is_new) {1336$viewer = $this->getViewer();13371338$xactions = id(new DifferentialTransactionQuery())1339->setViewer($viewer)1340->withObjectPHIDs(array($revision->getPHID()))1341->withAuthorPHIDs(array($viewer->getPHID()))1342->setOrder('newest')1343->setLimit(1)1344->execute();13451346if (!$xactions) {1347$this->warnings[] = id(new PHUIInfoView())1348->setTitle(pht('No Actions'))1349->setSeverity(PHUIInfoView::SEVERITY_WARNING)1350->appendChild(1351pht(1352'Showing all changes because you have never taken an '.1353'action on this revision.'));1354} else {1355$xaction = head($xactions);13561357// Find the transactions which updated this revision. We want to1358// figure out which diff was active when you last took an action.1359$updates = id(new DifferentialTransactionQuery())1360->setViewer($viewer)1361->withObjectPHIDs(array($revision->getPHID()))1362->withTransactionTypes(1363array(1364DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE,1365))1366->setOrder('oldest')1367->execute();13681369// Sort the diffs into two buckets: those older than your last action1370// and those newer than your last action.1371$older = array();1372$newer = array();1373foreach ($updates as $update) {1374// If you updated the revision with "arc diff", try to count that1375// update as "before your last action".1376if ($update->getDateCreated() <= $xaction->getDateCreated()) {1377$older[] = $update->getNewValue();1378} else {1379$newer[] = $update->getNewValue();1380}1381}13821383if (!$newer) {1384$this->warnings[] = id(new PHUIInfoView())1385->setTitle(pht('No Recent Updates'))1386->setSeverity(PHUIInfoView::SEVERITY_WARNING)1387->appendChild(1388pht(1389'Showing all changes because the diff for this revision '.1390'has not been updated since your last action.'));1391} else {1392$older = array_fuse($older);13931394// Find the most recent diff from before the last action.1395$old = null;1396foreach ($diffs as $diff) {1397if (!isset($older[$diff->getPHID()])) {1398break;1399}14001401$old = $diff;1402}14031404// It's possible we may not find such a diff: transactions may have1405// been removed from the database, for example. If we miss, just1406// fail into some reasonable state since 404'ing would be perplexing.1407if ($old) {1408$this->warnings[] = id(new PHUIInfoView())1409->setTitle(pht('New Changes Shown'))1410->setSeverity(PHUIInfoView::SEVERITY_NOTICE)1411->appendChild(1412pht(1413'Showing changes since the last action you took on this '.1414'revision.'));14151416$old_id = $old->getID();1417}1418}1419}1420}14211422if (isset($diffs[$old_id])) {1423return $old_id;1424}14251426return null;1427}14281429private function getNewDiffID(DifferentialRevision $revision, array $diffs) {1430assert_instances_of($diffs, 'DifferentialDiff');1431$request = $this->getRequest();14321433$diffs = mpull($diffs, null, 'getID');14341435$is_new = ($request->getURIData('filter') === 'new');1436$new_id = $request->getInt('id');14371438if ($is_new && $new_id) {1439return new Aphront404Response();1440}14411442if (isset($diffs[$new_id])) {1443return $new_id;1444}14451446return (int)last_key($diffs);1447}14481449}145014511452