diff --git a/conf/default.conf.php b/conf/default.conf.php index 6b8a7c815d..8bbbaf9c4f 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -1,305 +1,314 @@ null, // If you have multiple environments, provide the production environment URI // here so that emails, etc., generated in development/sandbox environments // contain the right links. 'phabricator.production-uri' => null, // The default PHID for users who haven't uploaded a profile image. It should // be 50x50px. 'user.default-profile-image-phid' => 'PHID-FILE-f57aaefce707fc4060ef', // -- Access Control -------------------------------------------------------- // // Phabricator users have one of three access levels: "anyone", "verified", // or "admin". "anyone" means every user, including users who do not have // accounts or are not logged into the system. "verified" is users who have // accounts, are logged in, and have satisfied whatever verification steps // the configuration requires (e.g., email verification and/or manual // approval). "admin" is verified users with the "administrator" flag set. // These configuration options control which access level is required to read // data from Phabricator (e.g., view revisions and comments in Differential) // and write data to Phabricator (e.g., upload files and create diffs). By // default they are both set to "verified", meaning only verified user // accounts can interact with the system in any meaningful way. // If you are configuring an install for an open source project, you may // want to reduce the "phabricator.read-access" requirement to "anyone". This // will allow anyone to browse Phabricator content, even without logging in. // Alternatively, you could raise the "phabricator.write-access" requirement // to "admin", effectively creating a read-only install. // Controls the minimum access level required to read data from Phabricator // (e.g., view revisions in Differential). Allowed values are "anyone", // "verified", or "admin". Note that "anyone" includes users who are not // logged in! You should leave this at 'verified' unless you want your data // to be publicly readable (e.g., you are developing open source software). 'phabricator.read-access' => 'verified', // Controls the minimum access level required to write data to Phabricator // (e.g., create new revisions in Differential). Allowed values are // "verified" or "admin". Setting this to "admin" will effectively create a // read-only install. 'phabricator.write-access' => 'verified', // -- DarkConsole ----------------------------------------------------------- // // DarkConsole is a administrative debugging/profiling tool built into // Phabricator. You can leave it disabled unless you're developing against // Phabricator. // Determines whether or not DarkConsole is available. DarkConsole exposes // some data like queries and stack traces, so you should be careful about // turning it on in production (although users can not normally see it, even // if the deployment configuration enables it). 'darkconsole.enabled' => true, // Always enable DarkConsole, even for logged out users. This potentially // exposes sensitive information to users, so make sure untrusted users can // not access an install running in this mode. You should definitely leave // this off in production. It is only really useful for using DarkConsole // utilties to debug or profile logged-out pages. You must set // 'darkconsole.enabled' to use this option. 'darkconsole.always-on' => false, // Allows you to mask certain configuration values from appearing in the // "Config" tab of DarkConsole. 'darkconsole.config-mask' => array( 'mysql.pass', 'amazon-ses.secret-key', 'recaptcha.private-key', 'phabricator.csrf-key', 'facebook.application-secret', 'github.secret', ), // -- MySQL --------------------------------------------------------------- // // The username to use when connecting to MySQL. 'mysql.user' => 'root', // The password to use when connecting to MySQL. 'mysql.pass' => '', // The MySQL server to connect to. 'mysql.host' => 'localhost', // -- Email ----------------------------------------------------------------- // // Some Phabricator tools send email notifications, e.g. when Differential // revisions are updated or Maniphest tasks are changed. These options allow // you to configure how email is delivered. // You can test your mail setup by going to "MetaMTA" in the web interface, // clicking "Send New Message", and then composing a message. // Default address to send mail "From". 'metamta.default-address' => 'noreply@example.com', // Domain used to generate Message-IDs. 'metamta.domain' => 'example.com', // When a user takes an action which generates an email notification (like // commenting on a Differential revision), Phabricator can either send that // mail "From" the user's email address (like "alincoln@logcabin.com") or // "From" the 'metamta.default-address' address. The user experience is // generally better if Phabricator uses the user's real address as the "From" // since the messages are easier to organize when they appear in mail clients, // but this will only work if the server is authorized to send email on behalf // of the "From" domain. Practically, this means: // - If you are doing an install for Example Corp and all the users will // have corporate @corp.example.com addresses and any hosts Phabricator // is running on are authorized to send email from corp.example.com, // you can enable this to make the user experience a little better. // - If you are doing an install for an open source project and your // users will be registering via Facebook and using personal email // addresses, you MUST NOT enable this or virtually all of your outgoing // email will vanish into SFP blackholes. // - If your install is anything else, you're much safer leaving this // off since the risk in turning it on is that your outgoing mail will // mostly never arrive. 'metamta.can-send-as-user' => false, // Adapter class to use to transmit mail to the MTA. The default uses // PHPMailerLite, which will invoke PHP's mail() function. This is appropriate // if mail() actually works on your host, but if you haven't configured mail // it may not be so great. You can also use Amazon SES, by changing this to // 'PhabricatorMailImplementationAmazonSESAdapter', signing up for SES, and // filling in your 'amazon-ses.access-key' and 'amazon-ses.secret-key' below. 'metamta.mail-adapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', // When email is sent, try to hand it off to the MTA immediately. This may // be worth disabling if your MTA infrastructure is slow or unreliable. If you // disable this option, you must run the 'metamta_mta.php' daemon or mail // won't be handed off to the MTA. If you're using Amazon SES it can be a // little slugish sometimes so it may be worth disabling this and moving to // the daemon after you've got your install up and running. If you have a // properly configured local MTA it should not be necessary to disable this. 'metamta.send-immediately' => true, // If you're using Amazon SES to send email, provide your AWS access key // and AWS secret key here. To set up Amazon SES with Phabricator, you need // to: // - Make sure 'metamta.mail-adapter' is set to: // "PhabricatorMailImplementationAmazonSESAdapter" // - Make sure 'metamta.can-send-as-user' is false. // - Make sure 'metamta.default-address' is configured to something sensible. // - Make sure 'metamta.default-address' is a validated SES "From" address. 'amazon-ses.access-key' => null, 'amazon-ses.secret-key' => null, // -- Auth ------------------------------------------------------------------ // // Can users login with a username/password, or by following the link from // a password reset email? You can disable this and configure one or more // OAuth providers instead. 'auth.password-auth-enabled' => true, // -- Accounts -------------------------------------------------------------- // // Is basic account information (email, real name, profile picture) editable? // If you set up Phabricator to automatically synchronize account information // from some other authoritative system, you can disable this to ensure // information remains consistent across both systems. 'account.editable' => true, // -- Facebook ------------------------------------------------------------ // // Can users use Facebook credentials to login to Phabricator? 'facebook.auth-enabled' => false, // Can users use Facebook credentials to create new Phabricator accounts? 'facebook.registration-enabled' => true, // Are Facebook accounts permanently linked to Phabricator accounts, or can // the user unlink them? 'facebook.auth-permanent' => false, // The Facebook "Application ID" to use for Facebook API access. 'facebook.application-id' => null, // The Facebook "Application Secret" to use for Facebook API access. 'facebook.application-secret' => null, // -- Github ---------------------------------------------------------------- // // Can users use Github credentials to login to Phabricator? 'github.auth-enabled' => false, // Can users use Github credentials to create new Phabricator accounts? 'github.registration-enabled' => true, // Are Github accounts permanently linked to Phabricator accounts, or can // the user unlink them? 'github.auth-permanent' => false, // The Github "Client ID" to use for Github API access. 'github.application-id' => null, // The Github "Secret" to use for Github API access. 'github.application-secret' => null, // -- Recaptcha ------------------------------------------------------------- // // Is Recaptcha enabled? If disabled, captchas will not appear. 'recaptcha.enabled' => false, // Your Recaptcha public key, obtained from Recaptcha. 'recaptcha.public-key' => null, // Your Recaptcha private key, obtained from Recaptcha. 'recaptcha.private-key' => null, // -- Misc ------------------------------------------------------------------ // // This is hashed with other inputs to generate CSRF tokens. If you want, you // can change it to some other string which is unique to your install. This // will make your install more secure in a vague, mostly theoretical way. But // it will take you like 3 seconds of mashing on your keyboard to set it up so // you might as well. 'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3', // Version string displayed in the footer. You probably should leave this // alone. 'phabricator.version' => 'UNSTABLE', // PHP requires that you set a timezone in your php.ini before using date // functions, or it will emit a warning. If this isn't possible (for instance, // because you are using HPHP) you can set some valid constant for // date_default_timezone_set() here and Phabricator will set it on your // behalf, silencing the warning. 'phabricator.timezone' => null, // -- Files ----------------------------------------------------------------- // // Lists which uploaded file types may be viewed in the browser. If a file // has a mime type which does not appear in this list, it will always be // downloaded instead of displayed. This is a security consideration: if a // user uploads a file of type "text/html" and it is displayed as // "text/html", they can eaily execute XSS attacks. This is also a usability // consideration, since browsers tend to freak out when viewing enormous // binary files. // // The keys in this array are viewable mime types; the values are the mime // types they will be delivered as when they are viewed in the browser. 'files.viewable-mime-types' => array( 'image/jpeg' => 'image/jpeg', 'image/jpg' => 'image/jpg', 'image/png' => 'image/png', 'image/gif' => 'image/gif', 'text/plain' => 'text/plain; charset=utf-8', ), +// -- Differential ---------------------------------------------------------- // + + 'differential.revision-custom-detail-renderer' => null, + + +// -- Maniphest ------------------------------------------------------------- // + + 'maniphest.enabled' => true, + // -- Customization --------------------------------------------------------- // // Paths to additional phutil libraries to load. 'load-libraries' => array(), 'aphront.default-application-configuration-class' => 'AphrontDefaultApplicationConfiguration', 'controller.oauth-registration' => 'PhabricatorOAuthDefaultRegistrationController', // Directory that phd (the Phabricator daemon control script) should use to // track running daemons. 'phd.pid-directory' => '/var/tmp/phd', ); diff --git a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php index b510dd45bd..7c1a338088 100644 --- a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php @@ -1,851 +1,855 @@ revisionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $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 = end($diffs); $target_id = $request->getInt('id'); if ($target_id) { if (isset($diffs[$target_id])) { $target = $diffs[$target_id]; } } $diffs = mpull($diffs, null, 'getID'); if (empty($diffs[$diff_vs])) { $diff_vs = null; } list($changesets, $vs_map) = $this->loadChangesetsAndVsMap($diffs, $diff_vs, $target); $comments = $revision->loadComments(); $comments = array_merge( $this->getImplicitComments($revision), $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 ($revision->getAttached() as $type => $phids) { foreach ($phids as $phid => $info) { $object_phids[] = $phid; } } $object_phids = array_unique($object_phids); $handles = id(new PhabricatorObjectHandleData($object_phids)) ->loadHandles(); $request_uri = $request->getRequestURI(); $limit = 100; $large = $request->getStr('large'); if (count($changesets) > $limit && !$large) { $count = number_format(count($changesets)); $warning = new AphrontErrorView(); $warning->setTitle('Very Large Diff'); $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $warning->setWidth(AphrontErrorView::WIDTH_WIDE); $warning->appendChild( "

This diff is very large and affects {$count} files. Only ". "the first {$limit} files are shown. ". "". phutil_render_tag( 'a', array( 'href' => $request_uri->alter('large', 'true'), ), 'Show All Files'). ""); $warning = $warning->render(); $visible_changesets = array_slice($changesets, 0, $limit, true); } else { $warning = null; $visible_changesets = $changesets; } $diff_properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d AND name IN (%Ls)', $target->getID(), array( 'arc:lint', 'arc:unit', )); $diff_properties = mpull($diff_properties, 'getData', 'getName'); $revision_detail = new DifferentialRevisionDetailView(); $revision_detail->setRevision($revision); $custom_renderer_class = PhabricatorEnv::getEnvConfig( 'differential.revision-custom-detail-renderer'); if ($custom_renderer_class) { PhutilSymbolLoader::loadClass($custom_renderer_class); $custom_renderer = newv($custom_renderer_class, array()); } $properties = $this->getRevisionProperties( $revision, $target, $handles, $diff_properties); if ($custom_renderer) { $properties = array_merge( $properties, $custom_renderer->generateProperties($revision, $target)); } $revision_detail->setProperties($properties); $actions = $this->getRevisionActions($revision); if ($custom_renderer) { $actions = array_merge( $actions, $custom_renderer->generateActionLinks($revision, $target)); } $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); $diff_history = new DifferentialRevisionUpdateHistoryView(); $diff_history->setDiffs($diffs); $diff_history->setSelectedVersusDiffID($diff_vs); $diff_history->setSelectedDiffID($target->getID()); $toc_view = new DifferentialDiffTableOfContentsView(); $toc_view->setChangesets($changesets); $changeset_view = new DifferentialChangesetListView(); $changeset_view->setChangesets($visible_changesets); $changeset_view->setEditable(true); $changeset_view->setRevision($revision); $changeset_view->setVsMap($vs_map); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'differential-comment-'.$revision->getID()); if ($draft) { $draft = $draft->getDraft(); } else { $draft = null; } $comment_form = new DifferentialAddCommentView(); $comment_form->setRevision($revision); $comment_form->setActions($this->getRevisionCommentActions($revision)); $comment_form->setActionURI('/differential/comment/save/'); $comment_form->setUser($user); $comment_form->setDraft($draft); return $this->buildStandardPageResponse( '

'. $revision_detail->render(). $comment_view->render(). $diff_history->render(). $toc_view->render(). $warning. $changeset_view->render(). $comment_form->render(). '
', array( 'title' => $revision->getTitle(), )); } private function getImplicitComments(DifferentialRevision $revision) { $template = new DifferentialComment(); $template->setAuthorPHID($revision->getAuthorPHID()); $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 getRevisionProperties( DifferentialRevision $revision, DifferentialDiff $diff, array $handles, array $diff_properties) { $properties = array(); $status = $revision->getStatus(); $status = DifferentialRevisionStatus::getNameForRevisionStatus($status); $properties['Revision Status'] = ''.$status.''; $author = $handles[$revision->getAuthorPHID()]; $properties['Author'] = $author->renderLink(); $properties['Reviewers'] = $this->renderHandleLinkList( array_select_keys( $handles, $revision->getReviewers())); $properties['CCs'] = $this->renderHandleLinkList( array_select_keys( $handles, $revision->getCCPHIDs())); $host = $diff->getSourceMachine(); if ($host) { $properties['Host'] = phutil_escape_html($host); } $path = $diff->getSourcePath(); if ($path) { $branch = $diff->getBranch() ? ' ('.$diff->getBranch().')' : ''; $properties['Path'] = phutil_escape_html("{$path} {$branch}"); } $lstar = DifferentialRevisionUpdateHistoryView::renderDiffLintStar($diff); $lmsg = DifferentialRevisionUpdateHistoryView::getDiffLintMessage($diff); $ldata = idx($diff_properties, 'arc:lint'); $ltail = null; if ($ldata) { $ldata = igroup($ldata, 'path'); $lint_messages = array(); foreach ($ldata as $path => $messages) { $message_markup = array(); foreach ($messages as $message) { $path = idx($message, 'path'); $line = idx($message, 'line'); $code = idx($message, 'code'); $severity = idx($message, 'severity'); $name = idx($message, 'name'); $description = idx($message, 'description'); $message_markup[] = '
  • '. ''. phutil_escape_html(ucwords($severity)). ''. ' '. '('.phutil_escape_html($code).') '. phutil_escape_html($name). ' at line '.phutil_escape_html($line). '

    '.phutil_escape_html($description).'

    '. '
  • '; } $lint_messages[] = '
  • '. 'Lint for '.phutil_escape_html($path).''. ''. '
  • '; } $ltail = '
    '. ''. '
    '; } $properties['Lint'] = $lstar.' '.$lmsg.$ltail; $ustar = DifferentialRevisionUpdateHistoryView::renderDiffUnitStar($diff); $umsg = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff); $udata = idx($diff_properties, 'arc:unit'); $utail = null; if ($udata) { $unit_messages = array(); foreach ($udata as $test) { $name = phutil_escape_html(idx($test, 'name')); $result = phutil_escape_html(idx($test, 'result')); $userdata = phutil_escape_html(idx($test, 'userdata')); if (strlen($userdata) > 256) { $userdata = substr($userdata, 0, 256).'...'; } $userdata = str_replace("\n", '
    ', $userdata); $unit_messages[] = ''. ''.$name.''. ''. '
    '. strtoupper($result). '
    '. ''. ''.$userdata.''. ''; } $utail = '
    '. ''. implode("\n", $unit_messages). '
    '. '
    '; } $properties['Unit'] = $ustar.' '.$umsg.$utail; - $tasks = $revision->getAttachedPHIDs( - PhabricatorPHIDConstants::PHID_TYPE_TASK); - if ($tasks) { - $links = array(); - foreach ($tasks as $task_phid) { - $links[] = $handles[$task_phid]->renderLink(); + if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) { + $tasks = $revision->getAttachedPHIDs( + PhabricatorPHIDConstants::PHID_TYPE_TASK); + if ($tasks) { + $links = array(); + foreach ($tasks as $task_phid) { + $links[] = $handles[$task_phid]->renderLink(); + } + $properties['Maniphest Tasks'] = implode('
    ', $links); } - $properties['Maniphest Tasks'] = implode('
    ', $links); } $commit_phids = $revision->getCommitPHIDs(); if ($commit_phids) { $links = array(); foreach ($commit_phids as $commit_phid) { $links[] = $handles[$commit_phid]->renderLink(); } $properties['Commits'] = implode('
    ', $links); } return $properties; } private function getRevisionActions(DifferentialRevision $revision) { $viewer_phid = $this->getRequest()->getUser()->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()); $status = $revision->getStatus(); $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); $links = array(); if ($viewer_is_owner) { $links[] = array( 'class' => 'revision-edit', 'href' => "/differential/revision/edit/{$revision_id}/", 'name' => 'Edit Revision', ); } if (!$viewer_is_owner && !$viewer_is_reviewer) { $action = $viewer_is_cc ? 'rem' : 'add'; $links[] = array( 'class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add', 'href' => "/differential/subscribe/{$action}/{$revision_id}/", 'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe', 'instant' => true, ); } else { $links[] = array( 'class' => 'subscribe-rem unavailable', 'name' => 'Automatically Subscribed', ); } require_celerity_resource('phabricator-object-selector-css'); require_celerity_resource('javelin-behavior-phabricator-object-selector'); - $links[] = array( - 'class' => 'attach-maniphest', - 'name' => 'Edit Maniphest Tasks', - 'href' => "/differential/attach/{$revision_id}/TASK/", - 'sigil' => 'workflow', - ); + if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) { + $links[] = array( + 'class' => 'attach-maniphest', + 'name' => 'Edit Maniphest Tasks', + 'href' => "/differential/attach/{$revision_id}/TASK/", + 'sigil' => 'workflow', + ); + } $links[] = array( 'class' => 'transcripts-metamta', 'name' => 'MetaMTA Transcripts', 'href' => "/mail/?phid={$revision_phid}", ); $links[] = array( 'class' => 'transcripts-herald', 'name' => 'Herald Transcripts', 'href' => "/herald/transcript/?phid={$revision_phid}", ); return $links; } private function renderHandleLinkList(array $list) { if (empty($list)) { return 'None'; } return implode(', ', mpull($list, 'renderLink')); } private function getRevisionCommentActions(DifferentialRevision $revision) { $actions = array( DifferentialAction::ACTION_COMMENT => true, ); $viewer_phid = $this->getRequest()->getUser()->getPHID(); $viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID()); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); if ($viewer_is_owner) { switch ($revision->getStatus()) { case DifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ABANDON] = true; break; case DifferentialRevisionStatus::NEEDS_REVISION: case DifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_REQUEST] = true; break; case DifferentialRevisionStatus::COMMITTED: break; case DifferentialRevisionStatus::ABANDONED: $actions[DifferentialAction::ACTION_RECLAIM] = true; break; } } else { switch ($revision->getStatus()) { case DifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_REJECT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case DifferentialRevisionStatus::NEEDS_REVISION: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case DifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_REJECT] = true; break; case DifferentialRevisionStatus::COMMITTED: case DifferentialRevisionStatus::ABANDONED: break; } } $actions[DifferentialAction::ACTION_ADDREVIEWERS] = true; return array_keys($actions); } private function loadInlineComments(array $comments, array &$changesets) { $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(array $diffs, $diff_vs, $target) { $load_ids = array(); if ($diff_vs) { $load_ids[] = $diff_vs; } $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'); $vs_map = array(); if ($diff_vs) { $vs_changesets = idx($changeset_groups, $diff_vs, array()); $vs_changesets = mpull($vs_changesets, null, 'getFilename'); foreach ($changesets as $key => $changeset) { $file = $changeset->getFilename(); if (isset($vs_changesets[$file])) { $vs_map[$changeset->getID()] = $vs_changesets[$file]->getID(); unset($vs_changesets[$file]); } } foreach ($vs_changesets as $changeset) { $changesets[$changeset->getID()] = $changeset; $vs_map[$changeset->getID()] = -1; } } $changesets = msort($changesets, 'getSortKey'); return array($changesets, $vs_map); } } /* protected function getSandcastleURI(Diff $diff) { $uri = $this->getDiffProperty($diff, 'facebook:sandcastle_uri'); if (!$uri) { $uri = $diff->getSandboxURL(); } return $uri; } protected function getDiffProperty(Diff $diff, $property, $default = null) { $diff_id = $diff->getID(); if (empty($this->diffProperties[$diff_id])) { $props = id(new DifferentialDiffProperty()) ->loadAllWhere('diffID = %s', $diff_id); $dict = array_pull($props, 'getData', 'getName'); $this->diffProperties[$diff_id] = $dict; } return idx($this->diffProperties[$diff_id], $property, $default); } $diff_table->appendChild( {id(