/** * 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; }
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; }
/** * 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)); }
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)); }
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; }
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; }
public function isBinaryFile($path) { // Note that we need the lint engine set before this can be used. return ArcanistDiffUtils::isHeuristicBinaryFile($this->getData($path)); }