/**
  * Assert that two values are equal. The test fails if they are not.
  *
  * NOTE: This method uses PHP's strict equality test operator ("===") to
  * compare values. This means values and types must be equal, key order must
  * be identical in arrays, and objects must be referentially identical.
  *
  * @param wild    The theoretically expected value, generated by careful
  *                reasoning about the properties of the system.
  * @param wild    The empirically derived value, generated by executing the
  *                test.
  * @param string  A human-readable description of what these values represent,
  *                and particularly of what a discrepancy means.
  *
  * @return void
  * @task assert
  */
 protected final function assertEqual($expect, $result, $message = null)
 {
     if ($expect === $result) {
         $this->assertions++;
         return;
     }
     $expect = PhutilReadableSerializer::printableValue($expect);
     $result = PhutilReadableSerializer::printableValue($result);
     $where = debug_backtrace();
     $where = array_shift($where);
     $line = idx($where, 'line');
     $file = basename(idx($where, 'file'));
     $output = "Assertion failed at line {$line} in {$file}";
     if ($message) {
         $output .= ": {$message}";
     }
     $output .= "\n";
     if (strpos($expect, "\n") === false && strpos($result, "\n") === false) {
         $output .= "Expected: {$expect}\n";
         $output .= "Actual: {$result}";
     } else {
         $output .= "Expected vs Actual Output Diff\n";
         $output .= ArcanistDiffUtils::renderDifferences($expect, $result, $lines = 0xffff);
     }
     $this->failTest($output);
     throw new ArcanistPhutilTestTerminatedException($output);
 }
 public function testLevenshtein()
 {
     $tests = array(array('a', 'b', 'x'), array('kalrmr(array($b))', 'array($b)', 'dddddddssssssssds'), array('array($b)', 'kalrmr(array($b))', 'iiiiiiissssssssis'), array('zkalrmr(array($b))z', 'xarray($b)x', 'dddddddxsssssssssdx'), array('xarray($b)x', 'zkalrmr(array($b))z', 'iiiiiiixsssssssssix'), array('abcdefghi', 'abcdefghi', 'sssssssss'), array('abcdefghi', 'abcdefghijkl', 'sssssssssiii'), array('abcdefghijkl', 'abcdefghi', 'sssssssssddd'), array('xyzabcdefghi', 'abcdefghi', 'dddsssssssss'), array('abcdefghi', 'xyzabcdefghi', 'iiisssssssss'), array('abcdefg', 'abxdxfg', 'ssxsxss'), array('private function a($a, $b) {', 'public function and($b, $c) {', 'siixsdddxsssssssssssiissxsssxsss'), array('        if (' . 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' . 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' . 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) {', '        if(' . 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' . 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' . 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) {', 'ssssssssssds' . 'ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss' . 'ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss' . 'sssssssssssssssssssssssssssssssssssssss'));
     foreach ($tests as $test) {
         $this->assertEqual($test[2], ArcanistDiffUtils::buildLevenshteinDifferenceString($test[0], $test[1]));
     }
 }
 public function testGenerateUTF8IntralineDiff()
 {
     // Both Strings Empty.
     $left = '';
     $right = '';
     $result = array(array(array(0, 0)), array(array(0, 0)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // Left String Empty.
     $left = '';
     $right = "Grumpy☃at";
     $result = array(array(array(0, 0)), array(array(0, 11)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // Right String Empty.
     $left = "Grumpy☃at";
     $right = '';
     $result = array(array(array(0, 11)), array(array(0, 0)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // Both Strings Same
     $left = "Grumpy☃at";
     $right = "Grumpy☃at";
     $result = array(array(array(0, 11)), array(array(0, 11)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // Both Strings are different.
     $left = "Grumpy☃at";
     $right = 'Smiling Dog';
     $result = array(array(array(1, 11)), array(array(1, 11)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // String with one difference in the middle.
     $left = 'GrumpyCat';
     $right = "Grumpy☃at";
     $result = array(array(array(0, 6), array(1, 1), array(0, 2)), array(array(0, 6), array(1, 3), array(0, 2)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // Differences in middle, not connected to each other.
     $left = 'GrumpyCat';
     $right = "Grumpy☃a☃t";
     $result = array(array(array(0, 6), array(1, 2), array(0, 1)), array(array(0, 6), array(1, 7), array(0, 1)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // String with difference at the beginning.
     $left = "GrumpyC☃t";
     $right = "DrumpyC☃t";
     $result = array(array(array(1, 1), array(0, 10)), array(array(1, 1), array(0, 10)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // String with difference at the end.
     $left = "GrumpyC☃t";
     $right = "GrumpyC☃P";
     $result = array(array(array(0, 10), array(1, 1)), array(array(0, 10), array(1, 1)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // String with differences at the beginning and end.
     $left = "GrumpyC☃t";
     $right = "DrumpyC☃P";
     $result = array(array(array(1, 1), array(0, 9), array(1, 1)), array(array(1, 1), array(0, 9), array(1, 1)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
     // This is a unicode combining character, "COMBINING DOUBLE TILDE".
     $cc = "͠";
     $left = 'Senor';
     $right = "Sen{$cc}or";
     $result = array(array(array(0, 2), array(1, 1), array(0, 2)), array(array(0, 2), array(1, 3), array(0, 2)));
     $this->assertEqual($result, ArcanistDiffUtils::generateIntralineDiff($left, $right));
 }
 public final function lintPath($path)
 {
     if (!isset($this->rawLintOutput[$path])) {
         return;
     }
     list($new_content) = $this->rawLintOutput[$path];
     $old_content = $this->getData($path);
     if ($new_content != $old_content) {
         $diff = ArcanistDiffUtils::renderDifferences($old_content, $new_content);
         $this->raiseLintAtOffset(0, self::LINT_FORMATTING, $this->getLintMessage($diff), $old_content, $new_content);
     }
 }
 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);
             $corpus_table = phutil_render_tag('table', array('class' => "diffusion-source remarkup-code PhabricatorMonospaced"), implode("\n", $rows));
             $corpus = phutil_render_tag('div', array('style' => 'padding: 0 2em;'), $corpus_table);
             break;
     }
     return $corpus;
 }
示例#6
0
 protected function generateChanges()
 {
     $parser = $this->newDiffParser();
     $is_raw = $this->isRawDiffSource();
     if ($is_raw) {
         if ($this->getArgument('raw')) {
             file_put_contents('php://stderr', "Reading diff from stdin...\n");
             $raw_diff = file_get_contents('php://stdin');
         } else {
             if ($this->getArgument('raw-command')) {
                 list($raw_diff) = execx($this->getArgument('raw-command'));
             } else {
                 throw new Exception("Unknown raw diff source.");
             }
         }
         $changes = $parser->parseDiff($raw_diff);
         foreach ($changes as $key => $change) {
             // Remove "message" changes, e.g. from "git show".
             if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
                 unset($changes[$key]);
             }
         }
         return $changes;
     }
     $repository_api = $this->getRepositoryAPI();
     if ($repository_api instanceof ArcanistSubversionAPI) {
         $paths = $this->generateAffectedPaths();
         $this->primeSubversionWorkingCopyData($paths);
         // Check to make sure the user is diffing from a consistent base revision.
         // This is mostly just an abuse sanity check because it's silly to do this
         // and makes the code more difficult to effectively review, but it also
         // affects patches and makes them nonportable.
         $bases = $repository_api->getSVNBaseRevisions();
         // Remove all files with baserev "0"; these files are new.
         foreach ($bases as $path => $baserev) {
             if ($bases[$path] <= 0) {
                 unset($bases[$path]);
             }
         }
         if ($bases) {
             $rev = reset($bases);
             $revlist = array();
             foreach ($bases as $path => $baserev) {
                 $revlist[] = "    Revision {$baserev}, {$path}";
             }
             $revlist = implode("\n", $revlist);
             foreach ($bases as $path => $baserev) {
                 if ($baserev !== $rev) {
                     throw new ArcanistUsageException("Base revisions of changed paths are mismatched. Update all " . "paths to the same base revision before creating a diff: " . "\n\n" . $revlist);
                 }
             }
             // If you have a change which affects several files, all of which are
             // at a consistent base revision, treat that revision as the effective
             // base revision. The use case here is that you made a change to some
             // file, which updates it to HEAD, but want to be able to change it
             // again without updating the entire working copy. This is a little
             // sketchy but it arises in Facebook Ops workflows with config files and
             // doesn't have any real material tradeoffs (e.g., these patches are
             // perfectly applyable).
             $repository_api->overrideSVNBaseRevisionNumber($rev);
         }
         $changes = $parser->parseSubversionDiff($repository_api, $paths);
     } else {
         if ($repository_api instanceof ArcanistGitAPI) {
             $diff = $repository_api->getFullGitDiff();
             if (!strlen($diff)) {
                 throw new ArcanistUsageException("No changes found. (Did you specify the wrong commit range?)");
             }
             $changes = $parser->parseDiff($diff);
         } else {
             if ($repository_api instanceof ArcanistMercurialAPI) {
                 $diff = $repository_api->getFullMercurialDiff();
                 if (!strlen($diff)) {
                     throw new ArcanistUsageException("No changes found. (Did you specify the wrong commit range?)");
                 }
                 $changes = $parser->parseDiff($diff);
             } else {
                 throw new Exception("Repository API is not supported.");
             }
         }
     }
     if (count($changes) > 250) {
         $count = number_format(count($changes));
         $message = "This diff has a very large number of changes ({$count}). " . "Differential works best for changes which will receive detailed " . "human review, and not as well for large automated changes or " . "bulk checkins. Continue anyway?";
         if (!phutil_console_confirm($message)) {
             throw new ArcanistUsageException("Aborted generation of gigantic diff.");
         }
     }
     $limit = 1024 * 1024 * 4;
     foreach ($changes as $change) {
         $size = 0;
         foreach ($change->getHunks() as $hunk) {
             $size += strlen($hunk->getCorpus());
         }
         if ($size > $limit) {
             $file_name = $change->getCurrentPath();
             $change_size = number_format($size);
             $byte_warning = "Diff for '{$file_name}' with context is {$change_size} bytes in " . "length. Generally, source changes should not be this large.";
             if (!$this->getArgument('less-context')) {
                 $byte_warning .= " If this file is a huge text file, try using the " . "'--less-context' flag.";
             }
             if ($repository_api instanceof ArcanistSubversionAPI) {
                 throw new ArcanistUsageException("{$byte_warning} If the file is not a text file, mark it as " . "binary with:" . "\n\n" . "  \$ svn propset svn:mime-type application/octet-stream <filename>" . "\n");
             } else {
                 $confirm = "{$byte_warning} If the file is not a text file, you can " . "mark it 'binary'. Mark this file as 'binary' and continue?";
                 if (phutil_console_confirm($confirm)) {
                     $change->convertToBinaryChange();
                 } else {
                     throw new ArcanistUsageException("Aborted generation of gigantic diff.");
                 }
             }
         }
     }
     $try_encoding = nonempty($this->getArgument('encoding'), null);
     $utf8_problems = array();
     foreach ($changes as $change) {
         foreach ($change->getHunks() as $hunk) {
             $corpus = $hunk->getCorpus();
             if (!phutil_is_utf8($corpus)) {
                 // If this corpus is heuristically binary, don't try to convert it.
                 // mb_check_encoding() and mb_convert_encoding() are both very very
                 // liberal about what they're willing to process.
                 $is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus);
                 if (!$is_binary) {
                     if (!$try_encoding) {
                         try {
                             $try_encoding = $this->getRepositoryEncoding();
                         } catch (ConduitClientException $e) {
                             if ($e->getErrorCode() == 'ERR-BAD-ARCANIST-PROJECT') {
                                 echo phutil_console_wrap("Lookup of encoding in arcanist project failed\n" . $e->getMessage());
                             } else {
                                 throw $e;
                             }
                         }
                     }
                     if ($try_encoding && $try_encoding != 'UTF-8') {
                         if (!function_exists('mb_convert_encoding')) {
                             throw new ArcanistUsageException("This diff includes a file encoded in '{$try_encoding}', " . "but you don't have the PHP mbstring extension installed " . "so it can't be converted to UTF-8. Install mbstring.");
                         }
                         $corpus = mb_convert_encoding($corpus, 'UTF-8', $try_encoding);
                         $name = $change->getCurrentPath();
                         if (phutil_is_utf8($corpus)) {
                             $this->writeStatusMessage("Converted a '{$name}' hunk from '{$try_encoding}' " . "to UTF-8.\n");
                             $hunk->setCorpus($corpus);
                             continue;
                         }
                     }
                 }
                 $utf8_problems[] = $change;
                 break;
             }
         }
     }
     // If there are non-binary files which aren't valid UTF-8, warn the user
     // and treat them as binary changes. See D327 for discussion of why Arcanist
     // has this behavior.
     if ($utf8_problems) {
         $utf8_warning = pht("This diff includes file(s) which are not valid UTF-8 (they contain " . "invalid byte sequences). You can either stop this workflow and " . "fix these files, or continue. If you continue, these files will " . "be marked as binary.", count($utf8_problems)) . "\n\n" . "You can learn more about how Phabricator handles character encodings " . "(and how to configure encoding settings and detect and correct " . "encoding problems) by reading 'User Guide: UTF-8 and Character " . "Encoding' in the Phabricator documentation.\n\n";
         "    " . pht('AFFECTED FILE(S)', count($utf8_problems)) . "\n";
         $confirm = pht('Do you want to mark these files as binary and continue?', count($utf8_problems));
         echo phutil_console_format("**Invalid Content Encoding (Non-UTF8)**\n");
         echo phutil_console_wrap($utf8_warning);
         $file_list = mpull($utf8_problems, 'getCurrentPath');
         $file_list = '    ' . implode("\n    ", $file_list);
         echo $file_list;
         if (!phutil_console_confirm($confirm, $default_no = false)) {
             throw new ArcanistUsageException("Aborted workflow to fix UTF-8.");
         } else {
             foreach ($utf8_problems as $change) {
                 $change->convertToBinaryChange();
             }
         }
     }
     foreach ($changes as $change) {
         $path = $change->getCurrentPath();
         // Certain types of changes (moves and copies) don't contain change data
         // when expressed in raw "git diff" form. Augment any such diffs with
         // textual data.
         if ($change->getNeedsSyntheticGitHunks()) {
             $diff = $repository_api->getRawDiffText($path, $moves = false);
             $parser = $this->newDiffParser();
             $raw_changes = $parser->parseDiff($diff);
             foreach ($raw_changes as $raw_change) {
                 if ($raw_change->getCurrentPath() == $path) {
                     $change->setFileType($raw_change->getFileType());
                     foreach ($raw_change->getHunks() as $hunk) {
                         $change->addHunk($hunk);
                     }
                     break;
                 }
             }
             $change->setNeedsSyntheticGitHunks(false);
         }
         if ($change->getFileType() != ArcanistDiffChangeType::FILE_BINARY) {
             continue;
         }
         $name = basename($path);
         $old_file = $repository_api->getOriginalFileData($path);
         $old_dict = $this->uploadFile($old_file, $name, 'old binary');
         if ($old_dict['guid']) {
             $change->setMetadata('old:binary-phid', $old_dict['guid']);
         }
         $change->setMetadata('old:file:size', $old_dict['size']);
         $change->setMetadata('old:file:mime-type', $old_dict['mime']);
         $new_file = $repository_api->getCurrentFileData($path);
         $new_dict = $this->uploadFile($new_file, $name, 'new binary');
         if ($new_dict['guid']) {
             $change->setMetadata('new:binary-phid', $new_dict['guid']);
         }
         $change->setMetadata('new:file:size', $new_dict['size']);
         $change->setMetadata('new:file:mime-type', $new_dict['mime']);
         if (preg_match('@^image/@', $new_dict['mime'])) {
             $change->setFileType(ArcanistDiffChangeType::FILE_IMAGE);
         }
     }
     return $changes;
 }
 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]) > $this->lineWidth) {
             $render[$key] = $this->lineWrap($render[$key]);
         }
     }
 }
 private function browseFile()
 {
     $viewer = $this->getViewer();
     $request = $this->getRequest();
     $drequest = $this->getDiffusionRequest();
     $repository = $drequest->getRepository();
     $before = $request->getStr('before');
     if ($before) {
         return $this->buildBeforeResponse($before);
     }
     $path = $drequest->getPath();
     $preferences = $viewer->loadPreferences();
     $show_blame = $request->getBool('blame', $preferences->getPreference(PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME, false));
     $show_color = $request->getBool('color', $preferences->getPreference(PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR, true));
     $view = $request->getStr('view');
     if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) {
         $preferences->setPreference(PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME, $show_blame);
         $preferences->setPreference(PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR, $show_color);
         $preferences->save();
         $uri = $request->getRequestURI()->alter('blame', null)->alter('color', null);
         return id(new AphrontRedirectResponse())->setURI($uri);
     }
     // We need the blame information if blame is on and we're building plain
     // text, or blame is on and this is an Ajax request. If blame is on and
     // this is a colorized request, we don't show blame at first (we ajax it
     // in afterward) so we don't need to query for it.
     $needs_blame = $show_blame && !$show_color || $show_blame && $request->isAjax();
     $params = array('commit' => $drequest->getCommit(), 'path' => $drequest->getPath());
     $byte_limit = null;
     if ($view !== 'raw') {
         $byte_limit = PhabricatorFileStorageEngine::getChunkThreshold();
         $time_limit = 10;
         $params += array('timeout' => $time_limit, 'byteLimit' => $byte_limit);
     }
     $response = $this->callConduitWithDiffusionRequest('diffusion.filecontentquery', $params);
     $hit_byte_limit = $response['tooHuge'];
     $hit_time_limit = $response['tooSlow'];
     $file_phid = $response['filePHID'];
     if ($hit_byte_limit) {
         $corpus = $this->buildErrorCorpus(pht('This file is larger than %s byte(s), and too large to display ' . 'in the web UI.', phutil_format_bytes($byte_limit)));
     } else {
         if ($hit_time_limit) {
             $corpus = $this->buildErrorCorpus(pht('This file took too long to load from the repository (more than ' . '%s second(s)).', new PhutilNumber($time_limit)));
         } else {
             $file = id(new PhabricatorFileQuery())->setViewer($viewer)->withPHIDs(array($file_phid))->executeOne();
             if (!$file) {
                 throw new Exception(pht('Failed to load content file!'));
             }
             if ($view === 'raw') {
                 return $file->getRedirectResponse();
             }
             $data = $file->loadFileData();
             if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) {
                 $file_uri = $file->getBestURI();
                 if ($file->isViewableImage()) {
                     $corpus = $this->buildImageCorpus($file_uri);
                 } else {
                     $corpus = $this->buildBinaryCorpus($file_uri, $data);
                 }
             } else {
                 $this->loadLintMessages();
                 $this->coverage = $drequest->loadCoverage();
                 // Build the content of the file.
                 $corpus = $this->buildCorpus($show_blame, $show_color, $data, $needs_blame, $drequest, $path, $data);
             }
         }
     }
     if ($request->isAjax()) {
         return id(new AphrontAjaxResponse())->setContent($corpus);
     }
     require_celerity_resource('diffusion-source-css');
     // Render the page.
     $view = $this->buildActionView($drequest);
     $action_list = $this->enrichActionView($view, $drequest, $show_blame, $show_color);
     $properties = $this->buildPropertyView($drequest, $action_list);
     $object_box = id(new PHUIObjectBoxView())->setHeader($this->buildHeaderView($drequest))->addPropertyList($properties);
     $content = array();
     $content[] = $object_box;
     $follow = $request->getStr('follow');
     if ($follow) {
         $notice = new PHUIInfoView();
         $notice->setSeverity(PHUIInfoView::SEVERITY_WARNING);
         $notice->setTitle(pht('Unable to Continue'));
         switch ($follow) {
             case 'first':
                 $notice->appendChild(pht('Unable to continue tracing the history of this file because ' . 'this commit is the first commit in the repository.'));
                 break;
             case 'created':
                 $notice->appendChild(pht('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 PHUIInfoView();
         $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
         $notice->setTitle(pht('File Renamed'));
         $notice->appendChild(pht('File history passes through a rename from "%s" to "%s".', $drequest->getPath(), $renamed));
         $content[] = $notice;
     }
     $content[] = $corpus;
     $content[] = $this->buildOpenRevisions();
     $crumbs = $this->buildCrumbs(array('branch' => true, 'path' => true, 'view' => 'browse'));
     $basename = basename($this->getDiffusionRequest()->getPath());
     return $this->newPage()->setTitle(array($basename, $repository->getDisplayName()))->setCrumbs($crumbs)->appendChild($content);
 }
 protected function applyIntraline(&$render, $intra, $corpus)
 {
     $line_break = "<span class=\"over-the-line\">⬅</span><br />";
     foreach ($render as $key => $text) {
         if (isset($intra[$key])) {
             $render[$key] = ArcanistDiffUtils::applyIntralineDiff($text, $intra[$key]);
         }
         if (isset($corpus[$key]) && strlen($corpus[$key]) > $this->lineWidth) {
             $lines = phutil_utf8_hard_wrap_html($render[$key], $this->lineWidth);
             $render[$key] = implode($line_break, $lines);
         }
     }
 }
 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;
 }
示例#11
0
 /**
  * Assert that two values are equal, strictly. The test fails if they are not.
  *
  * NOTE: This method uses PHP's strict equality test operator (`===`) to
  * compare values. This means values and types must be equal, key order must
  * be identical in arrays, and objects must be referentially identical.
  *
  * @param wild    The theoretically expected value, generated by careful
  *                reasoning about the properties of the system.
  * @param wild    The empirically derived value, generated by executing the
  *                test.
  * @param string  A human-readable description of what these values represent,
  *                and particularly of what a discrepancy means.
  *
  * @return void
  * @task assert
  */
 protected final function assertEqual($expect, $result, $message = null)
 {
     if ($expect === $result) {
         $this->assertions++;
         return;
     }
     $expect = PhutilReadableSerializer::printableValue($expect);
     $result = PhutilReadableSerializer::printableValue($result);
     $caller = self::getCallerInfo();
     $file = $caller['file'];
     $line = $caller['line'];
     if ($message !== null) {
         $output = pht('Assertion failed, expected values to be equal (at %s:%d): %s', $file, $line, $message);
     } else {
         $output = pht('Assertion failed, expected values to be equal (at %s:%d).', $file, $line);
     }
     $output .= "\n";
     if (strpos($expect, "\n") === false && strpos($result, "\n") === false) {
         $output .= pht("Expected: %s\n  Actual: %s", $expect, $result);
     } else {
         $output .= pht("Expected vs Actual Output Diff\n%s", ArcanistDiffUtils::renderDifferences($expect, $result, $lines = 0xffff));
     }
     $this->failTest($output);
     throw new PhutilTestTerminatedException($output);
 }
 private function applyIntraline(&$render, $intra, $corpus)
 {
     foreach ($render as $key => $text) {
         if (isset($intra[$key])) {
             $render[$key] = ArcanistDiffUtils::applyIntralineDiff($text, $intra[$key]);
         }
     }
 }
 private function browseFile()
 {
     $viewer = $this->getViewer();
     $request = $this->getRequest();
     $drequest = $this->getDiffusionRequest();
     $repository = $drequest->getRepository();
     $before = $request->getStr('before');
     if ($before) {
         return $this->buildBeforeResponse($before);
     }
     $path = $drequest->getPath();
     $blame_key = PhabricatorDiffusionBlameSetting::SETTINGKEY;
     $color_key = PhabricatorDiffusionColorSetting::SETTINGKEY;
     $show_blame = $request->getBool('blame', $viewer->getUserSetting($blame_key));
     $show_color = $request->getBool('color', $viewer->getUserSetting($color_key));
     $view = $request->getStr('view');
     if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) {
         $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
         $editor = id(new PhabricatorUserPreferencesEditor())->setActor($viewer)->setContentSourceFromRequest($request)->setContinueOnNoEffect(true)->setContinueOnMissingFields(true);
         $xactions = array();
         $xactions[] = $preferences->newTransaction($blame_key, $show_blame);
         $xactions[] = $preferences->newTransaction($color_key, $show_color);
         $editor->applyTransactions($preferences, $xactions);
         $uri = $request->getRequestURI()->alter('blame', null)->alter('color', null);
         return id(new AphrontRedirectResponse())->setURI($uri);
     }
     // We need the blame information if blame is on and we're building plain
     // text, or blame is on and this is an Ajax request. If blame is on and
     // this is a colorized request, we don't show blame at first (we ajax it
     // in afterward) so we don't need to query for it.
     $needs_blame = $show_blame && !$show_color || $show_blame && $request->isAjax();
     $params = array('commit' => $drequest->getCommit(), 'path' => $drequest->getPath());
     $byte_limit = null;
     if ($view !== 'raw') {
         $byte_limit = PhabricatorFileStorageEngine::getChunkThreshold();
         $time_limit = 10;
         $params += array('timeout' => $time_limit, 'byteLimit' => $byte_limit);
     }
     $response = $this->callConduitWithDiffusionRequest('diffusion.filecontentquery', $params);
     $hit_byte_limit = $response['tooHuge'];
     $hit_time_limit = $response['tooSlow'];
     $file_phid = $response['filePHID'];
     if ($hit_byte_limit) {
         $corpus = $this->buildErrorCorpus(pht('This file is larger than %s byte(s), and too large to display ' . 'in the web UI.', phutil_format_bytes($byte_limit)));
     } else {
         if ($hit_time_limit) {
             $corpus = $this->buildErrorCorpus(pht('This file took too long to load from the repository (more than ' . '%s second(s)).', new PhutilNumber($time_limit)));
         } else {
             $file = id(new PhabricatorFileQuery())->setViewer($viewer)->withPHIDs(array($file_phid))->executeOne();
             if (!$file) {
                 throw new Exception(pht('Failed to load content file!'));
             }
             if ($view === 'raw') {
                 return $file->getRedirectResponse();
             }
             $data = $file->loadFileData();
             $ref = $this->getGitLFSRef($repository, $data);
             if ($ref) {
                 if ($view == 'git-lfs') {
                     $file = $this->loadGitLFSFile($ref);
                     // Rename the file locally so we generate a better vanity URI for
                     // it. In storage, it just has a name like "lfs-13f9a94c0923...",
                     // since we don't get any hints about possible human-readable names
                     // at upload time.
                     $basename = basename($drequest->getPath());
                     $file->makeEphemeral();
                     $file->setName($basename);
                     return $file->getRedirectResponse();
                 } else {
                     $corpus = $this->buildGitLFSCorpus($ref);
                 }
             } else {
                 if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) {
                     $file_uri = $file->getBestURI();
                     if ($file->isViewableImage()) {
                         $corpus = $this->buildImageCorpus($file_uri);
                     } else {
                         $corpus = $this->buildBinaryCorpus($file_uri, $data);
                     }
                 } else {
                     $this->loadLintMessages();
                     $this->coverage = $drequest->loadCoverage();
                     // Build the content of the file.
                     $corpus = $this->buildCorpus($show_blame, $show_color, $data, $needs_blame, $drequest, $path, $data);
                 }
             }
         }
     }
     if ($request->isAjax()) {
         return id(new AphrontAjaxResponse())->setContent($corpus);
     }
     require_celerity_resource('diffusion-source-css');
     // Render the page.
     $view = $this->buildCurtain($drequest);
     $curtain = $this->enrichCurtain($view, $drequest, $show_blame, $show_color);
     $properties = $this->buildPropertyView($drequest);
     $header = $this->buildHeaderView($drequest);
     $header->setHeaderIcon('fa-file-code-o');
     $content = array();
     $follow = $request->getStr('follow');
     if ($follow) {
         $notice = new PHUIInfoView();
         $notice->setSeverity(PHUIInfoView::SEVERITY_WARNING);
         $notice->setTitle(pht('Unable to Continue'));
         switch ($follow) {
             case 'first':
                 $notice->appendChild(pht('Unable to continue tracing the history of this file because ' . 'this commit is the first commit in the repository.'));
                 break;
             case 'created':
                 $notice->appendChild(pht('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 PHUIInfoView();
         $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
         $notice->setTitle(pht('File Renamed'));
         $notice->appendChild(pht('File history passes through a rename from "%s" to "%s".', $drequest->getPath(), $renamed));
         $content[] = $notice;
     }
     $content[] = $corpus;
     $content[] = $this->buildOpenRevisions();
     $crumbs = $this->buildCrumbs(array('branch' => true, 'path' => true, 'view' => 'browse'));
     $crumbs->setBorder(true);
     $basename = basename($this->getDiffusionRequest()->getPath());
     $view = id(new PHUITwoColumnView())->setHeader($header)->setCurtain($curtain)->setMainColumn(array($content));
     if ($properties) {
         $view->addPropertySection(pht('Details'), $properties);
     }
     $title = array($basename, $repository->getDisplayName());
     return $this->newPage()->setTitle($title)->setCrumbs($crumbs)->appendChild(array($view));
 }
 public function processRequest()
 {
     $request = $this->getRequest();
     $drequest = $this->getDiffusionRequest();
     $viewer = $request->getUser();
     $before = $request->getStr('before');
     if ($before) {
         return $this->buildBeforeResponse($before);
     }
     $path = $drequest->getPath();
     $preferences = $viewer->loadPreferences();
     $show_blame = $request->getBool('blame', $preferences->getPreference(PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME, false));
     $show_color = $request->getBool('color', $preferences->getPreference(PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR, true));
     $view = $request->getStr('view');
     if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) {
         $preferences->setPreference(PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME, $show_blame);
         $preferences->setPreference(PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR, $show_color);
         $preferences->save();
         $uri = $request->getRequestURI()->alter('blame', null)->alter('color', null);
         return id(new AphrontRedirectResponse())->setURI($uri);
     }
     // We need the blame information if blame is on and we're building plain
     // text, or blame is on and this is an Ajax request. If blame is on and
     // this is a colorized request, we don't show blame at first (we ajax it
     // in afterward) so we don't need to query for it.
     $needs_blame = $show_blame && !$show_color || $show_blame && $request->isAjax();
     $file_content = DiffusionFileContent::newFromConduit($this->callConduitWithDiffusionRequest('diffusion.filecontentquery', array('commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'needsBlame' => $needs_blame)));
     $data = $file_content->getCorpus();
     if ($view === 'raw') {
         return $this->buildRawResponse($path, $data);
     }
     $this->loadLintMessages();
     $this->coverage = $drequest->loadCoverage();
     $binary_uri = null;
     if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) {
         $file = $this->loadFileForData($path, $data);
         $file_uri = $file->getBestURI();
         if ($file->isViewableImage()) {
             $corpus = $this->buildImageCorpus($file_uri);
         } else {
             $corpus = $this->buildBinaryCorpus($file_uri, $data);
             $binary_uri = $file_uri;
         }
     } else {
         // Build the content of the file.
         $corpus = $this->buildCorpus($show_blame, $show_color, $file_content, $needs_blame, $drequest, $path, $data);
     }
     if ($request->isAjax()) {
         return id(new AphrontAjaxResponse())->setContent($corpus);
     }
     require_celerity_resource('diffusion-source-css');
     // Render the page.
     $view = $this->buildActionView($drequest);
     $action_list = $this->enrichActionView($view, $drequest, $show_blame, $show_color);
     $properties = $this->buildPropertyView($drequest, $action_list);
     $object_box = id(new PHUIObjectBoxView())->setHeader($this->buildHeaderView($drequest))->addPropertyList($properties);
     $content = array();
     $content[] = $object_box;
     $follow = $request->getStr('follow');
     if ($follow) {
         $notice = new AphrontErrorView();
         $notice->setSeverity(AphrontErrorView::SEVERITY_WARNING);
         $notice->setTitle(pht('Unable to Continue'));
         switch ($follow) {
             case 'first':
                 $notice->appendChild(pht('Unable to continue tracing the history of this file because ' . 'this commit is the first commit in the repository.'));
                 break;
             case 'created':
                 $notice->appendChild(pht('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(pht('File Renamed'));
         $notice->appendChild(pht("File history passes through a rename from '%s' to '%s'.", $drequest->getPath(), $renamed));
         $content[] = $notice;
     }
     $content[] = $corpus;
     $content[] = $this->buildOpenRevisions();
     $crumbs = $this->buildCrumbs(array('branch' => true, 'path' => true, 'view' => 'browse'));
     $basename = basename($this->getDiffusionRequest()->getPath());
     return $this->buildApplicationPage(array($crumbs, $content), array('title' => $basename, 'device' => false));
 }
示例#15
0
 protected function parseChangeset(ArcanistDiffChange $change)
 {
     $all_changes = array();
     do {
         $hunk = new ArcanistDiffHunk();
         $line = $this->getLine();
         $real = array();
         // In the case where only one line is changed, the length is omitted.
         // The final group is for git, which appends a guess at the function
         // context to the diff.
         $matches = null;
         $ok = preg_match('/^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@(?: .*?)?$/U', $line, $matches);
         if (!$ok) {
             // It's possible we hit the style of an svn1.7 property change.
             // This is a 4-line Index block, followed by an empty line, followed
             // by a "Property changes on:" section similar to svn1.6.
             if ($line == '') {
                 $line = $this->nextNonemptyLine();
                 $ok = preg_match('/^Property changes on:/', $line);
                 if (!$ok) {
                     $this->didFailParse("Confused by empty line");
                 }
                 $line = $this->nextLine();
                 return $this->parsePropertyHunk($change);
             }
             $this->didFailParse("Expected hunk header '@@ -NN,NN +NN,NN @@'.");
         }
         $hunk->setOldOffset($matches[1]);
         $hunk->setNewOffset($matches[3]);
         // Cover for the cases where length wasn't present (implying one line).
         $old_len = idx($matches, 2);
         if (!strlen($old_len)) {
             $old_len = 1;
         }
         $new_len = idx($matches, 4);
         if (!strlen($new_len)) {
             $new_len = 1;
         }
         $hunk->setOldLength($old_len);
         $hunk->setNewLength($new_len);
         $add = 0;
         $del = 0;
         $advance = false;
         while (($line = $this->nextLine()) !== null) {
             if (strlen($line)) {
                 $char = $line[0];
             } else {
                 $char = '~';
             }
             switch ($char) {
                 case '\\':
                     if (!preg_match('@\\ No newline at end of file@', $line)) {
                         $this->didFailParse("Expected '\\ No newline at end of file'.");
                     }
                     if ($new_len) {
                         $real[] = $line;
                         $hunk->setIsMissingOldNewline(true);
                     } else {
                         $real[] = $line;
                         $hunk->setIsMissingNewNewline(true);
                     }
                     if (!$new_len) {
                         $advance = true;
                         break 2;
                     }
                     break;
                 case '+':
                     if (!$new_len) {
                         break 2;
                     }
                     ++$add;
                     --$new_len;
                     $real[] = $line;
                     break;
                 case '-':
                     if (!$old_len) {
                         break 2;
                     }
                     ++$del;
                     --$old_len;
                     $real[] = $line;
                     break;
                 case ' ':
                     if (!$old_len && !$new_len) {
                         break 2;
                     }
                     --$old_len;
                     --$new_len;
                     $real[] = $line;
                     break;
                 case '~':
                     $advance = true;
                     break 2;
                 default:
                     break 2;
             }
         }
         if ($old_len != 0 || $new_len != 0) {
             $this->didFailParse("Found the wrong number of hunk lines.");
         }
         $corpus = implode("\n", $real);
         $is_binary = false;
         if ($this->detectBinaryFiles) {
             $is_binary = !phutil_is_utf8($corpus);
             if ($is_binary && $this->tryEncoding) {
                 $is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus);
                 if (!$is_binary) {
                     // NOTE: This feature is HIGHLY EXPERIMENTAL and will cause a lot
                     // of issues. Use it at your own risk.
                     $corpus = mb_convert_encoding($corpus, 'UTF-8', $this->tryEncoding);
                     if (!phutil_is_utf8($corpus)) {
                         throw new Exception('Failed converting hunk to ' . $this->tryEncoding);
                     }
                 }
             }
         }
         if ($is_binary) {
             // SVN happily treats binary files which aren't marked with the right
             // mime type as text files. Detect that junk here and mark the file
             // binary. We'll catch stuff with unicode too, but that's verboten
             // anyway. If there are too many false positives with this we might
             // need to make it threshold-triggered instead of triggering on any
             // unprintable byte.
             $change->setFileType(ArcanistDiffChangeType::FILE_BINARY);
         } else {
             $hunk->setCorpus($corpus);
             $hunk->setAddLines($add);
             $hunk->setDelLines($del);
             $change->addHunk($hunk);
         }
         if ($advance) {
             $line = $this->nextNonemptyLine();
         }
     } while (preg_match('/^@@ /', $line));
 }
示例#16
0
 protected function parseChangeset(ArcanistDiffChange $change)
 {
     // If a diff includes two sets of changes to the same file, let the
     // second one win. In particular, this occurs when adding subdirectories
     // in Subversion that contain files: the file text will be present in
     // both the directory diff and the file diff. See T5555. Dropping the
     // hunks lets whichever one shows up later win instead of showing changes
     // twice.
     $change->dropHunks();
     $all_changes = array();
     do {
         $hunk = new ArcanistDiffHunk();
         $line = $this->getLineTrimmed();
         $real = array();
         // In the case where only one line is changed, the length is omitted.
         // The final group is for git, which appends a guess at the function
         // context to the diff.
         $matches = null;
         $ok = preg_match('/^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@(?: .*?)?$/U', $line, $matches);
         if (!$ok) {
             // It's possible we hit the style of an svn1.7 property change.
             // This is a 4-line Index block, followed by an empty line, followed
             // by a "Property changes on:" section similar to svn1.6.
             if ($line == '') {
                 $line = $this->nextNonemptyLine();
                 $ok = preg_match('/^Property changes on:/', $line);
                 if (!$ok) {
                     $this->didFailParse(pht('Confused by empty line'));
                 }
                 $line = $this->nextLine();
                 return $this->parsePropertyHunk($change);
             }
             $this->didFailParse(pht("Expected hunk header '%s'.", '@@ -NN,NN +NN,NN @@'));
         }
         $hunk->setOldOffset($matches[1]);
         $hunk->setNewOffset($matches[3]);
         // Cover for the cases where length wasn't present (implying one line).
         $old_len = idx($matches, 2);
         if (!strlen($old_len)) {
             $old_len = 1;
         }
         $new_len = idx($matches, 4);
         if (!strlen($new_len)) {
             $new_len = 1;
         }
         $hunk->setOldLength($old_len);
         $hunk->setNewLength($new_len);
         $add = 0;
         $del = 0;
         $hit_next_hunk = false;
         while (($line = $this->nextLine()) !== null) {
             if (strlen(rtrim($line, "\r\n"))) {
                 $char = $line[0];
             } else {
                 // Normally, we do not encouter empty lines in diffs, because
                 // unchanged lines have an initial space. However, in Git, with
                 // the option `diff.suppress-blank-empty` set, unchanged blank lines
                 // emit as completely empty. If we encounter a completely empty line,
                 // treat it as a ' ' (i.e., unchanged empty line) line.
                 $char = ' ';
             }
             switch ($char) {
                 case '\\':
                     if (!preg_match('@\\ No newline at end of file@', $line)) {
                         $this->didFailParse(pht("Expected '\\ No newline at end of file'."));
                     }
                     if ($new_len) {
                         $real[] = $line;
                         $hunk->setIsMissingOldNewline(true);
                     } else {
                         $real[] = $line;
                         $hunk->setIsMissingNewNewline(true);
                     }
                     if (!$new_len) {
                         break 2;
                     }
                     break;
                 case '+':
                     ++$add;
                     --$new_len;
                     $real[] = $line;
                     break;
                 case '-':
                     if (!$old_len) {
                         // In this case, we've hit "---" from a new file. So don't
                         // advance the line cursor.
                         $hit_next_hunk = true;
                         break 2;
                     }
                     ++$del;
                     --$old_len;
                     $real[] = $line;
                     break;
                 case ' ':
                     if (!$old_len && !$new_len) {
                         break 2;
                     }
                     --$old_len;
                     --$new_len;
                     $real[] = $line;
                     break;
                 default:
                     // We hit something, likely another hunk.
                     $hit_next_hunk = true;
                     break 2;
             }
         }
         if ($old_len || $new_len) {
             $this->didFailParse(pht('Found the wrong number of hunk lines.'));
         }
         $corpus = implode('', $real);
         $is_binary = false;
         if ($this->detectBinaryFiles) {
             $is_binary = !phutil_is_utf8($corpus);
             $try_encoding = $this->tryEncoding;
             if ($is_binary && $try_encoding) {
                 $is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus);
                 if (!$is_binary) {
                     $corpus = phutil_utf8_convert($corpus, 'UTF-8', $try_encoding);
                     if (!phutil_is_utf8($corpus)) {
                         throw new Exception(pht("Failed to convert a hunk from '%s' to UTF-8. " . "Check that the specified encoding is correct.", $try_encoding));
                     }
                 }
             }
         }
         if ($is_binary) {
             // SVN happily treats binary files which aren't marked with the right
             // mime type as text files. Detect that junk here and mark the file
             // binary. We'll catch stuff with unicode too, but that's verboten
             // anyway. If there are too many false positives with this we might
             // need to make it threshold-triggered instead of triggering on any
             // unprintable byte.
             $change->setFileType(ArcanistDiffChangeType::FILE_BINARY);
         } else {
             $hunk->setCorpus($corpus);
             $hunk->setAddLines($add);
             $hunk->setDelLines($del);
             $change->addHunk($hunk);
         }
         if (!$hit_next_hunk) {
             $line = $this->nextNonemptyLine();
         }
     } while (preg_match('/^@@ /', $line));
 }
 public final function isBinaryFile($path)
 {
     try {
         $data = $this->loadData($path);
     } catch (Exception $ex) {
         return false;
     }
     return ArcanistDiffUtils::isHeuristicBinaryFile($data);
 }
 public function generateIntraLineDiffs()
 {
     $old = $this->getOldLines();
     $new = $this->getNewLines();
     $diffs = array();
     foreach ($old as $key => $o) {
         $n = $new[$key];
         if (!$o || !$n) {
             continue;
         }
         if ($o['type'] != $n['type']) {
             $diffs[$key] = ArcanistDiffUtils::generateIntralineDiff($o['text'], $n['text']);
         }
     }
     $this->setIntraLineDiffs($diffs);
     return $this;
 }
示例#19
0
 protected function generateChanges()
 {
     $parser = $this->newDiffParser();
     $is_raw = $this->isRawDiffSource();
     if ($is_raw) {
         if ($this->getArgument('raw')) {
             fwrite(STDERR, pht('Reading diff from stdin...') . "\n");
             $raw_diff = file_get_contents('php://stdin');
         } else {
             if ($this->getArgument('raw-command')) {
                 list($raw_diff) = execx('%C', $this->getArgument('raw-command'));
             } else {
                 throw new Exception(pht('Unknown raw diff source.'));
             }
         }
         $changes = $parser->parseDiff($raw_diff);
         foreach ($changes as $key => $change) {
             // Remove "message" changes, e.g. from "git show".
             if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
                 unset($changes[$key]);
             }
         }
         return $changes;
     }
     $repository_api = $this->getRepositoryAPI();
     if ($repository_api instanceof ArcanistSubversionAPI) {
         $paths = $this->generateAffectedPaths();
         $this->primeSubversionWorkingCopyData($paths);
         // Check to make sure the user is diffing from a consistent base revision.
         // This is mostly just an abuse sanity check because it's silly to do this
         // and makes the code more difficult to effectively review, but it also
         // affects patches and makes them nonportable.
         $bases = $repository_api->getSVNBaseRevisions();
         // Remove all files with baserev "0"; these files are new.
         foreach ($bases as $path => $baserev) {
             if ($bases[$path] <= 0) {
                 unset($bases[$path]);
             }
         }
         if ($bases) {
             $rev = reset($bases);
             $revlist = array();
             foreach ($bases as $path => $baserev) {
                 $revlist[] = '    ' . pht('Revision %s, %s', $baserev, $path);
             }
             $revlist = implode("\n", $revlist);
             foreach ($bases as $path => $baserev) {
                 if ($baserev !== $rev) {
                     throw new ArcanistUsageException(pht("Base revisions of changed paths are mismatched. Update all " . "paths to the same base revision before creating a diff: " . "\n\n%s", $revlist));
                 }
             }
             // If you have a change which affects several files, all of which are
             // at a consistent base revision, treat that revision as the effective
             // base revision. The use case here is that you made a change to some
             // file, which updates it to HEAD, but want to be able to change it
             // again without updating the entire working copy. This is a little
             // sketchy but it arises in Facebook Ops workflows with config files and
             // doesn't have any real material tradeoffs (e.g., these patches are
             // perfectly applyable).
             $repository_api->overrideSVNBaseRevisionNumber($rev);
         }
         $changes = $parser->parseSubversionDiff($repository_api, $paths);
     } else {
         if ($repository_api instanceof ArcanistGitAPI) {
             $diff = $repository_api->getFullGitDiff($repository_api->getBaseCommit(), $repository_api->getHeadCommit());
             if (!strlen($diff)) {
                 throw new ArcanistUsageException(pht('No changes found. (Did you specify the wrong commit range?)'));
             }
             $changes = $parser->parseDiff($diff);
         } else {
             if ($repository_api instanceof ArcanistMercurialAPI) {
                 $diff = $repository_api->getFullMercurialDiff();
                 if (!strlen($diff)) {
                     throw new ArcanistUsageException(pht('No changes found. (Did you specify the wrong commit range?)'));
                 }
                 $changes = $parser->parseDiff($diff);
             } else {
                 throw new Exception(pht('Repository API is not supported.'));
             }
         }
     }
     if (count($changes) > 250) {
         $message = pht('This diff has a very large number of changes (%s). Differential ' . 'works best for changes which will receive detailed human review, ' . 'and not as well for large automated changes or bulk checkins. ' . 'See %s for information about reviewing big checkins. Continue anyway?', phutil_count($changes), 'https://secure.phabricator.com/book/phabricator/article/' . 'differential_large_changes/');
         if (!phutil_console_confirm($message)) {
             throw new ArcanistUsageException(pht('Aborted generation of gigantic diff.'));
         }
     }
     $limit = 1024 * 1024 * 4;
     foreach ($changes as $change) {
         $size = 0;
         foreach ($change->getHunks() as $hunk) {
             $size += strlen($hunk->getCorpus());
         }
         if ($size > $limit) {
             $byte_warning = pht("Diff for '%s' with context is %s bytes in length. " . "Generally, source changes should not be this large.", $change->getCurrentPath(), new PhutilNumber($size));
             if (!$this->getArgument('less-context')) {
                 $byte_warning .= ' ' . pht("If this file is a huge text file, try using the '%s' flag.", '--less-context');
             }
             if ($repository_api instanceof ArcanistSubversionAPI) {
                 throw new ArcanistUsageException($byte_warning . ' ' . pht("If the file is not a text file, mark it as binary with:" . "\n\n  \$ %s\n", 'svn propset svn:mime-type application/octet-stream <filename>'));
             } else {
                 $confirm = $byte_warning . ' ' . pht("If the file is not a text file, you can mark it 'binary'. " . "Mark this file as 'binary' and continue?");
                 if (phutil_console_confirm($confirm)) {
                     $change->convertToBinaryChange($repository_api);
                 } else {
                     throw new ArcanistUsageException(pht('Aborted generation of gigantic diff.'));
                 }
             }
         }
     }
     $try_encoding = nonempty($this->getArgument('encoding'), null);
     $utf8_problems = array();
     foreach ($changes as $change) {
         foreach ($change->getHunks() as $hunk) {
             $corpus = $hunk->getCorpus();
             if (!phutil_is_utf8($corpus)) {
                 // If this corpus is heuristically binary, don't try to convert it.
                 // mb_check_encoding() and mb_convert_encoding() are both very very
                 // liberal about what they're willing to process.
                 $is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus);
                 if (!$is_binary) {
                     if (!$try_encoding) {
                         try {
                             $try_encoding = $this->getRepositoryEncoding();
                         } catch (ConduitClientException $e) {
                             if ($e->getErrorCode() == 'ERR-BAD-ARCANIST-PROJECT') {
                                 echo phutil_console_wrap(pht('Lookup of encoding in arcanist project failed: %s', $e->getMessage()) . "\n");
                             } else {
                                 throw $e;
                             }
                         }
                     }
                     if ($try_encoding) {
                         $corpus = phutil_utf8_convert($corpus, 'UTF-8', $try_encoding);
                         $name = $change->getCurrentPath();
                         if (phutil_is_utf8($corpus)) {
                             $this->writeStatusMessage(pht("Converted a '%s' hunk from '%s' to UTF-8.\n", $name, $try_encoding));
                             $hunk->setCorpus($corpus);
                             continue;
                         }
                     }
                 }
                 $utf8_problems[] = $change;
                 break;
             }
         }
     }
     // If there are non-binary files which aren't valid UTF-8, warn the user
     // and treat them as binary changes. See D327 for discussion of why Arcanist
     // has this behavior.
     if ($utf8_problems) {
         $utf8_warning = sprintf("%s\n\n%s\n\n    %s\n", pht('This diff includes %s file(s) which are not valid UTF-8 (they ' . 'contain invalid byte sequences). You can either stop this ' . 'workflow and fix these files, or continue. If you continue, ' . 'these files will be marked as binary.', phutil_count($utf8_problems)), pht("You can learn more about how Phabricator handles character " . "encodings (and how to configure encoding settings and detect and " . "correct encoding problems) by reading 'User Guide: UTF-8 and " . "Character Encoding' in the Phabricator documentation."), pht('%s AFFECTED FILE(S)', phutil_count($utf8_problems)));
         $confirm = pht('Do you want to mark these %s file(s) as binary and continue?', phutil_count($utf8_problems));
         echo phutil_console_format("**%s**\n", pht('Invalid Content Encoding (Non-UTF8)'));
         echo phutil_console_wrap($utf8_warning);
         $file_list = mpull($utf8_problems, 'getCurrentPath');
         $file_list = '    ' . implode("\n    ", $file_list);
         echo $file_list;
         if (!phutil_console_confirm($confirm, $default_no = false)) {
             throw new ArcanistUsageException(pht('Aborted workflow to fix UTF-8.'));
         } else {
             foreach ($utf8_problems as $change) {
                 $change->convertToBinaryChange($repository_api);
             }
         }
     }
     $this->uploadFilesForChanges($changes);
     return $changes;
 }
示例#20
0
 public function isBinaryFile($path)
 {
     // Note that we need the lint engine set before this can be used.
     return ArcanistDiffUtils::isHeuristicBinaryFile($this->getData($path));
 }