diff --git a/src/applications/differential/parser/changeset/DifferentialChangesetParser.php b/src/applications/differential/parser/changeset/DifferentialChangesetParser.php index cd684cfef8..58ba4810e4 100644 --- a/src/applications/differential/parser/changeset/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/changeset/DifferentialChangesetParser.php @@ -1,1342 +1,1347 @@ changeset = $changeset; $this->setFilename($changeset->getFilename()); $this->setChangesetID($changeset->getID()); return $this; } public function setWhitespaceMode($whitespace_mode) { $this->whitespaceMode = $whitespace_mode; return $this; } public function setOldChangesetID($old_changeset_id) { $this->oldChangesetID = $old_changeset_id; return $this; } public function setChangesetID($changeset_id) { $this->changesetID = $changeset_id; return $this; } + public function getChangeset() { + return $this->changeset; + } + public function getChangesetID() { return $this->changesetID; } public function setFilename($filename) { $this->filename = $filename; if (strpos($filename, '.', 1) !== false) { $parts = explode('.', $filename); $this->filetype = end($parts); } } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function setMarkupEngine(PhutilMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function parseHunk(DifferentialHunk $hunk) { $this->parsedHunk = true; $lines = $hunk->getChanges(); // Flatten UTF-8 into "\0". We don't support UTF-8 because the diffing // algorithms are byte-oriented (not character oriented) and everyone seems // to be in agreement that it's fairly reasonable not to allow UTF-8 in // source files. These bytes will later be replaced with a "?" glyph, but // in the meantime we replace them with "\0" since Pygments is happy to // deal with that. $lines = preg_replace('/[\x80-\xFF]/', "\0", $lines); $lines = str_replace( array("\t", "\r\n", "\r"), array(' ', "\n", "\n"), $lines); $lines = explode("\n", $lines); $types = array(); foreach ($lines as $line_index => $line) { $lines[$line_index] = $line; if (isset($line[0])) { $char = $line[0]; if ($char == ' ') { $types[$line_index] = null; } else if ($char == '\\' && $line_index > 0) { $types[$line_index] = $types[$line_index - 1]; } else { $types[$line_index] = $char; } } else { $types[$line_index] = null; } } $old_line = $hunk->getOldOffset(); $new_line = $hunk->getNewOffset(); $num_lines = count($lines); if ($old_line > 1) { $this->missingOld[$old_line] = true; } else if ($new_line > 1) { $this->missingNew[$new_line] = true; } for ($cursor = 0; $cursor < $num_lines; $cursor++) { $type = $types[$cursor]; $data = array( 'type' => $type, 'text' => (string)substr($lines[$cursor], 1), 'line' => $new_line, ); switch ($type) { case '+': $this->new[] = $data; ++$new_line; break; case '-': $data['line'] = $old_line; $this->old[] = $data; ++$old_line; break; default: $this->new[] = $data; $data['line'] = $old_line; $this->old[] = $data; ++$new_line; ++$old_line; break; } } } public function getDisplayLine($offset, $length) { $start = 1; for ($ii = $offset; $ii > 0; $ii--) { if ($this->new[$ii] && $this->new[$ii]['line']) { $start = $this->new[$ii]['line']; break; } } $end = $start; for ($ii = $offset + $length; $ii < count($this->new); $ii++) { if ($this->new[$ii] && $this->new[$ii]['line']) { $end = $this->new[$ii]['line']; break; } } return "{$start},{$end}"; } public function parseInlineComment(DifferentialInlineComment $comment) { $this->comments[] = $comment; return $this; } public function process() { $old = array(); $new = array(); $n = 0; $this->old = array_reverse($this->old); $this->new = array_reverse($this->new); $whitelines = false; $changed = false; $skip_intra = array(); while (count($this->old) || count($this->new)) { $o_desc = array_pop($this->old); $n_desc = array_pop($this->new); $oend = end($this->old); if ($oend) { $o_next = $oend['type']; } else { $o_next = null; } $nend = end($this->new); if ($nend) { $n_next = $nend['type']; } else { $n_next = null; } if ($o_desc) { $o_type = $o_desc['type']; } else { $o_type = null; } if ($n_desc) { $n_type = $n_desc['type']; } else { $n_type = null; } if (($o_type != null) && ($n_type == null)) { $old[] = $o_desc; $new[] = null; if ($n_desc) { array_push($this->new, $n_desc); } $changed = true; continue; } if (($n_type != null) && ($o_type == null)) { $old[] = null; $new[] = $n_desc; if ($o_desc) { array_push($this->old, $o_desc); } $changed = true; continue; } if ($this->whitespaceMode != self::WHITESPACE_SHOW_ALL) { $similar = false; switch ($this->whitespaceMode) { case self::WHITESPACE_IGNORE_TRAILING: if (rtrim($o_desc['text']) == rtrim($n_desc['text'])) { $similar = true; } break; } if ($similar) { $o_desc['type'] = null; $n_desc['type'] = null; $skip_intra[count($old)] = true; $whitelines = true; } else { $changed = true; } } else { $changed = true; } $old[] = $o_desc; $new[] = $n_desc; } $this->old = $old; $this->new = $new; if ($this->subparser && false) { // TODO: This is bugged // Use the subparser's side-by-side line information -- notably, the // change types -- but replace all the line text with ours. This lets us // render whitespace-only changes without marking them as different. $old = $this->subparser->old; $new = $this->subparser->new; $old_text = ipull($this->old, 'text', 'line'); $new_text = ipull($this->new, 'text', 'line'); foreach ($old as $k => $desc) { if (empty($desc)) { continue; } $old[$k]['text'] = idx($old_text, $desc['line']); } foreach ($new as $k => $desc) { if (empty($desc)) { continue; } $new[$k]['text'] = idx($new_text, $desc['line']); } $this->old = $old; $this->new = $new; } $min_length = min(count($this->old), count($this->new)); for ($ii = 0; $ii < $min_length; $ii++) { if ($this->old[$ii] || $this->new[$ii]) { if (isset($this->old[$ii]['text'])) { $otext = $this->old[$ii]['text']; } else { $otext = ''; } if (isset($this->new[$ii]['text'])) { $ntext = $this->new[$ii]['text']; } else { $ntext = ''; } if ($otext != $ntext && empty($skip_intra[$ii])) { $this->intra[$ii] = ArcanistDiffUtils::generateIntralineDiff( $otext, $ntext); } } } $lines_context = self::LINES_CONTEXT; $max_length = max(count($this->old), count($this->new)); $old = $this->old; $new = $this->new; $visible = false; $last = 0; for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) { $offset = $cursor + $lines_context; if ((isset($old[$offset]) && $old[$offset]['type']) || (isset($new[$offset]) && $new[$offset]['type'])) { $visible = true; $last = $offset; } else if ($cursor > $last + $lines_context) { $visible = false; } if ($visible && $cursor > 0) { $this->visible[$cursor] = 1; } } $old_corpus = ipull($this->old, 'text'); $old_corpus_block = implode("\n", $old_corpus); $new_corpus = ipull($this->new, 'text'); $new_corpus_block = implode("\n", $new_corpus); if ($this->noHighlight) { $this->oldRender = explode("\n", phutil_escape_html($old_corpus_block)); $this->newRender = explode("\n", phutil_escape_html($new_corpus_block)); } else { $this->oldRender = $this->sourceHighlight($this->old, $old_corpus_block); $this->newRender = $this->sourceHighlight($this->new, $new_corpus_block); } $this->applyIntraline( $this->oldRender, ipull($this->intra, 0), $old_corpus); $this->applyIntraline( $this->newRender, ipull($this->intra, 1), $new_corpus); $this->tokenHighlight($this->oldRender); $this->tokenHighlight($this->newRender); $unchanged = false; if ($this->subparser && false) { $unchanged = $this->subparser->isUnchanged(); $whitelines = $this->subparser->isWhitespaceOnly(); } else if (!$changed) { $filetype = $this->changeset->getFileType(); if ($filetype == DifferentialChangeType::FILE_TEXT || $filetype == DifferentialChangeType::FILE_SYMLINK) { $unchanged = true; } } $generated = (strpos($new_corpus_block, '@'.'generated') !== false); $this->specialAttributes = array( self::ATTR_GENERATED => $generated, self::ATTR_UNCHANGED => $unchanged, self::ATTR_DELETED => array_filter($this->old) && !array_filter($this->new), self::ATTR_WHITELINES => $whitelines ); } public function loadCache() { if (!$this->changesetID) { return false; } $data = null; $changeset = new DifferentialChangeset(); $conn_r = $changeset->establishConnection('r'); $data = queryfx_one( $conn_r, 'SELECT * FROM %T WHERE id = %d', $changeset->getTableName().'_parse_cache', $this->changesetID); if (!$data) { return false; } $data = json_decode($data['cache'], true); if (!is_array($data) || !$data) { return false; } foreach (self::getCacheableProperties() as $cache_key) { if (!array_key_exists($cache_key, $data)) { // If we're missing a cache key, assume we're looking at an old cache // and ignore it. return false; } } if ($data['cacheVersion'] !== self::CACHE_VERSION) { return false; } unset($data['cacheVersion'], $data['cacheHost']); $cache_prop = array_select_keys($data, self::getCacheableProperties()); foreach ($cache_prop as $cache_key => $v) { $this->$cache_key = $v; } return true; } protected static function getCacheableProperties() { return array( 'visible', 'new', 'old', 'intra', 'newRender', 'oldRender', 'specialAttributes', 'missingOld', 'missingNew', 'cacheVersion', 'cacheHost', ); } public function saveCache() { if (!$this->changesetID) { return false; } $cache = array(); foreach (self::getCacheableProperties() as $cache_key) { switch ($cache_key) { case 'cacheVersion': $cache[$cache_key] = self::CACHE_VERSION; break; case 'cacheHost': $cache[$cache_key] = php_uname('n'); break; default: $cache[$cache_key] = $this->$cache_key; break; } } $cache = json_encode($cache); try { $changeset = new DifferentialChangeset(); $conn_w = $changeset->establishConnection('w'); queryfx( $conn_w, 'INSERT INTO %T (id, cache) VALUES (%d, %s) ON DUPLICATE KEY UPDATE cache = VALUES(cache)', $changeset->getTableName().'_parse_cache', $this->changesetID, $cache); } catch (AphrontQueryException $ex) { // TODO: uhoh } } public function isGenerated() { return idx($this->specialAttributes, self::ATTR_GENERATED, false); } public function isDeleted() { return idx($this->specialAttributes, self::ATTR_DELETED, false); } public function isUnchanged() { return idx($this->specialAttributes, self::ATTR_UNCHANGED, false); } public function isWhitespaceOnly() { return idx($this->specialAttributes, self::ATTR_WHITELINES, false); } public function getLength() { return max(count($this->old), count($this->new)); } protected function applyIntraline(&$render, $intra, $corpus) { foreach ($render as $key => $text) { if (isset($intra[$key])) { $render[$key] = ArcanistDiffUtils::applyIntralineDiff( $text, $intra[$key]); } if (isset($corpus[$key]) && strlen($corpus[$key]) > 80) { $render[$key] = $this->lineWrap($render[$key]); } } } protected function lineWrap($l) { $c = 0; $len = strlen($l); $ins = array(); for ($ii = 0; $ii < $len; ++$ii) { if ($l[$ii] == '&') { do { ++$ii; } while ($l[$ii] != ';'); ++$c; } else if ($l[$ii] == '<') { do { ++$ii; } while ($l[$ii] != '>'); } else { ++$c; } if ($c == 80) { $ins[] = ($ii + 1); $c = 0; } } while (($pos = array_pop($ins))) { $l = substr_replace( $l, "\xE2\xAC\x85
", $pos, 0); } return $l; } protected function tokenHighlight(&$render) { foreach ($render as $key => $text) { $render[$key] = str_replace( "\0", ''."\xEF\xBF\xBD".'', $text); } } protected function sourceHighlight($data, $corpus) { $result = $this->highlightEngine->highlightSource( $this->filetype, $corpus); $result_lines = explode("\n", $result); foreach ($data as $key => $info) { if (!$info) { unset($result_lines[$key]); } } return $result_lines; } private function tryCacheStuff() { $whitespace_mode = $this->whitespaceMode; switch ($whitespace_mode) { case self::WHITESPACE_SHOW_ALL: case self::WHITESPACE_IGNORE_TRAILING: break; default: $whitespace_mode = self::WHITESPACE_IGNORE_ALL; break; } $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_ALL); $this->whitespaceMode = $whitespace_mode; $changeset = $this->changeset; if ($changeset->getFileType() == DifferentialChangeType::FILE_TEXT || $changeset->getFileType() == DifferentialChangeType::FILE_SYMLINK) { if ($skip_cache || !$this->loadCache()) { if ($this->whitespaceMode == self::WHITESPACE_IGNORE_ALL) { // Huge mess. Generate a "-bw" (ignore all whitespace changes) diff, // parse it out, and then play a shell game with the parsed format // in process() so we highlight only changed lines but render // whitespace differences. If we don't do this, we either fail to // render whitespace changes (which is incredibly confusing, // especially for python) or often produce a much larger set of // differences than necessary. $old_tmp = new TempFile(); $new_tmp = new TempFile(); Filesystem::writeFile($old_tmp, $changeset->makeOldFile()); Filesystem::writeFile($new_tmp, $changeset->makeNewFile()); list($err, $diff) = exec_manual( 'diff -bw -U65535 %s %s', $old_tmp, $new_tmp); if (!strlen($diff)) { // If there's no diff text, that means the files are identical // except for whitespace changes. Build a synthetic, changeless // diff. TODO: this is incredibly hacky. $entire_file = explode("\n", $changeset->makeOldFile()); foreach ($entire_file as $k => $line) { $entire_file[$k] = ' '.$line; } $len = count($entire_file); $entire_file = implode("\n", $entire_file); $diff = <<parseDiff($diff); $diff = DifferentialDiff::newFromRawChanges($changes); $changesets = $diff->getChangesets(); $alt_changeset = reset($changesets); $this->subparser = new DifferentialChangesetParser(); $this->subparser->setChangeset($alt_changeset); $this->subparser->setWhitespaceMode(self::WHITESPACE_IGNORE_TRAILING); } 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->tryCacheStuff(); $changeset_id = $this->changesetID; $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) { 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->isCommentInNewFile($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->isCommentInNewFile($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); } private function isCommentInNewFile(DifferentialInlineComment $comment) { if ($this->oldChangesetID) { return ($comment->getChangesetID() != $this->oldChangesetID); } else { return $comment->getIsNewFile(); } } protected function renderShield($message, $more) { - $end = $this->getLength(); - $changeset_id = $this->getChangesetID(); if ($more) { + $end = $this->getLength(); + $reference = $this->getChangeset()->getRenderingReference(); $more = ' '. javelin_render_tag( 'a', array( 'mustcapture' => true, 'sigil' => 'show-more', 'class' => 'complete', 'href' => '#', 'meta' => array( - 'id' => $changeset_id, + 'id' => $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 = 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); $changeset = $this->changesetID; + $reference = $this->getChangeset()->getRenderingReference(); for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) { if (empty($mask[$ii])) { $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( - 'id' => $changeset, + 'id' => $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( - 'id' => $changeset, + 'id' => $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( - 'id' => $changeset, + 'id' => $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) { + if ($o_num && $changeset) { $o_id = ' id="C'.$changeset.'OL'.$o_num.'"'; } else { $o_id = null; } - if ($n_num) { + if ($n_num && $changeset) { $n_id = ' id="C'.$changeset.'NL'.$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->isCommentInNewFile($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( ); } } return $table; */ } protected function renderChangesetTable($changeset, $contents) { $props = $this->renderPropertyChangeHeader($this->changeset); $table = null; if ($contents) { $table = '
Property Changes Old Value New Value
{$key} {$oval} {$nval}
'. $contents. '
'; } 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/storage/changeset/DifferentialChangeset.php b/src/applications/differential/storage/changeset/DifferentialChangeset.php index 9fde61affb..14c2bb8e8c 100644 --- a/src/applications/differential/storage/changeset/DifferentialChangeset.php +++ b/src/applications/differential/storage/changeset/DifferentialChangeset.php @@ -1,147 +1,160 @@ array( 'metadata' => self::SERIALIZATION_JSON, 'oldProperties' => self::SERIALIZATION_JSON, 'newProperties' => self::SERIALIZATION_JSON, 'awayPaths' => self::SERIALIZATION_JSON, )) + parent::getConfiguration(); } public function getAffectedLineCount() { return $this->getAddLines() + $this->getDelLines(); } public function getFileType() { return $this->fileType; } public function getChangeType() { return $this->changeType; } public function attachHunks(array $hunks) { $this->hunks = $hunks; return $this; } public function getHunks() { if ($this->hunks === null) { throw new Exception("Must load and attach hunks first!"); } return $this->hunks; } public function getDisplayFilename() { $name = $this->getFilename(); if ($this->getFileType() == DifferentialChangeType::FILE_DIRECTORY) { $name .= '/'; } return $name; } + public function setRenderingReference($rendering_reference) { + $this->renderingReference = $rendering_reference; + return $this; + } + + public function getRenderingReference() { + if ($this->renderingReference) { + return $this->renderingReference; + } + return $this->getID(); + } + public function addUnsavedHunk(DifferentialHunk $hunk) { if ($this->hunks === null) { $this->hunks = array(); } $this->hunks[] = $hunk; $this->unsavedHunks[] = $hunk; return $this; } public function loadHunks() { if (!$this->getID()) { return array(); } return id(new DifferentialHunk())->loadAllWhere( 'changesetID = %d', $this->getID()); } public function save() { // TODO: Sort out transactions // $this->openTransaction(); $ret = parent::save(); foreach ($this->unsavedHunks as $hunk) { $hunk->setChangesetID($this->getID()); $hunk->save(); } // $this->saveTransaction(); return $ret; } public function delete() { // $this->openTransaction(); foreach ($this->loadHunks() as $hunk) { $hunk->delete(); } $this->_hunks = array(); $ret = parent::delete(); // $this->saveTransaction(); return $ret; } public function getSortKey() { $sort_key = $this->getFilename(); // Sort files with ".h" in them first, so headers (.h, .hpp) come before // implementations (.c, .cpp, .cs). $sort_key = str_replace('.h', '.!h', $sort_key); return $sort_key; } public function makeNewFile() { $file = array(); foreach ($this->getHunks() as $hunk) { $file[] = $hunk->makeNewFile(); } return implode("\n", $file); } public function makeOldFile() { $file = array(); foreach ($this->getHunks() as $hunk) { $file[] = $hunk->makeOldFile(); } return implode("\n", $file); } public function getAnchorName() { return substr(md5($this->getFilename()), 0, 8); } } diff --git a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php index 66fa6dd472..1dc86e05d8 100644 --- a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php +++ b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php @@ -1,134 +1,136 @@ changesets = $changesets; return $this; } public function setEditable($editable) { $this->editable = $editable; return $this; } public function setRevision(DifferentialRevision $revision) { $this->revision = $revision; return $this; } public function setVsMap(array $vs_map) { $this->vsMap = $vs_map; return $this; } public function setRenderURI($render_uri) { $this->renderURI = $render_uri; return $this; } public function render() { require_celerity_resource('differential-changeset-view-css'); $vs_map = $this->vsMap; $changesets = $this->changesets; $output = array(); $mapping = array(); foreach ($changesets as $key => $changeset) { $file = $changeset->getFilename(); $class = 'differential-changeset'; if (!$this->editable) { $class .= ' differential-changeset-noneditable'; } $id = $changeset->getID(); if ($id) { $vs_id = idx($vs_map, $id); } else { $vs_id = null; } - $detail_uri = new PhutilURI('/differential/changeset/'); + $ref = $changeset->getRenderingReference(); + + $detail_uri = new PhutilURI($this->renderURI); $detail_uri->setQueryParams( array( - 'id' => $id, + 'id' => $ref, 'vs' => $vs_id, 'whitespace' => 'TODO', )); $detail_button = phutil_render_tag( 'a', array( 'style' => 'float: right', 'class' => 'button small grey', 'href' => $detail_uri, 'target' => '_blank', ), 'Standalone View'); $uniq_id = celerity_generate_unique_node_id(); $detail = new DifferentialChangesetDetailView(); $detail->setChangeset($changeset); $detail->addButton($detail_button); $detail->appendChild( phutil_render_tag( 'div', array( 'id' => $uniq_id, ), '
Loading...
')); $output[] = $detail->render(); $mapping[$uniq_id] = array( - $changeset->getID(), + $ref, $vs_id); } $whitespace = null; Javelin::initBehavior('differential-populate', array( 'registry' => $mapping, 'whitespace' => $whitespace, 'uri' => $this->renderURI, )); Javelin::initBehavior('differential-show-more', array( 'uri' => $this->renderURI, )); if ($this->editable) { $revision = $this->revision; Javelin::initBehavior('differential-edit-inline-comments', array( 'uri' => '/differential/comment/inline/edit/'.$revision->getID().'/', )); } return '
'. implode("\n", $output). '
'; } } diff --git a/src/applications/diffusion/controller/commit/DiffusionCommitController.php b/src/applications/diffusion/controller/commit/DiffusionCommitController.php index 0c62de6e0a..0c7f4ecfc8 100644 --- a/src/applications/diffusion/controller/commit/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/commit/DiffusionCommitController.php @@ -1,119 +1,146 @@ getDiffusionRequest(); + $callsign = $drequest->getRepository()->getCallsign(); + $content = array(); $content[] = $this->buildCrumbs(array( 'commit' => true, )); $detail_panel = new AphrontPanelView(); $repository = $drequest->getRepository(); $commit = $drequest->loadCommit(); if (!$commit) { // TODO: Make more user-friendly. throw new Exception('This commit has not parsed yet.'); } $commit_data = $drequest->loadCommitData(); require_celerity_resource('diffusion-commit-view-css'); $detail_panel->appendChild( '
'. ''. '

Revision Detail

'. '
'. ''. ''. ''. ''. ''. '
Author:'.phutil_escape_html($commit_data->getAuthorName()).'
'. '
'. '
'. phutil_escape_html($commit_data->getCommitMessage()). '
'. '
'. '
'); $content[] = $detail_panel; $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $changes = $change_query->loadChanges(); $change_table = new DiffusionCommitChangeTableView(); $change_table->setDiffusionRequest($drequest); $change_table->setPathChanges($changes); // TODO: Large number of modified files check. $count = number_format(count($changes)); $bad_commit = null; if ($count == 0) { $bad_commit = queryfx_one( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE fullCommitName = %s', PhabricatorRepository::TABLE_BADCOMMIT, - 'r'.$repository->getCallsign().$commit->getCommitIdentifier()); + 'r'.$callsign.$commit->getCommitIdentifier()); } if ($bad_commit) { $error_panel = new AphrontErrorView(); $error_panel->setWidth(AphrontErrorView::WIDTH_WIDE); $error_panel->setTitle('Bad Commit'); $error_panel->appendChild( phutil_escape_html($bad_commit['description'])); $content[] = $error_panel; } else { $change_panel = new AphrontPanelView(); $change_panel->setHeader("Changes ({$count})"); $change_panel->appendChild($change_table); $content[] = $change_panel; - $change_list = - '
'. - '(list of changes goes here)'. - '
'; + if ($changes) { + $changesets = DiffusionPathChange::convertToDifferentialChangesets( + $changes); + foreach ($changesets as $changeset) { + $branch = $drequest->getBranchURIComponent( + $drequest->getBranch()); + $filename = $changeset->getFilename(); + $commit = $drequest->getCommit(); + $reference = "{$branch}{$filename};{$commit}"; + $changeset->setRenderingReference($reference); + } + + $change_list = new DifferentialChangesetListView(); + $change_list->setChangesets($changesets); + $change_list->setRenderURI('/diffusion/'.$callsign.'/diff/'); + + // TODO: This is pretty awkward, unify the CSS between Diffusion and + // Differential better. + require_celerity_resource('differential-core-view-css'); + $change_list = + '
'. + $change_list->render(). + '
'; + } else { + $change_list = + '
'. + '(no changes blah blah)'. + '
'; + } $content[] = $change_list; } return $this->buildStandardPageResponse( $content, array( 'title' => 'Diffusion', )); } } diff --git a/src/applications/diffusion/controller/commit/__init__.php b/src/applications/diffusion/controller/commit/__init__.php index 6e629ab204..36ce108d3e 100644 --- a/src/applications/diffusion/controller/commit/__init__.php +++ b/src/applications/diffusion/controller/commit/__init__.php @@ -1,22 +1,24 @@ path = $path; return $this; } final public function getPath() { return $this->path; } public function setChangeType($change_type) { $this->changeType = $change_type; return $this; } public function getChangeType() { return $this->changeType; } public function setFileType($file_type) { $this->fileType = $file_type; return $this; } public function getFileType() { return $this->fileType; } public function setTargetPath($target_path) { $this->targetPath = $target_path; return $this; } public function getTargetPath() { return $this->targetPath; } public function setAwayPaths(array $away_paths) { $this->awayPaths = $away_paths; return $this; } public function getAwayPaths() { return $this->awayPaths; } final public function setCommitIdentifier($commit) { $this->commitIdentifier = $commit; return $this; } final public function getCommitIdentifier() { return $this->commitIdentifier; } final public function setCommit($commit) { $this->commit = $commit; return $this; } final public function getCommit() { return $this->commit; } final public function setCommitData($commit_data) { $this->commitData = $commit_data; return $this; } final public function getCommitData() { return $this->commitData; } final public function getEpoch() { if ($this->getCommit()) { return $this->getCommit()->getEpoch(); } return null; } final public function getAuthorName() { if ($this->getCommitData()) { return $this->getCommitData()->getAuthorName(); } return null; } final public function getSummary() { if (!$this->getCommitData()) { return null; } $message = $this->getCommitData()->getCommitMessage(); $first = idx(explode("\n", $message), 0); return substr($first, 0, 80); } + final public static function convertToArcanistChanges(array $changes) { + $direct = array(); + $result = array(); + foreach ($changes as $path) { + $change = new ArcanistDiffChange(); + $change->setCurrentPath($path->getPath()); + $direct[] = $path->getPath(); + $change->setType($path->getChangeType()); + $file_type = $path->getFileType(); + if ($file_type == DifferentialChangeType::FILE_NORMAL) { + $file_type = DifferentialChangeType::FILE_TEXT; + } + $change->setFileType($file_type); + $change->setOldPath($path->getTargetPath()); + foreach ($path->getAwayPaths() as $away_path) { + $change->addAwayPath($away_path); + } + $result[$path->getPath()] = $change; + } + + return array_select_keys($result, $direct); + } + + final public static function convertToDifferentialChangesets(array $changes) { + $arcanist_changes = self::convertToArcanistChanges($changes); + $diff = DifferentialDiff::newFromRawChanges($arcanist_changes); + return $diff->getChangesets(); + } } diff --git a/src/applications/diffusion/data/pathchange/__init__.php b/src/applications/diffusion/data/pathchange/__init__.php index 0256f567bf..46cfbb20fa 100644 --- a/src/applications/diffusion/data/pathchange/__init__.php +++ b/src/applications/diffusion/data/pathchange/__init__.php @@ -1,12 +1,17 @@ getRequest(); $path_change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $path_changes = $path_change_query->loadChanges(); $path = null; foreach ($path_changes as $change) { if ($change->getPath() == $drequest->getPath()) { $path = $change; } } if (!$path) { return null; } $change_type = $path->getChangeType(); switch ($change_type) { case DifferentialChangeType::TYPE_MULTICOPY: case DifferentialChangeType::TYPE_DELETE: if ($path->getTargetPath()) { $old = array( $path->getTargetPath(), $path->getTargetCommitIdentifier()); } else { $old = array($path->getPath(), $path->getCommitIdentifier() - 1); } $old_name = $path->getPath(); $new_name = ''; $new = null; break; case DifferentialChangeType::TYPE_ADD: $old = null; $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = ''; $new_name = $path->getPath(); break; case DifferentialChangeType::TYPE_MOVE_HERE: case DifferentialChangeType::TYPE_COPY_HERE: $old = array( $path->getTargetPath(), $path->getTargetCommitIdentifier()); $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = $path->getTargetPath(); $new_name = $path->getPath(); break; default: $old = array($path->getPath(), $path->getCommitIdentifier() - 1); $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = $path->getPath(); $new_name = $path->getPath(); break; } $futures = array( 'old' => $this->buildContentFuture($old), 'new' => $this->buildContentFuture($new), ); $futures = array_filter($futures); foreach (Futures($futures) as $key => $future) { $futures[$key] = $future->resolvex(); } $old_data = idx($futures, 'old', ''); $new_data = idx($futures, 'new', ''); $old_tmp = new TempFile(); $new_tmp = new TempFile(); Filesystem::writeFile($old_tmp, $old_data); Filesystem::writeFile($new_tmp, $new_data); list($err, $raw_diff) = exec_manual( 'diff -L %s -L %s -U65535 %s %s', nonempty($old_name, '/dev/universe').' 9999-99-99', nonempty($new_name, '/dev/universe').' 9999-99-99', $old_tmp, $new_tmp); $parser = new ArcanistDiffParser(); $parser->setDetectBinaryFiles(true); $change = $parser->parseDiffusionPathChangesAndRawDiff( $drequest->getPath(), $path_changes, $raw_diff); $diff = DifferentialDiff::newFromRawChanges(array($change)); $changesets = $diff->getChangesets(); $changeset = reset($changesets); - $id = $drequest->getPath().';'.$drequest->getCommit(); - $changeset->setID($id); + $reference = $drequest->getPath().';'.$drequest->getCommit(); + $changeset->setRenderingReference($reference); return $changeset; } private function buildContentFuture($spec) { if (!$spec) { return null; } $drequest = $this->getRequest(); $repository = $drequest->getRepository(); list($ref, $rev) = $spec; return new ExecFuture( 'svn --non-interactive cat %s%s@%d', $repository->getDetail('remote-uri'), $ref, $rev); } }