diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 6b38ed042..168d053c6 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1,1091 +1,1107 @@ <?php final class DifferentialRevisionViewController extends DifferentialController { private $revisionID; public function shouldAllowPublic() { return true; } public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); $this->revisionID = $request->getURIData('id'); $viewer_is_anonymous = !$viewer->isLoggedIn(); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) ->setViewer($request->getUser()) ->needRelationships(true) ->needReviewerStatus(true) ->needReviewerAuthority(true) ->executeOne(); if (!$revision) { return new Aphront404Response(); } $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withRevisionIDs(array($this->revisionID)) ->execute(); $diffs = array_reverse($diffs, $preserve_keys = true); if (!$diffs) { throw new Exception( 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(); $object_phids = array_merge( $revision->getReviewers(), $revision->getCCPHIDs(), $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 <strong>%s</strong>', 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); - $comment_view = $this->buildTransactions( + $timeline = $this->buildTransactions( $revision, $diff_vs ? $diffs[$diff_vs] : $target, $target, $old_ids, $new_ids); - if (!$viewer_is_anonymous) { - $comment_view->setQuoteRef('D'.$revision->getID()); - $comment_view->setQuoteTargetID('comment-content'); - } + $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, ); } - $footer[] = id(new DifferentialRevisionEditEngine()) + $comment_view = id(new DifferentialRevisionEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($revision); - $object_id = 'D'.$revision->getID(); + $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($object_id, '/'.$object_id); + $crumbs->addTextCrumb($monogram, $revision->getURI()); $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('D'.$revision->getID()) - ->setBaseURI(new PhutilURI('/D'.$revision->getID())) + ->setTitle($monogram) + ->setBaseURI(new PhutilURI($revision->getURI())) ->setCollapsed((bool)$collapsed_value) ->build($changesets); } Javelin::initBehavior('differential-user-select'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setCurtain($curtain) - ->setMainColumn(array( - $operations_box, - $info_view, - $details, - $diff_detail_box, - $unit_box, - $comment_view, - $signature_message, - )) + ->setMainColumn( + array( + $operations_box, + $info_view, + $details, + $diff_detail_box, + $unit_box, + $timeline, + $signature_message, + )) ->setFooter($footer); $page = $this->newPage() - ->setTitle($object_id.' '.$revision->getTitle()) + ->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); 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/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 13054f63c..44793d187 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -1,477 +1,496 @@ <?php /** * @concrete-extensible */ class PhabricatorApplicationTransactionCommentView extends AphrontView { private $submitButtonName; private $action; private $previewPanelID; private $previewTimelineID; private $previewToggleID; private $formID; private $commentID; private $draft; private $requestURI; private $showPreview = true; private $objectPHID; private $headerText; private $noPermission; private $fullWidth; + private $infoView; private $currentVersion; private $versionedDraft; private $commentActions; private $commentActionGroups = array(); private $transactionTimeline; public function setObjectPHID($object_phid) { $this->objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setShowPreview($show_preview) { $this->showPreview = $show_preview; return $this; } public function getShowPreview() { return $this->showPreview; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setCurrentVersion($current_version) { $this->currentVersion = $current_version; return $this; } public function getCurrentVersion() { return $this->currentVersion; } public function setVersionedDraft( PhabricatorVersionedDraft $versioned_draft) { $this->versionedDraft = $versioned_draft; return $this; } public function getVersionedDraft() { return $this->versionedDraft; } public function setDraft(PhabricatorDraft $draft) { $this->draft = $draft; return $this; } public function getDraft() { return $this->draft; } public function setSubmitButtonName($submit_button_name) { $this->submitButtonName = $submit_button_name; return $this; } public function getSubmitButtonName() { return $this->submitButtonName; } public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } public function setHeaderText($text) { $this->headerText = $text; return $this; } public function setFullWidth($fw) { $this->fullWidth = $fw; return $this; } + public function setInfoView(PHUIInfoView $info_view) { + $this->infoView = $info_view; + return $this; + } + + public function getInfoView() { + return $this->infoView; + } + public function setCommentActions(array $comment_actions) { assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction'); $this->commentActions = $comment_actions; return $this; } public function getCommentActions() { return $this->commentActions; } public function setCommentActionGroups(array $groups) { assert_instances_of($groups, 'PhabricatorEditEngineCommentActionGroup'); $this->commentActionGroups = $groups; return $this; } public function getCommentActionGroups() { return $this->commentActionGroups; } public function setNoPermission($no_permission) { $this->noPermission = $no_permission; return $this; } public function getNoPermission() { return $this->noPermission; } public function setTransactionTimeline( PhabricatorApplicationTransactionView $timeline) { $timeline->setQuoteTargetID($this->getCommentID()); if ($this->getNoPermission()) { $timeline->setShouldTerminate(true); } $this->transactionTimeline = $timeline; return $this; } public function render() { if ($this->getNoPermission()) { return null; } $user = $this->getUser(); if (!$user->isLoggedIn()) { $uri = id(new PhutilURI('/login/')) ->setQueryParam('next', (string)$this->getRequestURI()); return id(new PHUIObjectBoxView()) ->setFlush(true) ->appendChild( javelin_tag( 'a', array( 'class' => 'login-to-comment button', 'href' => $uri, ), pht('Login to Comment'))); } $data = array(); $comment = $this->renderCommentPanel(); if ($this->getShowPreview()) { $preview = $this->renderPreviewPanel(); } else { $preview = null; } if (!$this->getCommentActions()) { Javelin::initBehavior( 'phabricator-transaction-comment-form', array( 'formID' => $this->getFormID(), 'timelineID' => $this->getPreviewTimelineID(), 'panelID' => $this->getPreviewPanelID(), 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), )); } require_celerity_resource('phui-comment-form-css'); $image_uri = $user->getProfileImageURI(); $image = phutil_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phui-comment-image', )); $wedge = phutil_tag( 'div', array( 'class' => 'phui-timeline-wedge', ), ''); $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->addClass('phui-comment-form-view') ->addSigil('phui-comment-form') ->appendChild($image) ->appendChild($wedge) ->appendChild($comment); return array($comment_box, $preview); } private function renderCommentPanel() { $draft_comment = ''; $draft_key = null; if ($this->getDraft()) { $draft_comment = $this->getDraft()->getDraft(); $draft_key = $this->getDraft()->getDraftKey(); } $versioned_draft = $this->getVersionedDraft(); if ($versioned_draft) { $draft_comment = $versioned_draft->getProperty('comment', ''); } if (!$this->getObjectPHID()) { throw new PhutilInvalidStateException('setObjectPHID', 'render'); } $version_key = PhabricatorVersionedDraft::KEY_VERSION; $version_value = $this->getCurrentVersion(); $form = id(new AphrontFormView()) ->setUser($this->getUser()) ->addSigil('transaction-append') ->setWorkflow(true) ->setFullWidth($this->fullWidth) ->setMetadata( array( 'objectPHID' => $this->getObjectPHID(), )) ->setAction($this->getAction()) ->setID($this->getFormID()) ->addHiddenInput('__draft__', $draft_key) ->addHiddenInput($version_key, $version_value); $comment_actions = $this->getCommentActions(); if ($comment_actions) { $action_map = array(); $type_map = array(); $comment_actions = mpull($comment_actions, null, 'getKey'); $draft_actions = array(); $draft_keys = array(); if ($versioned_draft) { $draft_actions = $versioned_draft->getProperty('actions', array()); if (!is_array($draft_actions)) { $draft_actions = array(); } foreach ($draft_actions as $action) { $type = idx($action, 'type'); $comment_action = idx($comment_actions, $type); if (!$comment_action) { continue; } $value = idx($action, 'value'); $comment_action->setValue($value); $draft_keys[] = $type; } } foreach ($comment_actions as $key => $comment_action) { $key = $comment_action->getKey(); $action_map[$key] = array( 'key' => $key, 'label' => $comment_action->getLabel(), 'type' => $comment_action->getPHUIXControlType(), 'spec' => $comment_action->getPHUIXControlSpecification(), 'initialValue' => $comment_action->getInitialValue(), 'groupKey' => $comment_action->getGroupKey(), ); $type_map[$key] = $comment_action; } $options = $this->newCommentActionOptions($action_map); $action_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); $place_id = celerity_generate_unique_node_id(); $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'editengine.actions', 'id' => $input_id, ))); $invisi_bar = phutil_tag( 'div', array( 'id' => $place_id, 'class' => 'phui-comment-control-stack', )); $action_select = id(new AphrontFormSelectControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-action-control') ->setID($action_id) ->setOptions($options); $action_bar = phutil_tag( 'div', array( 'class' => 'phui-comment-action-bar grouped', ), array( $action_select, )); $form->appendChild($action_bar); + + $info_view = $this->getInfoView(); + if ($info_view) { + $info_box = id(new PHUIBoxView()) + ->addMargin(PHUI::MARGIN_LARGE) + ->appendChild($info_view); + $form->appendChild($info_box); + } + $form->appendChild($invisi_bar); $form->addClass('phui-comment-has-actions'); Javelin::initBehavior( 'comment-actions', array( 'actionID' => $action_id, 'inputID' => $input_id, 'formID' => $this->getFormID(), 'placeID' => $place_id, 'panelID' => $this->getPreviewPanelID(), 'timelineID' => $this->getPreviewTimelineID(), 'actions' => $action_map, 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), 'drafts' => $draft_keys, )); } $submit_button = id(new AphrontFormSubmitControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-submit-control') ->setValue($this->getSubmitButtonName()); $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setID($this->getCommentID()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-textarea-control') ->setCanPin(true) ->setName('comment') ->setUser($this->getUser()) ->setValue($draft_comment)) ->appendChild( id(new AphrontFormSubmitControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-submit-control') ->setValue($this->getSubmitButtonName())); return $form; } private function renderPreviewPanel() { $preview = id(new PHUITimelineView()) ->setID($this->getPreviewTimelineID()); return phutil_tag( 'div', array( 'id' => $this->getPreviewPanelID(), 'style' => 'display: none', 'class' => 'phui-comment-preview-view', ), $preview); } private function getPreviewPanelID() { if (!$this->previewPanelID) { $this->previewPanelID = celerity_generate_unique_node_id(); } return $this->previewPanelID; } private function getPreviewTimelineID() { if (!$this->previewTimelineID) { $this->previewTimelineID = celerity_generate_unique_node_id(); } return $this->previewTimelineID; } public function setFormID($id) { $this->formID = $id; return $this; } private function getFormID() { if (!$this->formID) { $this->formID = celerity_generate_unique_node_id(); } return $this->formID; } private function getStatusID() { if (!$this->statusID) { $this->statusID = celerity_generate_unique_node_id(); } return $this->statusID; } private function getCommentID() { if (!$this->commentID) { $this->commentID = celerity_generate_unique_node_id(); } return $this->commentID; } private function newCommentActionOptions(array $action_map) { $options = array(); $options['+'] = pht('Add Action...'); // Merge options into groups. $groups = array(); foreach ($action_map as $key => $item) { $group_key = $item['groupKey']; if (!isset($groups[$group_key])) { $groups[$group_key] = array(); } $groups[$group_key][$key] = $item; } $group_specs = $this->getCommentActionGroups(); $group_labels = mpull($group_specs, 'getLabel', 'getKey'); // Reorder groups to put them in the same order as the recognized // group definitions. $groups = array_select_keys($groups, array_keys($group_labels)) + $groups; // Move options with no group to the end. $default_group = idx($groups, ''); if ($default_group) { unset($groups['']); $groups[''] = $default_group; } foreach ($groups as $group_key => $group_items) { if (strlen($group_key)) { $group_label = idx($group_labels, $group_key, $group_key); $options[$group_label] = ipull($group_items, 'label'); } else { foreach ($group_items as $key => $item) { $options[$key] = $item['label']; } } } return $options; } }