subparser = new DifferentialChangesetParser();
foreach ($changeset->getHunks() as $hunk) {
$this->subparser->parseHunk($hunk);
}
// this parser takes new changeset; will use subparser's text later
$changes = id(new ArcanistDiffParser())->parseDiff($diff);
$diff = DifferentialDiff::newFromRawChanges($changes);
$changesets = $diff->getChangesets();
$changeset = reset($changesets);
$this->setChangeset($changeset);
}
foreach ($changeset->getHunks() as $hunk) {
$this->parseHunk($hunk);
}
$this->process();
if (!$skip_cache) {
$this->saveCache();
}
}
}
}
public function render(
$range_start = null,
$range_len = null,
$mask_force = array()) {
$this->highlightEngine = new PhutilDefaultSyntaxHighlighterEngine();
$this->highlightEngine->setConfig(
'pygments.enabled',
PhabricatorEnv::getEnvConfig('pygments.enabled'));
$this->tryCacheStuff();
$feedback_mask = array();
switch ($this->changeset->getFileType()) {
case DifferentialChangeType::FILE_IMAGE:
$old = null;
$cur = null;
$metadata = $this->changeset->getMetadata();
$data = idx($metadata, 'attachment-data');
$old_phid = idx($metadata, 'old:binary-phid');
$new_phid = idx($metadata, 'new:binary-phid');
if ($old_phid || $new_phid) {
if ($old_phid) {
$old_uri = PhabricatorFileURI::getViewURIForPHID($old_phid);
$old = phutil_render_tag(
'img',
array(
'src' => $old_uri,
));
}
if ($new_phid) {
$new_uri = PhabricatorFileURI::getViewURIForPHID($new_phid);
$cur = phutil_render_tag(
'img',
array(
'src' => $new_uri,
));
}
}
$output = $this->renderChangesetTable(
$this->changeset,
''.
' | '.
''.
' '.
$old.
' '.
' | '.
' | '.
''.
' '.
$cur.
' '.
' | '.
'
');
return $output;
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
$output = $this->renderChangesetTable($this->changeset, null);
return $output;
}
$shield = null;
if ($range_start === null && $range_len === null && !$this->comments) {
if ($this->isGenerated()) {
$shield = $this->renderShield(
"This file contains generated code, which does not normally need ".
"to be reviewed.",
true);
} else if ($this->isUnchanged()) {
if ($this->isWhitespaceOnly()) {
$shield = $this->renderShield(
"This file was changed only by adding or removing trailing ".
"whitespace.",
false);
} else {
$shield = $this->renderShield(
"The contents of this file were not changed.",
false);
}
} else if ($this->isDeleted()) {
$shield = $this->renderShield(
"This file was completely deleted.",
true);
} else if ($this->changeset->getAffectedLineCount() > 2500) {
$lines = number_format($this->changeset->getAffectedLineCount());
$shield = $this->renderShield(
"This file has a very large number of changes ({$lines} lines).",
true);
} else if (preg_match('/\.sql3$/', $this->changeset->getFilename())) {
$shield = $this->renderShield(
".sql3 files are hidden by default.",
true);
}
}
if ($shield) {
return $this->renderChangesetTable($this->changeset, $shield);
}
$old_comments = array();
$new_comments = array();
$old_mask = array();
$new_mask = array();
$feedback_mask = array();
if ($this->comments) {
foreach ($this->comments as $comment) {
$start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0);
$end = $comment->getLineNumber() +
$comment->getLineLength() +
self::LINES_CONTEXT;
$new = $this->isCommentOnRightSideWhenDisplayed($comment);
for ($ii = $start; $ii <= $end; $ii++) {
if ($new) {
$new_mask[$ii] = true;
} else {
$old_mask[$ii] = true;
}
}
}
foreach ($this->old as $ii => $old) {
if (isset($old['line']) && isset($old_mask[$old['line']])) {
$feedback_mask[$ii] = true;
}
}
foreach ($this->new as $ii => $new) {
if (isset($new['line']) && isset($new_mask[$new['line']])) {
$feedback_mask[$ii] = true;
}
}
$this->comments = msort($this->comments, 'getID');
foreach ($this->comments as $comment) {
$final = $comment->getLineNumber() +
$comment->getLineLength();
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$new_comments[$final][] = $comment;
} else {
$old_comments[$final][] = $comment;
}
}
}
$html = $this->renderTextChange(
$range_start,
$range_len,
$mask_force,
$feedback_mask,
$old_comments,
$new_comments);
return $this->renderChangesetTable($this->changeset, $html);
}
/**
* Determine if an inline comment will appear on the rendered diff,
* taking into consideration which halves of which changesets will actually
* be shown.
*
* @param DifferentialInlineComment Comment to test for visibility.
* @return bool True if the comment is visible on the rendered diff.
*/
private function isCommentVisibleOnRenderedDiff(
DifferentialInlineComment $comment) {
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
if ($changeset_id == $this->leftSideChangesetID &&
$is_new == $this->leftSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Determine if a comment will appear on the right side of the display diff.
* Note that the comment must appear somewhere on the rendered changeset, as
* per isCommentVisibleOnRenderedDiff().
*
* @param DifferentialInlineComment Comment to test for display location.
* @return bool True for right, false for left.
*/
private function isCommentOnRightSideWhenDisplayed(
DifferentialInlineComment $comment) {
if (!$this->isCommentVisibleOnRenderedDiff($comment)) {
throw new Exception("Comment is not visible on changeset!");
}
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
return false;
}
protected function renderShield($message, $more) {
if ($more) {
$end = $this->getLength();
$reference = $this->renderingReference;
$more =
' '.
javelin_render_tag(
'a',
array(
'mustcapture' => true,
'sigil' => 'show-more',
'class' => 'complete',
'href' => '#',
'meta' => array(
'ref' => $reference,
'range' => "0-{$end}",
),
),
'Show File Contents');
} else {
$more = null;
}
return javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
''.
phutil_escape_html($message).
$more.
' | ');
}
protected function renderTextChange(
$range_start,
$range_len,
$mask_force,
$feedback_mask,
array $old_comments,
array $new_comments) {
$context_not_available = null;
if ($this->missingOld || $this->missingNew) {
$context_not_available = javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
''.
'Context not available.'.
' | ');
$context_not_available = $context_not_available;
}
$html = array();
$rows = max(
count($this->old),
count($this->new));
if ($range_start === null) {
$range_start = 0;
}
if ($range_len === null) {
$range_len = $rows;
}
$range_len = min($range_len, $rows - $range_start);
// Gaps - compute gaps in the visible display diff, where we will render
// "Show more context" spacers. This builds an aggregate $mask of all the
// lines we must show (because they are near changed lines, near inline
// comments, or the request has explicitly asked for them, i.e. resulting
// from the user clicking "show more") and then finds all the gaps between
// visible lines. If a gap is smaller than the context size, we just
// display it. Otherwise, we record it into $gaps and will render a
// "show more context" element instead of diff text below.
$gaps = array();
$gap_start = 0;
$in_gap = false;
$mask = $this->visible + $mask_force + $feedback_mask;
$mask[$range_start + $range_len] = true;
for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
if (isset($mask[$ii])) {
if ($in_gap) {
$gap_length = $ii - $gap_start;
if ($gap_length <= self::LINES_CONTEXT) {
for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
$mask[$jj] = true;
}
} else {
$gaps[] = array($gap_start, $gap_length);
}
$in_gap = false;
}
} else {
if (!$in_gap) {
$gap_start = $ii;
$in_gap = true;
}
}
}
$gaps = array_reverse($gaps);
$reference = $this->renderingReference;
$left_id = $this->leftSideChangesetID;
$right_id = $this->rightSideChangesetID;
// "N" stands for 'new' and means the comment should attach to the new file
// when stored, i.e. DifferentialInlineComment->setIsNewFile().
// "O" stands for 'old' and means the comment should attach to the old file.
$left_char = $this->leftSideAttachesToNewFile
? 'N'
: 'O';
$right_char = $this->rightSideAttachesToNewFile
? 'N'
: 'O';
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
// If we aren't going to show this line, we've just entered a gap.
// Pop information about the next gap off the $gaps stack and render
// an appropriate "Show more context" element. This branch eventually
// increments $ii by the entire size of the gap and then continues
// the loop.
$gap = array_pop($gaps);
$top = $gap[0];
$len = $gap[1];
$end = $top + $len - 20;
$contents = array();
if ($len > 40) {
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'ref' => $reference,
'range' => "{$top}-{$len}/{$top}-20",
),
),
"\xE2\x96\xB2 Show 20 Lines");
}
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'ref' => $reference,
'range' => "{$top}-{$len}/{$top}-{$len}",
),
),
'Show All '.$len.' Lines');
if ($len > 40) {
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'ref' => $reference,
'range' => "{$top}-{$len}/{$end}-20",
),
),
"\xE2\x96\xBC Show 20 Lines");
};
$container = javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
''.
implode(' • ', $contents).
' | ');
$html[] = $container;
$ii += ($len - 1);
continue;
}
if (isset($this->old[$ii])) {
$o_num = $this->old[$ii]['line'];
$o_text = isset($this->oldRender[$ii]) ? $this->oldRender[$ii] : null;
$o_attr = null;
if ($this->old[$ii]['type']) {
if (empty($this->new[$ii])) {
$o_attr = ' class="old old-full"';
} else {
$o_attr = ' class="old"';
}
}
} else {
$o_num = null;
$o_text = null;
$o_attr = null;
}
if (isset($this->new[$ii])) {
$n_num = $this->new[$ii]['line'];
$n_text = isset($this->newRender[$ii]) ? $this->newRender[$ii] : null;
$n_attr = null;
if ($this->new[$ii]['type']) {
if (empty($this->old[$ii])) {
$n_attr = ' class="new new-full"';
} else {
$n_attr = ' class="new"';
}
}
} else {
$n_num = null;
$n_text = null;
$n_attr = null;
}
if (($o_num && !empty($this->missingOld[$o_num])) ||
($n_num && !empty($this->missingNew[$n_num]))) {
$html[] = $context_not_available;
}
if ($o_num && $left_id) {
$o_id = ' id="C'.$left_id.$left_char.'L'.$o_num.'"';
} else {
$o_id = null;
}
if ($n_num && $right_id) {
$n_id = ' id="C'.$right_id.$right_char.'L'.$n_num.'"';
} else {
$n_id = null;
}
// NOTE: The Javascript is sensitive to whitespace changes in this
// block!
$html[] =
''.
'| '.$o_num.' | '.
''.$o_text.' | '.
''.$n_num.' | '.
''.$n_text.' | '.
'
';
if ($context_not_available && ($ii == $rows - 1)) {
$html[] = $context_not_available;
}
if ($o_num && isset($old_comments[$o_num])) {
foreach ($old_comments[$o_num] as $comment) {
$xhp = $this->renderInlineComment($comment);
$html[] =
' | '.
$xhp.
' | | |
';
}
}
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $comment) {
$xhp = $this->renderInlineComment($comment);
$html[] =
' | | | '.
$xhp.
' |
';
}
}
}
return implode('', $html);
}
private function renderInlineComment(DifferentialInlineComment $comment) {
$user = $this->user;
$edit = $user &&
($comment->getAuthorPHID() == $user->getPHID()) &&
(!$comment->getCommentID());
$on_right = $this->isCommentOnRightSideWhenDisplayed($comment);
return id(new DifferentialInlineCommentView())
->setInlineComment($comment)
->setOnRight($on_right)
->setHandles($this->handles)
->setMarkupEngine($this->markupEngine)
->setEditable($edit)
->render();
}
protected function renderPropertyChangeHeader($changeset) {
$old = $changeset->getOldProperties();
$new = $changeset->getNewProperties();
if ($old === $new) {
return null;
}
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
$new == array('unix:filemode' => '100644')) {
return null;
}
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
$old == array('unix:filemode' => '100644')) {
return null;
}
return null;
/*
TODO
$table = ;
$table->appendChild(
);
$keys = array_keys($old + $new);
sort($keys);
foreach ($keys as $key) {
$oval = idx($old, $key);
$nval = idx($new, $key);
if ($oval !== $nval) {
if ($oval === null) {
$oval = null;
}
if ($nval === null) {
$nval = null;
}
$table->appendChild(
| {$key} |
{$oval} |
{$nval} |
);
}
}
return $table;
*/
}
protected function renderChangesetTable($changeset, $contents) {
$props = $this->renderPropertyChangeHeader($this->changeset);
$table = null;
if ($contents) {
$table =
'';
}
if (!$table && !$props) {
$notice = $this->renderChangeTypeHeader($this->changeset, true);
} else {
$notice = $this->renderChangeTypeHeader($this->changeset, false);
}
return implode(
"\n",
array(
$notice,
$props,
$table,
));
}
protected function renderChangeTypeHeader($changeset, $force) {
static $articles = array(
DifferentialChangeType::FILE_IMAGE => 'an',
);
static $files = array(
DifferentialChangeType::FILE_TEXT => 'file',
DifferentialChangeType::FILE_IMAGE => 'image',
DifferentialChangeType::FILE_DIRECTORY => 'directory',
DifferentialChangeType::FILE_BINARY => 'binary file',
DifferentialChangeType::FILE_SYMLINK => 'symlink',
);
static $changes = array(
DifferentialChangeType::TYPE_ADD => 'added',
DifferentialChangeType::TYPE_CHANGE => 'changed',
DifferentialChangeType::TYPE_DELETE => 'deleted',
DifferentialChangeType::TYPE_MOVE_HERE => 'moved from',
DifferentialChangeType::TYPE_COPY_HERE => 'copied from',
DifferentialChangeType::TYPE_MOVE_AWAY => 'moved to',
DifferentialChangeType::TYPE_COPY_AWAY => 'copied to',
DifferentialChangeType::TYPE_MULTICOPY
=> 'deleted after being copied to',
);
$change = $changeset->getChangeType();
$file = $changeset->getFileType();
$message = null;
if ($change == DifferentialChangeType::TYPE_CHANGE &&
$file == DifferentialChangeType::FILE_TEXT) {
if ($force) {
// We have to force something to render because there were no changes
// of other kinds.
$message = "This {$files[$file]} was not modified.";
} else {
// Default case of changes to a text file, no metadata.
return null;
}
} else {
$verb = idx($changes, $change, 'changed');
switch ($change) {
default:
$message = "This {$files[$file]} was {$verb}.";
break;
case DifferentialChangeType::TYPE_MOVE_HERE:
case DifferentialChangeType::TYPE_COPY_HERE:
$message =
"This {$files[$file]} was {$verb} ".
"{$changeset->getOldFile()}.";
break;
case DifferentialChangeType::TYPE_MOVE_AWAY:
case DifferentialChangeType::TYPE_COPY_AWAY:
case DifferentialChangeType::TYPE_MULTICOPY:
$paths = $changeset->getAwayPaths();
if (count($paths) > 1) {
$message =
"This {$files[$file]} was {$verb}: ".
"".implode(', ', $paths).".";
} else {
$message =
"This {$files[$file]} was {$verb} ".
"".reset($paths).".";
}
break;
case DifferentialChangeType::TYPE_CHANGE:
$message = "This is ".idx($articles, $file, 'a')." {$files[$file]}.";
break;
}
}
return
''.
$message.
'
';
}
public function renderForEmail() {
$ret = '';
$min = min(count($this->old), count($this->new));
for ($i = 0; $i < $min; $i++) {
$o = $this->old[$i];
$n = $this->new[$i];
if (!isset($this->visible[$i])) {
continue;
}
if ($o['line'] && $n['line']) {
// It is quite possible there are better ways to achieve this. For
// example, "white-space: pre;" can do a better job, WERE IT NOT for
// broken email clients like OWA which use newlines to do weird
// wrapping. So dont give them newlines.
if (isset($this->intra[$i])) {
$ret .= sprintf(
"- %s
",
str_replace(" ", " ", phutil_escape_html($o['text']))
);
$ret .= sprintf(
"+ %s
",
str_replace(" ", " ", phutil_escape_html($n['text']))
);
} else {
$ret .= sprintf(" %s
",
str_replace(" ", " ", phutil_escape_html($n['text']))
);
}
} else if ($o['line'] && !$n['line']) {
$ret .= sprintf(
"- %s
",
str_replace(" ", " ", phutil_escape_html($o['text']))
);
} else {
$ret .= sprintf(
"+ %s
",
str_replace(" ", " ", phutil_escape_html($n['text']))
);
}
}
return $ret;
}
}
diff --git a/src/applications/differential/parser/changeset/__init__.php b/src/applications/differential/parser/changeset/__init__.php
index 2dc0087010..e6fd9e2ec8 100644
--- a/src/applications/differential/parser/changeset/__init__.php
+++ b/src/applications/differential/parser/changeset/__init__.php
@@ -1,29 +1,30 @@
', where '' ".
"is one of 'development', 'production', or a custom environment.");
}
if (!function_exists('mysql_connect')) {
phabricator_fatal_config_error(
"The PHP MySQL extension is not installed. This extension is required.");
}
if (!isset($_REQUEST['__path__'])) {
phabricator_fatal_config_error(
"__path__ is not set. Your rewrite rules are not configured correctly.");
}
if (get_magic_quotes_gpc()) {
phabricator_fatal_config_error(
"Your server is configured with PHP 'magic_quotes_gpc' enabled. This ".
"feature is 'highly discouraged' by PHP's developers and you must ".
"disable it to run Phabricator. Consult the PHP manual for instructions.");
}
require_once dirname(dirname(__FILE__)).'/conf/__init_conf__.php';
try {
$conf = phabricator_read_config_file($env);
$conf['phabricator.env'] = $env;
setup_aphront_basics();
phutil_require_module('phabricator', 'infrastructure/env');
PhabricatorEnv::setEnvConfig($conf);
phutil_require_module('phabricator', 'aphront/console/plugin/xhprof/api');
DarkConsoleXHProfPluginAPI::hookProfiler();
phutil_require_module('phabricator', 'aphront/console/plugin/errorlog/api');
PhutilErrorHandler::initialize();
} catch (Exception $ex) {
phabricator_fatal_config_error(
"[Exception] ".$ex->getMessage());
}
$tz = PhabricatorEnv::getEnvConfig('phabricator.timezone');
if ($tz) {
date_default_timezone_set($tz);
}
phutil_require_module('phabricator', 'aphront/console/plugin/errorlog/api');
phutil_require_module('phutil', 'error');
PhutilErrorHandler::setErrorListener(
array('DarkConsoleErrorLogPluginAPI', 'handleErrors'));
foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) {
phutil_load_library($library);
}
if (PhabricatorEnv::getEnvConfig('phabricator.setup')) {
PhabricatorSetup::runSetup();
return;
}
$host = $_SERVER['HTTP_HOST'];
$path = $_REQUEST['__path__'];
switch ($host) {
default:
$config_key = 'aphront.default-application-configuration-class';
$config_class = PhabricatorEnv::getEnvConfig($config_key);
PhutilSymbolLoader::loadClass($config_class);
$application = newv($config_class, array());
break;
}
$application->setHost($host);
$application->setPath($path);
$application->willBuildRequest();
$request = $application->buildRequest();
$application->setRequest($request);
list($controller, $uri_data) = $application->buildController();
try {
$response = $controller->willBeginExecution();
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->processRequest();
}
} catch (AphrontRedirectException $ex) {
$response = id(new AphrontRedirectResponse())
->setURI($ex->getURI());
} catch (Exception $ex) {
$response = $application->handleException($ex);
}
$response = $application->willSendResponse($response);
$response->setRequest($request);
$response_string = $response->buildResponseString();
$code = $response->getHTTPResponseCode();
if ($code != 200) {
header("HTTP/1.0 {$code}");
}
$headers = $response->getCacheHeaders();
$headers = array_merge($headers, $response->getHeaders());
foreach ($headers as $header) {
list($header, $value) = $header;
header("{$header}: {$value}");
}
// TODO: This shouldn't be possible in a production-configured environment.
if (isset($_REQUEST['__profile__']) &&
($_REQUEST['__profile__'] == 'all')) {
$profile = DarkConsoleXHProfPluginAPI::stopProfiler();
$profile =
'';
if (strpos($response_string, '') !== false) {
$response_string = str_replace(
'',
''.$profile,
$response_string);
} else {
echo $profile;
}
}
echo $response_string;
/**
* @group aphront
*/
function setup_aphront_basics() {
$aphront_root = dirname(dirname(__FILE__));
$libraries_root = dirname($aphront_root);
$root = null;
if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
$root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
}
ini_set('include_path', $libraries_root.':'.ini_get('include_path'));
@include_once $root.'libphutil/src/__phutil_library_init__.php';
if (!@constant('__LIBPHUTIL__')) {
echo "ERROR: Unable to load libphutil. Update your PHP 'include_path' to ".
"include the parent directory of libphutil/.\n";
exit(1);
}
// Load Phabricator itself using the absolute path, so we never end up doing
// anything surprising (loading index.php and libraries from different
// directories).
phutil_load_library($aphront_root.'/src');
phutil_load_library('arcanist/src');
}
function phabricator_fatal_config_error($msg) {
header('Content-Type: text/plain', $replace = true, $http_error = 500);
$error = "CONFIG ERROR: ".$msg."\n";
error_log($error);
echo $error;
die();
}