diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 1dffc8ff1b..5593053e22 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1,979 +1,978 @@ allowsAnonymousAccess(); } public function willProcessRequest(array $data) { $this->revisionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $viewer_is_anonymous = !$user->isLoggedIn(); $revision = id(new DifferentialRevision())->load($this->revisionID); if (!$revision) { return new Aphront404Response(); } $revision->loadRelationships(); $diffs = $revision->loadDiffs(); if (!$diffs) { throw new Exception( "This revision has no diffs. Something has gone quite wrong."); } $diff_vs = $request->getInt('vs'); $target_id = $request->getInt('id'); $target = idx($diffs, $target_id, end($diffs)); $target_manual = $target; if (!$target_id) { foreach ($diffs as $diff) { if ($diff->getCreationMethod() != 'commit') { $target_manual = $diff; } } } if (empty($diffs[$diff_vs])) { $diff_vs = null; } $arc_project = $target->loadArcanistProject(); $repository = ($arc_project ? $arc_project->loadRepository() : null); list($changesets, $vs_map, $vs_changesets, $rendering_references) = $this->loadChangesetsAndVsMap( $target, idx($diffs, $diff_vs), $repository); if ($request->getExists('download')) { return $this->buildRawDiffResponse( $changesets, $vs_changesets, $vs_map, $repository); } $props = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $target_manual->getID()); $props = mpull($props, 'getData', 'getName'); $aux_fields = $this->loadAuxiliaryFields($revision); $comments = $revision->loadComments(); $comments = array_merge( $this->getImplicitComments($revision, reset($diffs)), $comments); $all_changesets = $changesets; $inlines = $this->loadInlineComments($comments, $all_changesets); $object_phids = array_merge( $revision->getReviewers(), $revision->getCCPHIDs(), $revision->loadCommitPHIDs(), array( $revision->getAuthorPHID(), $user->getPHID(), ), mpull($comments, 'getAuthorPHID')); foreach ($comments as $comment) { $metadata = $comment->getMetadata(); $added_reviewers = idx( $metadata, DifferentialComment::METADATA_ADDED_REVIEWERS); if ($added_reviewers) { foreach ($added_reviewers as $phid) { $object_phids[] = $phid; } } $added_ccs = idx( $metadata, DifferentialComment::METADATA_ADDED_CCS); if ($added_ccs) { foreach ($added_ccs as $phid) { $object_phids[] = $phid; } } } foreach ($revision->getAttached() as $type => $phids) { foreach ($phids as $phid => $info) { $object_phids[] = $phid; } } $aux_phids = array(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setDiff($target); $aux_field->setManualDiff($target_manual); $aux_field->setDiffProperties($props); $aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionView(); } $object_phids = array_merge($object_phids, array_mergev($aux_phids)); $object_phids = array_unique($object_phids); $handles = $this->loadViewerHandles($object_phids); foreach ($aux_fields as $key => $aux_field) { // Make sure each field only has access to handles it specifically // requested, not all handles. Otherwise you can get a field which works // only in the presence of other fields. $aux_field->setHandles(array_select_keys($handles, $aux_phids[$key])); } $reviewer_warning = null; if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { $has_live_reviewer = false; foreach ($revision->getReviewers() as $reviewer) { if (!$handles[$reviewer]->isDisabled()) { $has_live_reviewer = true; break; } } if (!$has_live_reviewer) { $reviewer_warning = new AphrontErrorView(); $reviewer_warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $reviewer_warning->setTitle(pht('No Active Reviewers')); if ($revision->getReviewers()) { $reviewer_warning->appendChild( phutil_render_tag( 'p', array(), pht('All specified reviewers are disabled and this revision '. 'needs review. You may want to add some new reviewers.') )); } else { $reviewer_warning->appendChild( phutil_render_tag( 'p', array(), pht('This revision has no specified reviewers and needs '. 'review. You may want to add some reviewers.') )); } } } $request_uri = $request->getRequestURI(); $limit = 100; $large = $request->getStr('large'); if (count($changesets) > $limit && !$large) { $count = count($changesets); $warning = new AphrontErrorView(); $warning->setTitle('Very Large Diff'); $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $warning->appendChild( pht( - 'This diff is very large and affects %2$s files. Load each file '. + 'This diff is very large and affects %s files. Load each file '. 'individually.', - $count, - PhutilTranslator::getInstance()->formatNumber($count)). + new PhutilNumber($count)). " ". phutil_render_tag( 'a', array( 'href' => $request_uri ->alter('large', 'true') ->setFragment('toc'), ), pht('Show All Files Inline')). ""); $warning = $warning->render(); $my_inlines = id(new DifferentialInlineComment())->loadAllWhere( 'revisionID = %d AND commentID IS NULL AND authorPHID = %s AND '. 'changesetID IN (%Ld)', $this->revisionID, $user->getPHID(), mpull($changesets, 'getID')); $visible_changesets = array(); foreach ($inlines + $my_inlines as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { $visible_changesets[$changeset_id] = $changesets[$changeset_id]; } } if (!empty($props['arc:lint'])) { $changeset_paths = mpull($changesets, null, 'getFilename'); foreach ($props['arc:lint'] as $lint) { $changeset = idx($changeset_paths, $lint['path']); if ($changeset) { $visible_changesets[$changeset->getID()] = $changeset; } } } } else { $warning = null; $visible_changesets = $changesets; } $revision_detail = new DifferentialRevisionDetailView(); $revision_detail->setRevision($revision); $revision_detail->setDiff(reset($diffs)); $revision_detail->setAuxiliaryFields($aux_fields); $actions = $this->getRevisionActions($revision); $custom_renderer_class = PhabricatorEnv::getEnvConfig( 'differential.revision-custom-detail-renderer'); if ($custom_renderer_class) { // TODO: build a better version of the action links and deprecate the // whole DifferentialRevisionDetailRenderer class. $custom_renderer = newv($custom_renderer_class, array()); $custom_renderer->setDiff($target); if ($diff_vs) { $custom_renderer->setVSDiff($diffs[$diff_vs]); } $actions = array_merge( $actions, $custom_renderer->generateActionLinks($revision, $target_manual)); } $whitespace = $request->getStr( 'whitespace', DifferentialChangesetParser::WHITESPACE_IGNORE_ALL); if ($arc_project) { list($symbol_indexes, $project_phids) = $this->buildSymbolIndexes( $arc_project, $visible_changesets); } else { $symbol_indexes = array(); $project_phids = null; } $revision_detail->setActions($actions); $revision_detail->setUser($user); $comment_view = new DifferentialRevisionCommentListView(); $comment_view->setComments($comments); $comment_view->setHandles($handles); $comment_view->setInlineComments($inlines); $comment_view->setChangesets($all_changesets); $comment_view->setUser($user); $comment_view->setTargetDiff($target); $comment_view->setVersusDiffID($diff_vs); if ($arc_project) { Javelin::initBehavior( 'repository-crossreference', array( 'section' => $comment_view->getID(), 'projects' => $project_phids, )); } $changeset_view = new DifferentialChangesetListView(); $changeset_view->setChangesets($changesets); $changeset_view->setVisibleChangesets($visible_changesets); if (!$viewer_is_anonymous) { $changeset_view->setInlineCommentControllerURI( '/differential/comment/inline/edit/'.$revision->getID().'/'); } $changeset_view->setStandaloneURI('/differential/changeset/'); $changeset_view->setRawFileURIs( '/differential/changeset/?view=old', '/differential/changeset/?view=new'); $changeset_view->setUser($user); $changeset_view->setDiff($target); $changeset_view->setRenderingReferences($rendering_references); $changeset_view->setVsMap($vs_map); $changeset_view->setWhitespace($whitespace); if ($repository) { $changeset_view->setRepository($repository); } $changeset_view->setSymbolIndexes($symbol_indexes); $changeset_view->setTitle('Diff '.$target->getID()); $diff_history = new DifferentialRevisionUpdateHistoryView(); $diff_history->setDiffs($diffs); $diff_history->setSelectedVersusDiffID($diff_vs); $diff_history->setSelectedDiffID($target->getID()); $diff_history->setSelectedWhitespace($whitespace); $diff_history->setUser($user); $local_view = new DifferentialLocalCommitsView(); $local_view->setUser($user); $local_view->setLocalCommits(idx($props, 'local:commits')); if ($repository) { $other_revisions = $this->loadOtherRevisions( $changesets, $target, $repository); } else { $other_revisions = array(); } $other_view = null; if ($other_revisions) { $other_view = $this->renderOtherRevisions($other_revisions); } $toc_view = new DifferentialDiffTableOfContentsView(); $toc_view->setChangesets($changesets); $toc_view->setVisibleChangesets($visible_changesets); $toc_view->setRenderingReferences($rendering_references); $toc_view->setUnitTestData(idx($props, 'arc:unit', array())); if ($repository) { $toc_view->setRepository($repository); } $toc_view->setDiff($target); $toc_view->setUser($user); $toc_view->setRevisionID($revision->getID()); $toc_view->setWhitespace($whitespace); $comment_form = null; if (!$viewer_is_anonymous) { $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'differential-comment-'.$revision->getID()); $reviewers = array(); $ccs = array(); if ($draft) { $reviewers = idx($draft->getMetadata(), 'reviewers', array()); $ccs = idx($draft->getMetadata(), 'ccs', array()); if ($reviewers || $ccs) { $handles = $this->loadViewerHandles(array_merge($reviewers, $ccs)); $reviewers = array_select_keys($handles, $reviewers); $ccs = array_select_keys($handles, $ccs); } } $comment_form = new DifferentialAddCommentView(); $comment_form->setRevision($revision); $comment_form->setAuxFields($aux_fields); $comment_form->setActions($this->getRevisionCommentActions($revision)); $comment_form->setActionURI('/differential/comment/save/'); $comment_form->setUser($user); $comment_form->setDraft($draft); $comment_form->setReviewers(mpull($reviewers, 'getFullName', 'getPHID')); $comment_form->setCCs(mpull($ccs, 'getFullName', 'getPHID')); } $pane_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'differential-keyboard-navigation', array( 'haunt' => $pane_id, )); Javelin::initBehavior('differential-user-select'); $page_pane = id(new DifferentialPrimaryPaneView()) ->setID($pane_id) ->appendChild( $comment_view->render(). $diff_history->render(). $warning. $local_view->render(). $toc_view->render(). $other_view. $changeset_view->render()); if ($comment_form) { $page_pane->appendChild($comment_form->render()); } PhabricatorFeedStoryNotification::updateObjectNotificationViews( $user, $revision->getPHID()); $object_id = 'D'.$revision->getID(); $top_anchor = id(new PhabricatorAnchorView()) ->setAnchorName('top') ->setNavigationMarker(true); $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setAnchorName('top') ->setTitle('D'.$revision->getID()) ->setBaseURI(new PhutilURI('/D'.$revision->getID())) ->build($changesets); $nav->appendChild( array( $reviewer_warning, $top_anchor, $revision_detail, $page_pane, )); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($object_id) ->setHref('/'.$object_id)); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $object_id.' '.$revision->getTitle(), )); } private function getImplicitComments( DifferentialRevision $revision, DifferentialDiff $diff) { $author_phid = nonempty( $diff->getAuthorPHID(), $revision->getAuthorPHID()); $template = new DifferentialComment(); $template->setAuthorPHID($author_phid); $template->setRevisionID($revision->getID()); $template->setDateCreated($revision->getDateCreated()); $comments = array(); if (strlen($revision->getSummary())) { $summary_comment = clone $template; $summary_comment->setContent($revision->getSummary()); $summary_comment->setAction(DifferentialAction::ACTION_SUMMARIZE); $comments[] = $summary_comment; } if (strlen($revision->getTestPlan())) { $testplan_comment = clone $template; $testplan_comment->setContent($revision->getTestPlan()); $testplan_comment->setAction(DifferentialAction::ACTION_TESTPLAN); $comments[] = $testplan_comment; } return $comments; } private function getRevisionActions(DifferentialRevision $revision) { $user = $this->getRequest()->getUser(); $viewer_phid = $user->getPHID(); $viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs()); $viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn(); $status = $revision->getStatus(); $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); $links = array(); if ($viewer_is_owner) { $links[] = array( 'icon' => 'edit', 'href' => "/differential/revision/edit/{$revision_id}/", 'name' => pht('Edit Revision'), ); } if (!$viewer_is_anonymous) { if (!$viewer_is_owner && !$viewer_is_reviewer) { $action = $viewer_is_cc ? 'rem' : 'add'; $links[] = array( 'icon' => $viewer_is_cc ? 'subscribe-delete' : 'subscribe-add', 'href' => "/differential/subscribe/{$action}/{$revision_id}/", 'name' => $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe'), 'instant' => true, 'sigil' => 'workflow', ); } else { $links[] = array( 'icon' => 'subscribe-auto', 'name' => pht('Automatically Subscribed'), 'disabled' => true, ); } require_celerity_resource('phabricator-object-selector-css'); require_celerity_resource('javelin-behavior-phabricator-object-selector'); $links[] = array( 'icon' => 'link', 'name' => pht('Edit Dependencies'), 'href' => "/search/attach/{$revision_phid}/DREV/dependencies/", 'sigil' => 'workflow', ); if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) { $links[] = array( 'icon' => 'attach', 'name' => pht('Edit Maniphest Tasks'), 'href' => "/search/attach/{$revision_phid}/TASK/", 'sigil' => 'workflow', ); } if ($user->getIsAdmin()) { $links[] = array( 'icon' => 'file', 'name' => pht('MetaMTA Transcripts'), 'href' => "/mail/?phid={$revision_phid}", ); } $links[] = array( 'icon' => 'file', 'name' => pht('Herald Transcripts'), 'href' => "/herald/transcript/?phid={$revision_phid}", ); } $request_uri = $this->getRequest()->getRequestURI(); $links[] = array( 'icon' => 'download', 'name' => pht('Download Raw Diff'), 'href' => $request_uri->alter('download', 'true') ); return $links; } private function getRevisionCommentActions(DifferentialRevision $revision) { $actions = array( DifferentialAction::ACTION_COMMENT => true, ); $viewer = $this->getRequest()->getUser(); $viewer_phid = $viewer->getPHID(); $viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID()); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); $viewer_did_accept = ($viewer_phid === $revision->loadReviewedBy()); $status = $revision->getStatus(); $allow_self_accept = PhabricatorEnv::getEnvConfig( 'differential.allow-self-accept'); $always_allow_close = PhabricatorEnv::getEnvConfig( 'differential.always-allow-close'); $allow_reopen = PhabricatorEnv::getEnvConfig( 'differential.allow-reopen'); if ($viewer_is_owner) { switch ($status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept; $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_RETHINK] = true; break; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept; $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_REQUEST] = true; break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_REQUEST] = true; $actions[DifferentialAction::ACTION_RETHINK] = true; $actions[DifferentialAction::ACTION_CLOSE] = true; break; case ArcanistDifferentialRevisionStatus::CLOSED: break; case ArcanistDifferentialRevisionStatus::ABANDONED: $actions[DifferentialAction::ACTION_RECLAIM] = true; break; } } else { switch ($status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_REJECT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_REJECT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer && !$viewer_did_accept; break; case ArcanistDifferentialRevisionStatus::CLOSED: case ArcanistDifferentialRevisionStatus::ABANDONED: break; } if ($status != ArcanistDifferentialRevisionStatus::CLOSED) { $actions[DifferentialAction::ACTION_CLAIM] = true; $actions[DifferentialAction::ACTION_CLOSE] = $always_allow_close; } } $actions[DifferentialAction::ACTION_ADDREVIEWERS] = true; $actions[DifferentialAction::ACTION_ADDCCS] = true; $actions[DifferentialAction::ACTION_REOPEN] = $allow_reopen && ($status == ArcanistDifferentialRevisionStatus::CLOSED); $actions = array_keys(array_filter($actions)); $actions_dict = array(); foreach ($actions as $action) { $actions_dict[$action] = DifferentialAction::getActionVerb($action); } return $actions_dict; } private function loadInlineComments(array $comments, array &$changesets) { assert_instances_of($comments, 'DifferentialComment'); assert_instances_of($changesets, 'DifferentialChangeset'); $inline_comments = array(); $comment_ids = array_filter(mpull($comments, 'getID')); if (!$comment_ids) { return $inline_comments; } $inline_comments = id(new DifferentialInlineComment()) ->loadAllWhere( 'commentID in (%Ld)', $comment_ids); $load_changesets = array(); foreach ($inline_comments as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { continue; } $load_changesets[$changeset_id] = true; } $more_changesets = array(); if ($load_changesets) { $changeset_ids = array_keys($load_changesets); $more_changesets += id(new DifferentialChangeset()) ->loadAllWhere( 'id IN (%Ld)', $changeset_ids); } if ($more_changesets) { $changesets += $more_changesets; $changesets = msort($changesets, 'getSortKey'); } return $inline_comments; } private function loadChangesetsAndVsMap( DifferentialDiff $target, DifferentialDiff $diff_vs = null, PhabricatorRepository $repository = null) { $load_ids = array(); if ($diff_vs) { $load_ids[] = $diff_vs->getID(); } $load_ids[] = $target->getID(); $raw_changesets = id(new DifferentialChangeset()) ->loadAllWhere( 'diffID IN (%Ld)', $load_ids); $changeset_groups = mgroup($raw_changesets, 'getDiffID'); $changesets = idx($changeset_groups, $target->getID(), array()); $changesets = mpull($changesets, null, 'getID'); $refs = array(); $vs_map = array(); $vs_changesets = array(); if ($diff_vs) { $vs_id = $diff_vs->getID(); $vs_changesets_path_map = array(); foreach (idx($changeset_groups, $vs_id, array()) as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs); $vs_changesets_path_map[$path] = $changeset; $vs_changesets[$changeset->getID()] = $changeset; } foreach ($changesets as $key => $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $target); if (isset($vs_changesets_path_map[$path])) { $vs_map[$changeset->getID()] = $vs_changesets_path_map[$path]->getID(); $refs[$changeset->getID()] = $changeset->getID().'/'.$vs_changesets_path_map[$path]->getID(); unset($vs_changesets_path_map[$path]); } else { $refs[$changeset->getID()] = $changeset->getID(); } } foreach ($vs_changesets_path_map as $path => $changeset) { $changesets[$changeset->getID()] = $changeset; $vs_map[$changeset->getID()] = -1; $refs[$changeset->getID()] = $changeset->getID().'/-1'; } } else { foreach ($changesets as $changeset) { $refs[$changeset->getID()] = $changeset->getID(); } } $changesets = msort($changesets, 'getSortKey'); return array($changesets, $vs_map, $vs_changesets, $refs); } private function loadAuxiliaryFields(DifferentialRevision $revision) { $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { if (!$aux_field->shouldAppearOnRevisionView()) { unset($aux_fields[$key]); } else { $aux_field->setUser($this->getRequest()->getUser()); } } $aux_fields = DifferentialAuxiliaryField::loadFromStorage( $revision, $aux_fields); return $aux_fields; } private function buildSymbolIndexes( PhabricatorRepositoryArcanistProject $arc_project, array $visible_changesets) { assert_instances_of($visible_changesets, 'DifferentialChangeset'); $engine = PhabricatorSyntaxHighlighter::newEngine(); $langs = $arc_project->getSymbolIndexLanguages(); if (!$langs) { return array(array(), array()); } $symbol_indexes = array(); $project_phids = array_merge( array($arc_project->getPHID()), nonempty($arc_project->getSymbolIndexProjects(), array())); $indexed_langs = array_fill_keys($langs, true); foreach ($visible_changesets as $key => $changeset) { $lang = $engine->getLanguageFromFilename($changeset->getFilename()); if (isset($indexed_langs[$lang])) { $symbol_indexes[$key] = array( 'lang' => $lang, 'projects' => $project_phids, ); } } return array($symbol_indexes, $project_phids); } private function loadOtherRevisions( array $changesets, DifferentialDiff $target, PhabricatorRepository $repository) { assert_instances_of($changesets, 'DifferentialChangeset'); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $changeset->getAbsoluteRepositoryPath( $repository, $target); } if (!$paths) { return array(); } $path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs(); if (!$path_map) { return array(); } $query = id(new DifferentialRevisionQuery()) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->setOrder(DifferentialRevisionQuery::ORDER_PATH_MODIFIED) ->setLimit(10) ->needRelationships(true); foreach ($path_map as $path => $path_id) { $query->withPath($repository->getID(), $path_id); } $results = $query->execute(); // Strip out *this* revision. foreach ($results as $key => $result) { if ($result->getID() == $this->revisionID) { unset($results[$key]); } } return $results; } private function renderOtherRevisions(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $view = id(new DifferentialRevisionListView()) ->setRevisions($revisions) ->setFields(DifferentialRevisionListView::getDefaultFields()) ->setUser($this->getRequest()->getUser()) ->loadAssets(); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return id(new PhabricatorHeaderView()) ->setHeader(pht('Open Revisions Affecting These Files')) ->render(). '
'. $view->render(). '
'; } /** * Straight copy of the loadFileByPhid method in * @{class:DifferentialReviewRequestMail}. * * This is because of the code similarity between the buildPatch method in * @{class:DifferentialReviewRequestMail} and @{method:buildRawDiffResponse} * in this class. Both of these methods end up using call_user_func and this * piece of code is the lucky function. * * @return mixed (@{class:PhabricatorFile} if found, null if not) */ public function loadFileByPHID($phid) { $file = id(new PhabricatorFile())->loadOneWhere( 'phid = %s', $phid); if (!$file) { return null; } return $file->loadFileData(); } /** * Note this code is somewhat similar to the buildPatch method in * @{class:DifferentialReviewRequestMail}. * * @return @{class:AphrontRedirectResponse} */ private function buildRawDiffResponse( array $changesets, array $vs_changesets, array $vs_map, PhabricatorRepository $repository = null) { assert_instances_of($changesets, 'DifferentialChangeset'); assert_instances_of($vs_changesets, 'DifferentialChangeset'); $engine = new PhabricatorDifferenceEngine(); $generated_changesets = array(); foreach ($changesets as $changeset) { $changeset->attachHunks($changeset->loadHunks()); $right = $changeset->makeNewFile(); $choice = $changeset; $vs = idx($vs_map, $changeset->getID()); if ($vs == -1) { $left = $right; $right = $changeset->makeOldFile(); } else if ($vs) { $choice = $vs_changeset = $vs_changesets[$vs]; $vs_changeset->attachHunks($vs_changeset->loadHunks()); $left = $vs_changeset->makeNewFile(); } else { $left = $changeset->makeOldFile(); } $synthetic = $engine->generateChangesetFromFileContent( $left, $right); if (!$synthetic->getAffectedLineCount()) { $filetype = $choice->getFileType(); if ($filetype == DifferentialChangeType::FILE_TEXT || $filetype == DifferentialChangeType::FILE_SYMLINK) { continue; } } $choice->attachHunks($synthetic->getHunks()); $generated_changesets[] = $choice; } $diff = new DifferentialDiff(); $diff->attachChangesets($generated_changesets); $diff_dict = $diff->getDiffDict(); $changes = array(); foreach ($diff_dict['changes'] as $changedict) { $changes[] = ArcanistDiffChange::newFromDictionary($changedict); } $bundle = ArcanistBundle::newFromChanges($changes); $bundle->setLoadFileDataCallback(array($this, 'loadFileByPHID')); $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, )); return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } } diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php index e3f5592426..ac1e0abb80 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php @@ -1,948 +1,947 @@ getRequest(); $drequest = $this->getDiffusionRequest(); $before = $request->getStr('before'); if ($before) { return $this->buildBeforeResponse($before); } $path = $drequest->getPath(); $selected = $request->getStr('view'); $preferences = $request->getUser()->loadPreferences(); if (!$selected) { $selected = $preferences->getPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_VIEW, 'highlighted'); } else if ($request->isFormPost() && $selected != 'raw') { $preferences->setPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_VIEW, $selected); $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()->alter('view', $selected)); } $needs_blame = ($selected == 'blame' || $selected == 'plainblame'); $file_query = DiffusionFileContentQuery::newFromDiffusionRequest( $this->diffusionRequest); $file_query->setViewer($request->getUser()); $file_query->setNeedsBlame($needs_blame); $file_query->loadFileContent(); $data = $file_query->getRawData(); if ($selected === 'raw') { return $this->buildRawResponse($path, $data); } $this->loadLintMessages(); // Build the content of the file. $corpus = $this->buildCorpus( $selected, $file_query, $needs_blame, $drequest, $path, $data); require_celerity_resource('diffusion-source-css'); if ($this->corpusType == 'text') { $view_select_panel = $this->renderViewSelectPanel($selected); } else { $view_select_panel = null; } // Render the page. $content = array(); $follow = $request->getStr('follow'); if ($follow) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_WARNING); $notice->setTitle('Unable to Continue'); switch ($follow) { case 'first': $notice->appendChild( "Unable to continue tracing the history of this file because ". "this commit is the first commit in the repository."); break; case 'created': $notice->appendChild( "Unable to continue tracing the history of this file because ". "this commit created the file."); break; } $content[] = $notice; } $renamed = $request->getStr('renamed'); if ($renamed) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle('File Renamed'); $notice->appendChild( "File history passes through a rename from '". phutil_escape_html($drequest->getPath())."' to '". phutil_escape_html($renamed)."'."); $content[] = $notice; } $content[] = $view_select_panel; $content[] = $corpus; $content[] = $this->buildOpenRevisions(); $nav = $this->buildSideNav('browse', true); $nav->appendChild($content); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $nav->setCrumbs($crumbs); $basename = basename($this->getDiffusionRequest()->getPath()); return $this->buildApplicationPage( $nav, array( 'title' => $basename, )); } private function loadLintMessages() { $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); if (!$branch || !$branch->getLintCommit()) { return; } $this->lintCommit = $branch->getLintCommit(); $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = ''; if ($drequest->getLint()) { $where = qsprintf( $conn, 'AND code = %s', $drequest->getLint()); } $this->lintMessages = queryfx_all( $conn, 'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s', PhabricatorRepository::TABLE_LINTMESSAGE, $branch->getID(), $where, '/'.$drequest->getPath()); } private function buildCorpus($selected, DiffusionFileContentQuery $file_query, $needs_blame, DiffusionRequest $drequest, $path, $data) { if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) { $file = $this->loadFileForData($path, $data); $file_uri = $file->getBestURI(); if ($file->isViewableImage()) { $this->corpusType = 'image'; return $this->buildImageCorpus($file_uri); } else { $this->corpusType = 'binary'; return $this->buildBinaryCorpus($file_uri, $data); } } switch ($selected) { case 'plain': $style = "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; $corpus = phutil_render_tag( 'textarea', array( 'style' => $style, ), phutil_escape_html($file_query->getRawData())); break; case 'plainblame': $style = "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData(); $rows = array(); foreach ($text_list as $k => $line) { $rev = $rev_list[$k]; if (isset($blame_dict[$rev]['handle'])) { $author = $blame_dict[$rev]['handle']->getName(); } else { $author = $blame_dict[$rev]['author']; } $rows[] = sprintf("%-10s %-20s %s", substr($rev, 0, 7), $author, $line); } $corpus = phutil_render_tag( 'textarea', array( 'style' => $style, ), phutil_escape_html(implode("\n", $rows))); break; case 'highlighted': case 'blame': default: require_celerity_resource('syntax-highlighting-css'); list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData(); $text_list = implode("\n", $text_list); $text_list = PhabricatorSyntaxHighlighter::highlightWithFilename( $path, $text_list); $text_list = explode("\n", $text_list); $rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict, $needs_blame, $drequest, $file_query, $selected); $id = celerity_generate_unique_node_id(); $projects = $drequest->loadArcanistProjects(); $langs = array(); foreach ($projects as $project) { $ls = $project->getSymbolIndexLanguages(); if (!$ls) { continue; } $dep_projects = $project->getSymbolIndexProjects(); $dep_projects[] = $project->getPHID(); foreach ($ls as $lang) { if (!isset($langs[$lang])) { $langs[$lang] = array(); } $langs[$lang] += $dep_projects + array($project); } } $lang = last(explode('.', $drequest->getPath())); $prefs = $this->getRequest()->getUser()->loadPreferences(); $pref_symbols = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_SYMBOLS); if (isset($langs[$lang]) && $pref_symbols != 'disabled') { Javelin::initBehavior( 'repository-crossreference', array( 'container' => $id, 'lang' => $lang, 'projects' => $langs[$lang], )); } $corpus_table = javelin_render_tag( 'table', array( 'class' => "diffusion-source remarkup-code PhabricatorMonospaced", 'sigil' => 'diffusion-source', ), implode("\n", $rows)); $corpus = phutil_render_tag( 'div', array( 'style' => 'padding: 0 2em;', 'id' => $id, ), $corpus_table); break; } return $corpus; } private function renderViewSelectPanel($selected) { $toggle_blame = array( 'highlighted' => 'blame', 'blame' => 'highlighted', 'plain' => 'plainblame', 'plainblame' => 'plain', 'raw' => 'raw', // not a real case. ); $toggle_highlight = array( 'highlighted' => 'plain', 'blame' => 'plainblame', 'plain' => 'highlighted', 'plainblame' => 'blame', 'raw' => 'raw', // not a real case. ); $user = $this->getRequest()->getUser(); $base_uri = $this->getRequest()->getRequestURI(); $blame_on = ($selected == 'blame' || $selected == 'plainblame'); if ($blame_on) { $blame_text = pht('Disable Blame'); } else { $blame_text = pht('Enable Blame'); } $blame_button = $this->createViewAction( $blame_text, $base_uri->alter('view', $toggle_blame[$selected]), $user); $highlight_on = ($selected == 'blame' || $selected == 'highlighted'); if ($highlight_on) { $highlight_text = pht('Disable Highlighting'); } else { $highlight_text = pht('Enable Highlighting'); } $highlight_button = $this->createViewAction( $highlight_text, $base_uri->alter('view', $toggle_highlight[$selected]), $user); $href = null; if ($this->getRequest()->getStr('lint') !== null) { $lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages)); $href = $base_uri->alter('lint', null); } else if ($this->lintCommit === null) { $lint_text = pht('Lint not Available'); } else { $lint_text = pht( 'Show %d Lint Message(s)', count($this->lintMessages)); $href = $this->getDiffusionRequest()->generateURI(array( 'action' => 'browse', 'commit' => $this->lintCommit, ))->alter('lint', ''); } $lint_button = $this->createViewAction( $lint_text, $href, $user); if (!$href) { $lint_button->setDisabled(true); } $raw_button = $this->createViewAction( pht('View Raw File'), $base_uri->alter('view', 'raw'), $user, 'file'); $edit_button = $this->createEditAction(); return id(new PhabricatorActionListView()) ->setUser($user) ->addAction($blame_button) ->addAction($highlight_button) ->addAction($lint_button) ->addAction($raw_button) ->addAction($edit_button); } private function createViewAction( $localized_text, $href, $user, $icon = null) { return id(new PhabricatorActionView()) ->setName($localized_text) ->setIcon($icon) ->setUser($user) ->setRenderAsForm(true) ->setHref($href); } private function createEditAction() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $line = nonempty((int)$drequest->getLine(), 1); $callsign = $repository->getCallsign(); $editor_link = $user->loadEditorLink($path, $line, $callsign); $action = id(new PhabricatorActionView()) ->setName(pht('Open in Editor')) ->setIcon('edit'); $action->setHref($editor_link); $action->setDisabled(!$editor_link); return $action; } private function buildDisplayRows( array $text_list, array $rev_list, array $blame_dict, $needs_blame, DiffusionRequest $drequest, DiffusionFileContentQuery $file_query, $selected) { if ($blame_dict) { $epoch_list = ipull(ifilter($blame_dict, 'epoch'), 'epoch'); $epoch_min = min($epoch_list); $epoch_max = max($epoch_list); $epoch_range = ($epoch_max - $epoch_min) + 1; } $line_arr = array(); $line_str = $drequest->getLine(); $ranges = explode(',', $line_str); foreach ($ranges as $range) { if (strpos($range, '-') !== false) { list($min, $max) = explode('-', $range, 2); $line_arr[] = array( 'min' => min($min, $max), 'max' => max($min, $max), ); } else if (strlen($range)) { $line_arr[] = array( 'min' => $range, 'max' => $range, ); } } $display = array(); $line_number = 1; $last_rev = null; $color = null; foreach ($text_list as $k => $line) { $display_line = array( 'color' => null, 'epoch' => null, 'commit' => null, 'author' => null, 'target' => null, 'highlighted' => null, 'line' => $line_number, 'data' => $line, ); if ($needs_blame) { // If the line's rev is same as the line above, show empty content // with same color; otherwise generate blame info. The newer a change // is, the more saturated the color. $rev = idx($rev_list, $k, $last_rev); if ($last_rev == $rev) { $display_line['color'] = $color; } else { $blame = $blame_dict[$rev]; if (!isset($blame['epoch'])) { $color = '#ffd'; // Render as warning. } else { $color_ratio = ($blame['epoch'] - $epoch_min) / $epoch_range; $color_value = 0xF6 * (1.0 - $color_ratio); $color = sprintf( '#%02x%02x%02x', $color_value, 0xF6, $color_value); } $display_line['epoch'] = idx($blame, 'epoch'); $display_line['color'] = $color; $display_line['commit'] = $rev; if (isset($blame['handle'])) { $author_link = $blame['handle']->renderLink(); } else { $author_link = phutil_render_tag( 'span', array( ), phutil_escape_html($blame['author'])); } $display_line['author'] = $author_link; $last_rev = $rev; } } if ($line_arr) { if ($line_number == $line_arr[0]['min']) { $display_line['target'] = true; } foreach ($line_arr as $range) { if ($line_number >= $range['min'] && $line_number <= $range['max']) { $display_line['highlighted'] = true; } } } $display[] = $display_line; ++$line_number; } $commits = array_filter(ipull($display, 'commit')); if ($commits) { $commits = id(new PhabricatorAuditCommitQuery()) ->withIdentifiers($drequest->getRepository()->getID(), $commits) ->needCommitData(true) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); } $revision_ids = id(new DifferentialRevision()) ->loadIDsByCommitPHIDs(mpull($commits, 'getPHID')); $revisions = array(); if ($revision_ids) { $revisions = id(new DifferentialRevision())->loadAllWhere( 'id IN (%Ld)', $revision_ids); } $request = $this->getRequest(); $user = $request->getUser(); Javelin::initBehavior('phabricator-oncopy', array()); $engine = null; $inlines = array(); if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) { $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); foreach ($this->lintMessages as $message) { $inline = id(new PhabricatorAuditInlineComment()) ->setID($message['id']) ->setSyntheticAuthor( ArcanistLintSeverity::getStringForSeverity($message['severity']). ' '.$message['code'].' ('.$message['name'].')') ->setLineNumber($message['line']) ->setContent($message['description']); $inlines[$message['line']][] = $inline; $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); require_celerity_resource('differential-changeset-view-css'); } $rows = $this->renderInlines( idx($inlines, 0, array()), $needs_blame, $engine); foreach ($display as $line) { $line_href = $drequest->generateURI( array( 'action' => 'browse', 'line' => $line['line'], 'stable' => true, )); $blame = array(); if ($line['color']) { $color = $line['color']; $before_link = null; $commit_link = null; $revision_link = null; if (idx($line, 'commit')) { $commit = $line['commit']; $summary = 'Unknown'; if (idx($commits, $commit)) { $summary = $commits[$commit]->getCommitData()->getSummary(); } $tooltip = phabricator_date( $line['epoch'], $user)." \xC2\xB7 ".$summary; Javelin::initBehavior('phabricator-tooltips', array()); require_celerity_resource('aphront-tooltip-css'); $commit_link = javelin_render_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $line['commit'], )), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), phutil_escape_html(phutil_utf8_shorten($line['commit'], 9, ''))); $revision_id = null; if (idx($commits, $commit)) { $revision_id = idx($revision_ids, $commits[$commit]->getPHID()); } if ($revision_id) { $revision = idx($revisions, $revision_id); if (!$revision) { $tooltip = '(Invalid revision)'; } else { $tooltip = phabricator_date($revision->getDateModified(), $user). " \xC2\xB7 ". $revision->getTitle(); } $revision_link = javelin_render_tag( 'a', array( 'href' => '/D'.$revision_id, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), 'D'.$revision_id); } $uri = $line_href->alter('before', $commit); $before_link = javelin_render_tag( 'a', array( 'href' => $uri->setQueryParam('view', 'blame'), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => 'Skip Past This Commit', 'align' => 'E', 'size' => 300, ), ), "\xC2\xAB"); } $blame[] = phutil_render_tag( 'th', array( 'class' => 'diffusion-blame-link', 'style' => 'background: '.$color, ), $before_link); $blame[] = phutil_render_tag( 'th', array( 'class' => 'diffusion-rev-link', 'style' => 'background: '.$color, ), $commit_link); $blame[] = phutil_render_tag( 'th', array( 'class' => 'diffusion-rev-link', 'style' => 'background: '.$color, ), $revision_link); $blame[] = phutil_render_tag( 'th', array( 'class' => 'diffusion-author-link', 'style' => 'background: '.$color, ), idx($line, 'author')); } $line_link = phutil_render_tag( 'a', array( 'href' => $line_href, ), phutil_escape_html($line['line'])); $blame[] = javelin_render_tag( 'th', array( 'class' => 'diffusion-line-link', 'sigil' => 'diffusion-line-link', 'style' => isset($color) ? 'background: '.$color : null, ), $line_link); Javelin::initBehavior('diffusion-line-linker'); $blame = implode('', $blame); if ($line['target']) { Javelin::initBehavior( 'diffusion-jump-to', array( 'target' => 'scroll_target', )); $anchor_text = ''; } else { $anchor_text = null; } $line_text = phutil_render_tag( 'td', array( ), $anchor_text. "\xE2\x80\x8B". // NOTE: See phabricator-oncopy behavior. $line['data']); $rows[] = phutil_render_tag( 'tr', array( 'class' => ($line['highlighted'] ? 'highlighted' : null), ), $blame. $line_text); $rows = array_merge($rows, $this->renderInlines( idx($inlines, $line['line'], array()), $needs_blame, $engine)); } return $rows; } private function renderInlines(array $inlines, $needs_blame, $engine) { $rows = array(); foreach ($inlines as $inline) { $inline_view = id(new DifferentialInlineCommentView()) ->setMarkupEngine($engine) ->setInlineComment($inline) ->render(); $rows[] = ''. str_repeat('', ($needs_blame ? 5 : 1)). ''.$inline_view.''. ''; } return $rows; } private function loadFileForData($path, $data) { return PhabricatorFile::buildFromFileDataOrHash( $data, array( 'name' => basename($path), )); } private function buildRawResponse($path, $data) { $file = $this->loadFileForData($path, $data); return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } private function buildImageCorpus($file_uri) { $properties = new PhabricatorPropertyListView(); $properties->addProperty( pht('Image'), phutil_render_tag( 'img', array( 'src' => $file_uri, ))); $actions = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->addAction($this->createEditAction()); return array($actions, $properties); } private function buildBinaryCorpus($file_uri, $data) { $properties = new PhabricatorPropertyListView(); $size = strlen($data); $properties->addTextContent( - pht('This is a binary file. It is %2$s byte(s) in length.', - $size, - PhutilTranslator::getInstance()->formatNumber($size)) - ); + pht( + 'This is a binary file. It is %s byte(s) in length.', + new PhutilNumber($size))); $actions = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->addAction($this->createEditAction()) ->addAction(id(new PhabricatorActionView()) ->setName(pht('Download Binary File...')) ->setIcon('download') ->setHref($file_uri)); return array($actions, $properties); } private function buildBeforeResponse($before) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); // NOTE: We need to get the grandparent so we can capture filename changes // in the parent. $parent = $this->loadParentRevisionOf($before); $old_filename = null; $was_created = false; if ($parent) { $grandparent = $this->loadParentRevisionOf( $parent->getCommitIdentifier()); if ($grandparent) { $rename_query = new DiffusionRenameHistoryQuery(); $rename_query->setRequest($drequest); $rename_query->setOldCommit($grandparent->getCommitIdentifier()); $old_filename = $rename_query->loadOldFilename(); $was_created = $rename_query->getWasCreated(); } } $follow = null; if ($was_created) { // If the file was created in history, that means older commits won't // have it. Since we know it existed at 'before', it must have been // created then; jump there. $target_commit = $before; $follow = 'created'; } else if ($parent) { // If we found a parent, jump to it. This is the normal case. $target_commit = $parent->getCommitIdentifier(); } else { // If there's no parent, this was probably created in the initial commit? // And the "was_created" check will fail because we can't identify the // grandparent. Keep the user at 'before'. $target_commit = $before; $follow = 'first'; } $path = $drequest->getPath(); $renamed = null; if ($old_filename !== null && $old_filename !== '/'.$path) { $renamed = $path; $path = $old_filename; } $line = null; // If there's a follow error, drop the line so the user sees the message. if (!$follow) { $line = $this->getBeforeLineNumber($target_commit); } $before_uri = $drequest->generateURI( array( 'action' => 'browse', 'commit' => $target_commit, 'line' => $line, 'path' => $path, )); $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); $before_uri = $before_uri->alter('before', null); $before_uri = $before_uri->alter('renamed', $renamed); $before_uri = $before_uri->alter('follow', $follow); return id(new AphrontRedirectResponse())->setURI($before_uri); } private function getBeforeLineNumber($target_commit) { $drequest = $this->getDiffusionRequest(); $line = $drequest->getLine(); if (!$line) { return null; } $diff_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest); $diff_query->setAgainstCommit($target_commit); try { $raw_diff = $diff_query->loadRawDiff(); $old_line = 0; $new_line = 0; foreach (explode("\n", $raw_diff) as $text) { if ($text[0] == '-' || $text[0] == ' ') { $old_line++; } if ($text[0] == '+' || $text[0] == ' ') { $new_line++; } if ($new_line == $line) { return $old_line; } } // We didn't find the target line. return $line; } catch (Exception $ex) { return $line; } } private function loadParentRevisionOf($commit) { $drequest = $this->getDiffusionRequest(); $before_req = DiffusionRequest::newFromDictionary( array( 'repository' => $drequest->getRepository(), 'commit' => $commit, )); $query = DiffusionCommitParentsQuery::newFromDiffusionRequest($before_req); $parents = $query->loadParents(); return head($parents); } } diff --git a/src/applications/paste/controller/PhabricatorPasteListController.php b/src/applications/paste/controller/PhabricatorPasteListController.php index 32194daed1..d6c6b8eb4a 100644 --- a/src/applications/paste/controller/PhabricatorPasteListController.php +++ b/src/applications/paste/controller/PhabricatorPasteListController.php @@ -1,121 +1,120 @@ filter = idx($data, 'filter'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $query = id(new PhabricatorPasteQuery()) ->setViewer($user) ->needContent(true); $nav = $this->buildSideNavView($this->filter); $filter = $nav->getSelectedFilter(); switch ($filter) { case 'my': $query->withAuthorPHIDs(array($user->getPHID())); $title = pht('My Pastes'); $nodata = pht("You haven't created any Pastes yet."); break; case 'all': $title = pht('All Pastes'); $nodata = pht("There are no Pastes yet."); break; } $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); $pastes = $query->executeWithCursorPager($pager); $list = $this->buildPasteList($pastes); $list->setPager($pager); $list->setNoDataString($nodata); $header = id(new PhabricatorHeaderView()) ->setHeader($title); $nav->appendChild( array( $header, $list, )); $crumbs = $this ->buildApplicationCrumbs($nav) ->addCrumb( id(new PhabricatorCrumbView()) ->setName($title) ->setHref($this->getApplicationURI('filter/'.$filter.'/'))); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, ) ); } private function buildPasteList(array $pastes) { assert_instances_of($pastes, 'PhabricatorPaste'); $user = $this->getRequest()->getUser(); $this->loadHandles(mpull($pastes, 'getAuthorPHID')); $lang_map = PhabricatorEnv::getEnvConfig('pygments.dropdown-choices'); $list = new PhabricatorObjectItemListView(); $list->setUser($user); foreach ($pastes as $paste) { $created = phabricator_date($paste->getDateCreated(), $user); $author = $this->getHandle($paste->getAuthorPHID())->renderLink(); $source_code = $this->buildSourceCodeView($paste, 5)->render(); $source_code = phutil_render_tag( 'div', array( 'class' => 'phabricator-source-code-summary', ), $source_code); $line_count = count(explode("\n", $paste->getContent())); $line_count = pht( - '%2$s Line(s)', - $line_count, - PhutilTranslator::getInstance()->formatNumber($line_count)); + '%s Line(s)', + new PhutilNumber($line_count)); $item = id(new PhabricatorObjectItemView()) ->setHeader($paste->getFullName()) ->setHref('/P'.$paste->getID()) ->setObject($paste) ->addAttribute(pht('Created %s by %s', $created, $author)) ->addIcon('none', $line_count) ->appendChild($source_code); $lang_name = $paste->getLanguage(); if ($lang_name) { $lang_name = idx($lang_map, $lang_name, $lang_name); $item->addIcon('none', phutil_escape_html($lang_name)); } $list->addItem($item); } return $list; } } diff --git a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php index 8b102e0822..ced807deff 100644 --- a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php +++ b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php @@ -1,296 +1,296 @@ array( 'Differential Revision', 'Differential Revisions', ), 'file(s)' => array('file', 'files'), 'Maniphest Task(s)' => array('Maniphest Task', 'Maniphest Tasks'), 'Please fix these errors and try again.' => array( 'Please fix this error and try again.', 'Please fix these errors and try again.', ), '%d Error(s)' => array('%d Error', '%d Errors'), '%d Warning(s)' => array('%d Warning', '%d Warnings'), '%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'), '%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'), '%d Detail(s)' => array('%d Detail', '%d Details'), '(%d line(s))' => array('(%d line)', '(%d lines)'), 'COMMIT(S)' => array('COMMIT', 'COMMITS'), '%d line(s)' => array('%d line', '%d lines'), 'added %d commit(s): %s' => array( 'added commit: %2$s', 'added commits: %2$s', ), 'removed %d commit(s): %s' => array( 'removed commit: %2$s', 'removed commits: %2$s', ), 'changed %d commit(s), added %d: %s; removed %d: %s' => 'changed commits, added: %3$s; removed: %5$s', 'ATTACHED %d COMMIT(S)' => array( 'ATTACHED COMMIT', 'ATTACHED COMMITS', ), 'added %d dependencie(s): %s' => array( 'added dependency: %2$s', 'added dependencies: %2$s', ), 'added %d dependent task(s): %s' => array( 'added dependent task: %2$s', 'added dependent tasks: %2$s', ), 'removed %d dependencie(s): %s' => array( 'removed dependency: %2$s', 'removed dependencies: %2$s', ), 'removed %d dependent task(s): %s' => array( 'removed dependent task: %2$s', 'removed dependent tasks: %2$s', ), 'changed %d dependencie(s), added %d: %s; removed %d: %s' => 'changed dependencies, added: %3$s; removed: %5$s', 'changed %d dependent task(s), added %d: %s; removed %d: %s', 'changed dependent tasks, added: %3$s; removed: %5$s', 'DEPENDENT %d TASK(s)' => array( 'DEPENDENT TASK', 'DEPENDENT TASKS', ), 'DEPENDS ON %d TASK(S)' => array( 'DEPENDS ON TASK', 'DEPENDS ON TASKS', ), 'DIFFERENTIAL %d REVISION(S)' => array( 'DIFFERENTIAL REVISION', 'DIFFERENTIAL REVISIONS', ), 'added %d revision(s): %s' => array( 'added revision: %2$s', 'added revisions: %2$s', ), 'removed %d revision(s): %s' => array( 'removed revision: %2$s', 'removed revisions: %2$s', ), 'changed %d revision(s), added %d: %s; removed %d: %s' => 'changed revisions, added %3$s; removed %5$s', 'There are %d raw fact(s) in storage.' => array( 'There is %d raw fact in storage.', 'There are %d raw facts in storage.', ), 'There are %d aggregate fact(s) in storage.' => array( 'There is %d aggregate fact in storage.', 'There are %d aggregate facts in storage.', ), '%d Commit(s) Awaiting Audit' => array( '%d Commit Awaiting Audit', '%d Commits Awaiting Audit', ), '%d Problem Commit(s)' => array( '%d Problem Commit', '%d Problem Commits', ), '%d Review(s) Need Attention' => array( '%d Review Needs Attention', '%d Reviews Need Attention', ), '%d Review(s) Waiting on Others' => array( '%d Review Waiting on Others', '%d Reviews Waiting on Others', ), '%d Flagged Object(s)' => array( '%d Flagged Object', '%d Flagged Objects', ), '%d Unbreak Now Task(s)!' => array( '%d Unbreak Now Task!', '%d Unbreak Now Tasks!', ), '%d Assigned Task(s)' => array( '%d Assigned Task', '%d Assigned Tasks', ), 'Show %d Lint Message(s)' => array( 'Show %d Lint Message', 'Show %d Lint Messages', ), 'Hide %d Lint Message(s)' => array( 'Hide %d Lint Message', 'Hide %d Lint Messages', ), 'Switch for %d Lint Message(s)' => array( 'Switch for %d Lint Message', 'Switch for %d Lint Messages', ), '%d Lint Message(s)' => array( '%d Lint Message', '%d Lint Messages', ), - 'This is a binary file. It is %2$s byte(s) in length.' => array( - 'This is a binary file. It is %2$s byte in length.', - 'This is a binary file. It is %2$s bytes in length.', + 'This is a binary file. It is %s byte(s) in length.' => array( + 'This is a binary file. It is %s byte in length.', + 'This is a binary file. It is %s bytes in length.', ), '%d Action(s) Have No Effect' => array( 'Action Has No Effect', 'Actions Have No Effect', ), '%d Action(s) With No Effect' => array( 'Action With No Effect', 'Actions With No Effect', ), '%s added %d subscriber(s): %s.' => array( array( '%s added a subscriber: %3$s.', '%s added subscribers: %3$s.', ), array( '%s added a subscriber: %3$s.', '%s added subscribers: %3$s.', ), ), '%s removed %d subscriber(s): %s.' => array( array( '%s removed a subscriber: %3$s.', '%s removed subscribers: %3$s.', ), array( '%s removed a subscriber: %3$s.', '%s removed subscribers: %3$s.', ), ), '%s added %d participant(s): %s.' => array( array( '%s added a participant: %3$s.', '%s added participants: %3$s.', ), array( '%s added a participant: %3$s.', '%s added participants: %3$s.', ), ), '%s removed %d participant(s): %s.' => array( array( '%s removed a participant: %3$s.', '%s removed participants: %3$s.', ), array( '%s removed a participant: %3$s.', '%s removed participants: %3$s.', ), ), - '%2$s Line(s)' => array( - '%2$s Line', - '%2$s Lines', + '%s Line(s)' => array( + '%s Line', + '%s Lines', ), "Indexing %d object(s) of type %s." => array( "Indexing %d object of type %s.", "Indexing %d object of type %s.", ), 'Run these %d command(s):' => array( 'Run this command:', 'Run these commands:', ), 'Install these %d PHP extension(s):' => array( 'Install this %d PHP extension:', 'Install these %d PHP extensions:', ), 'The current Phabricator configuration has these %d value(s):' => array( 'The current Phabricator configuration has this %d value:', 'The current Phabricator configuration has these %d values:', ), 'To update these %d value(s), run these command(s) from the command line:' => array( 'To update this %d value, run this command from the command line:', 'To update these %d values, run these commands from the command line:', ), 'You can update these %d value(s) here:' => array( 'You can update this %d value here:', 'You can update these %d values here:', ), 'The current PHP configuration has these %d value(s):' => array( 'The current PHP configuration has this %d value:', 'The current PHP configuration has these %d values:', ), 'To update these %d value(s), edit your PHP configuration file.' => array( 'To update this %d value, edit your PHP configuration file.', 'To update these %d values, edit your PHP configuration file.', ), 'To update these %d value(s), edit your PHP configuration file, located '. 'here:' => array( 'To update this %d value, edit your PHP configuration file, located '. 'here:', 'To update these %d values, edit your PHP configuration file, located '. 'here:', ), 'PHP also loaded these configuration file(s):' => array( 'PHP also loaded this configuration file:', 'PHP also loaded these configuration files:', ), 'You have %d unresolved setup issue(s)...' => array( 'You have %d unresolved setup issue...', 'You have %d unresolved setup issues...', ), ); } }