1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-29 02:02:40 +01:00

(experimental) Merge branch 'master' into experimental

This commit is contained in:
epriestley 2017-09-27 08:16:31 -07:00
commit 56e3232c56
68 changed files with 1144 additions and 357 deletions

View file

@ -448,7 +448,8 @@ try {
} }
if (!$is_usage) { if (!$is_usage) {
fwrite(STDERR, phutil_console_format("**%s**\n", pht('Exception'))); fwrite(STDERR, phutil_console_format(
"<bg:red>** %s **</bg>\n", pht('Exception')));
while ($ex) { while ($ex) {
fwrite(STDERR, $ex->getMessage()."\n"); fwrite(STDERR, $ex->getMessage()."\n");
@ -666,16 +667,33 @@ function arcanist_load_libraries(
if ($ex->getLibrary() != 'arcanist') { if ($ex->getLibrary() != 'arcanist') {
throw $ex; throw $ex;
} }
$arc_dir = dirname(dirname(__FILE__));
$error = pht( // NOTE: If you are running `arc` against itself, we ignore the library
"You are trying to run one copy of Arcanist on another copy of ". // conflict created by loading the local `arc` library (in the current
"Arcanist. This operation is not supported. To execute Arcanist ". // working directory) and continue without loading it.
"operations against this working copy, run `%s` (from the current ".
"working copy) not some other copy of '%s' (you ran one from '%s').", // This means we only execute code in the `arcanist/` directory which is
'./bin/arc', // associated with the binary you are running, whereas we would normally
'arc', // execute local code.
$arc_dir);
throw new ArcanistUsageException($error); // This can make `arc` development slightly confusing if your setup is
// especially bizarre, but it allows `arc` to be used in automation
// workflows more easily. For some context, see PHI13.
$executing_directory = dirname(dirname(__FILE__));
$working_directory = dirname($location);
fwrite(
STDERR,
tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
pht('VERY META'),
pht(
'You are running one copy of Arcanist (at path "%s") against '.
'another copy of Arcanist (at path "%s"). Code in the current '.
'working directory will not be loaded or executed.',
$executing_directory,
$working_directory)));
} }
} }
} }

View file

@ -100,6 +100,7 @@ phutil_register_library_map(array(
'ArcanistConfigurationDrivenUnitTestEngine' => 'unit/engine/ArcanistConfigurationDrivenUnitTestEngine.php', 'ArcanistConfigurationDrivenUnitTestEngine' => 'unit/engine/ArcanistConfigurationDrivenUnitTestEngine.php',
'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php', 'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php',
'ArcanistConsoleLintRenderer' => 'lint/renderer/ArcanistConsoleLintRenderer.php', 'ArcanistConsoleLintRenderer' => 'lint/renderer/ArcanistConsoleLintRenderer.php',
'ArcanistConsoleLintRendererTestCase' => 'lint/renderer/__tests__/ArcanistConsoleLintRendererTestCase.php',
'ArcanistConstructorParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php', 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php',
'ArcanistConstructorParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConstructorParenthesesXHPASTLinterRuleTestCase.php', 'ArcanistConstructorParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConstructorParenthesesXHPASTLinterRuleTestCase.php',
'ArcanistControlStatementSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistControlStatementSpacingXHPASTLinterRule.php', 'ArcanistControlStatementSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistControlStatementSpacingXHPASTLinterRule.php',
@ -238,6 +239,7 @@ phutil_register_library_map(array(
'ArcanistLibraryTestCase' => '__tests__/ArcanistLibraryTestCase.php', 'ArcanistLibraryTestCase' => '__tests__/ArcanistLibraryTestCase.php',
'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php', 'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php',
'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php', 'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php',
'ArcanistLintMessageTestCase' => 'lint/__tests__/ArcanistLintMessageTestCase.php',
'ArcanistLintPatcher' => 'lint/ArcanistLintPatcher.php', 'ArcanistLintPatcher' => 'lint/ArcanistLintPatcher.php',
'ArcanistLintRenderer' => 'lint/renderer/ArcanistLintRenderer.php', 'ArcanistLintRenderer' => 'lint/renderer/ArcanistLintRenderer.php',
'ArcanistLintResult' => 'lint/ArcanistLintResult.php', 'ArcanistLintResult' => 'lint/ArcanistLintResult.php',
@ -287,6 +289,7 @@ phutil_register_library_map(array(
'ArcanistPEP8Linter' => 'lint/linter/ArcanistPEP8Linter.php', 'ArcanistPEP8Linter' => 'lint/linter/ArcanistPEP8Linter.php',
'ArcanistPEP8LinterTestCase' => 'lint/linter/__tests__/ArcanistPEP8LinterTestCase.php', 'ArcanistPEP8LinterTestCase' => 'lint/linter/__tests__/ArcanistPEP8LinterTestCase.php',
'ArcanistPHPCloseTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPCloseTagXHPASTLinterRule.php', 'ArcanistPHPCloseTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPCloseTagXHPASTLinterRule.php',
'ArcanistPHPCloseTagXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPCloseTagXHPASTLinterRuleTestCase.php',
'ArcanistPHPCompatibilityXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php', 'ArcanistPHPCompatibilityXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php',
'ArcanistPHPCompatibilityXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPCompatibilityXHPASTLinterRuleTestCase.php', 'ArcanistPHPCompatibilityXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPCompatibilityXHPASTLinterRuleTestCase.php',
'ArcanistPHPEchoTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPEchoTagXHPASTLinterRule.php', 'ArcanistPHPEchoTagXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPHPEchoTagXHPASTLinterRule.php',
@ -543,6 +546,7 @@ phutil_register_library_map(array(
'ArcanistConfigurationDrivenUnitTestEngine' => 'ArcanistUnitTestEngine', 'ArcanistConfigurationDrivenUnitTestEngine' => 'ArcanistUnitTestEngine',
'ArcanistConfigurationManager' => 'Phobject', 'ArcanistConfigurationManager' => 'Phobject',
'ArcanistConsoleLintRenderer' => 'ArcanistLintRenderer', 'ArcanistConsoleLintRenderer' => 'ArcanistLintRenderer',
'ArcanistConsoleLintRendererTestCase' => 'PhutilTestCase',
'ArcanistConstructorParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistConstructorParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistConstructorParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistControlStatementSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistControlStatementSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
@ -681,6 +685,7 @@ phutil_register_library_map(array(
'ArcanistLibraryTestCase' => 'PhutilLibraryTestCase', 'ArcanistLibraryTestCase' => 'PhutilLibraryTestCase',
'ArcanistLintEngine' => 'Phobject', 'ArcanistLintEngine' => 'Phobject',
'ArcanistLintMessage' => 'Phobject', 'ArcanistLintMessage' => 'Phobject',
'ArcanistLintMessageTestCase' => 'PhutilTestCase',
'ArcanistLintPatcher' => 'Phobject', 'ArcanistLintPatcher' => 'Phobject',
'ArcanistLintRenderer' => 'Phobject', 'ArcanistLintRenderer' => 'Phobject',
'ArcanistLintResult' => 'Phobject', 'ArcanistLintResult' => 'Phobject',
@ -730,6 +735,7 @@ phutil_register_library_map(array(
'ArcanistPEP8Linter' => 'ArcanistExternalLinter', 'ArcanistPEP8Linter' => 'ArcanistExternalLinter',
'ArcanistPEP8LinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistPEP8LinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistPHPCloseTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPHPCloseTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPHPCloseTagXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistPHPCompatibilityXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPHPCompatibilityXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPHPCompatibilityXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistPHPCompatibilityXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistPHPEchoTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPHPEchoTagXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',

View file

@ -80,13 +80,6 @@ final class ArcanistSettings extends Phobject {
'arc land'), 'arc land'),
'example' => '"develop"', 'example' => '"develop"',
), ),
'arc.land.update.default' => array(
'type' => 'string',
'help' => pht(
'The default strategy to use when arc land updates the feature '.
'branch. Supports "rebase" and "merge" strategies.'),
'example' => '"rebase"',
),
'arc.lint.cache' => array( 'arc.lint.cache' => array(
'type' => 'bool', 'type' => 'bool',
'help' => pht( 'help' => pht(

View file

@ -54,6 +54,7 @@ final class ArcanistHgServerChannel extends PhutilProtocolChannel {
private $mode = self::MODE_CHANNEL; private $mode = self::MODE_CHANNEL;
private $byteLengthOfNextChunk = 1; private $byteLengthOfNextChunk = 1;
private $buf = ''; private $buf = '';
private $outputChannel;
/* -( Protocol Implementation )-------------------------------------------- */ /* -( Protocol Implementation )-------------------------------------------- */
@ -137,7 +138,7 @@ final class ArcanistHgServerChannel extends PhutilProtocolChannel {
// 'output', 'error', 'result' or 'debug' respectively. This is a // 'output', 'error', 'result' or 'debug' respectively. This is a
// single byte long. Next, we'll expect a length. // single byte long. Next, we'll expect a length.
$this->channel = $chunk; $this->outputChannel = $chunk;
$this->byteLengthOfNextChunk = 4; $this->byteLengthOfNextChunk = 4;
$this->mode = self::MODE_LENGTH; $this->mode = self::MODE_LENGTH;
break; break;
@ -153,11 +154,11 @@ final class ArcanistHgServerChannel extends PhutilProtocolChannel {
// given length. We produce a message from the channel and the data // given length. We produce a message from the channel and the data
// and return it. Next, we expect another channel name. // and return it. Next, we expect another channel name.
$message = array($this->channel, $chunk); $message = array($this->outputChannel, $chunk);
$this->byteLengthOfNextChunk = 1; $this->byteLengthOfNextChunk = 1;
$this->mode = self::MODE_CHANNEL; $this->mode = self::MODE_CHANNEL;
$this->channel = null; $this->outputChannel = null;
$messages[] = $message; $messages[] = $message;
break; break;

View file

@ -59,7 +59,7 @@ final class ArcanistGitLandEngine
public function __destruct() { public function __destruct() {
if ($this->restoreWhenDestroyed) { if ($this->restoreWhenDestroyed) {
$this->writeWARN( $this->writeWarn(
pht('INTERRUPTED!'), pht('INTERRUPTED!'),
pht('Restoring working copy to its original state.')); pht('Restoring working copy to its original state.'));

View file

@ -40,7 +40,8 @@ final class ArcanistLintMessage extends Phobject {
$message->setGranularity(idx($dict, 'granularity')); $message->setGranularity(idx($dict, 'granularity'));
$message->setOtherLocations(idx($dict, 'locations', array())); $message->setOtherLocations(idx($dict, 'locations', array()));
if (isset($dict['bypassChangedLineFiltering'])) { if (isset($dict['bypassChangedLineFiltering'])) {
$message->bypassChangedLineFiltering($dict['bypassChangedLineFiltering']); $message->setBypassChangedLineFiltering(
$dict['bypassChangedLineFiltering']);
} }
return $message; return $message;
} }
@ -288,4 +289,83 @@ final class ArcanistLintMessage extends Phobject {
return $value; return $value;
} }
public function newTrimmedMessage() {
if (!$this->isPatchable()) {
return clone $this;
}
// If the original and replacement text have a similar prefix or suffix,
// we trim it to reduce the size of the diff we show to the user.
$replacement = $this->getReplacementText();
$original = $this->getOriginalText();
$replacement_length = strlen($replacement);
$original_length = strlen($original);
$minimum_length = min($original_length, $replacement_length);
$prefix_length = 0;
for ($ii = 0; $ii < $minimum_length; $ii++) {
if ($original[$ii] !== $replacement[$ii]) {
break;
}
$prefix_length++;
}
// NOTE: The two strings can't be the same because the message won't be
// "patchable" if they are, so we don't need a special check for the case
// where the entire string is a shared prefix.
// However, if the two strings are in the form "ABC" and "ABBC", we may
// find a prefix and a suffix with a combined length greater than the
// total size of the smaller string if we don't limit the search.
$max_suffix = ($minimum_length - $prefix_length);
$suffix_length = 0;
for ($ii = 1; $ii <= $max_suffix; $ii++) {
$original_char = $original[$original_length - $ii];
$replacement_char = $replacement[$replacement_length - $ii];
if ($original_char !== $replacement_char) {
break;
}
$suffix_length++;
}
if ($suffix_length) {
$original = substr($original, 0, -$suffix_length);
$replacement = substr($replacement, 0, -$suffix_length);
}
$line = $this->getLine();
$char = $this->getChar();
if ($prefix_length) {
$prefix = substr($original, 0, $prefix_length);
// NOTE: Prior to PHP7, `substr("a", 1)` returned false instead of
// the empty string. Cast these to force the PHP7-ish behavior we
// expect.
$original = (string)substr($original, $prefix_length);
$replacement = (string)substr($replacement, $prefix_length);
// If we've removed a prefix, we need to push the character and line
// number for the warning forward to account for the characters we threw
// away.
for ($ii = 0; $ii < $prefix_length; $ii++) {
$char++;
if ($prefix[$ii] == "\n") {
$line++;
$char = 1;
}
}
}
return id(clone $this)
->setOriginalText($original)
->setReplacementText($replacement)
->setLine($line)
->setChar($char);
}
} }

View file

@ -0,0 +1,88 @@
<?php
final class ArcanistLintMessageTestCase
extends PhutilTestCase {
public function testMessageTrimming() {
$map = array(
'simple' => array(
'old' => 'a',
'new' => 'b',
'old.expect' => 'a',
'new.expect' => 'b',
'line' => 1,
'char' => 1,
),
'prefix' => array(
'old' => 'ever after',
'new' => 'evermore',
'old.expect' => ' after',
'new.expect' => 'more',
'line' => 1,
'char' => 5,
),
'suffix' => array(
'old' => 'arcane archaeology',
'new' => 'mythic archaeology',
'old.expect' => 'arcane',
'new.expect' => 'mythic',
'line' => 1,
'char' => 1,
),
'both' => array(
'old' => 'large red apple',
'new' => 'large blue apple',
'old.expect' => 'red',
'new.expect' => 'blue',
'line' => 1,
'char' => 7,
),
'prefix-newline' => array(
'old' => "four score\nand five years ago",
'new' => "four score\nand seven years ago",
'old.expect' => 'five',
'new.expect' => 'seven',
'line' => 2,
'char' => 5,
),
'mid-newline' => array(
'old' => 'ABA',
'new' => 'ABBA',
'old.expect' => '',
'new.expect' => 'B',
'line' => 1,
'char' => 3,
),
);
foreach ($map as $key => $test_case) {
$message = id(new ArcanistLintMessage())
->setOriginalText($test_case['old'])
->setReplacementText($test_case['new'])
->setLine(1)
->setChar(1);
$actual = $message->newTrimmedMessage();
$this->assertEqual(
$test_case['old.expect'],
$actual->getOriginalText(),
pht('Original text for "%s".', $key));
$this->assertEqual(
$test_case['new.expect'],
$actual->getReplacementText(),
pht('Replacement text for "%s".', $key));
$this->assertEqual(
$test_case['line'],
$actual->getLine(),
pht('Line for "%s".', $key));
$this->assertEqual(
$test_case['char'],
$actual->getChar(),
pht('Char for "%s".', $key));
}
}
}

View file

@ -189,7 +189,7 @@ abstract class ArcanistBaseXHPASTLinter extends ArcanistFutureLinter {
* Get a path's parse exception from the responsible linter. * Get a path's parse exception from the responsible linter.
* *
* @param string Path to retrieve exception for. * @param string Path to retrieve exception for.
* @return Exeption|null Parse exception, if available. * @return Exception|null Parse exception, if available.
* @task sharing * @task sharing
*/ */
final protected function getXHPASTExceptionForPath($path) { final protected function getXHPASTExceptionForPath($path) {

View file

@ -51,12 +51,14 @@ final class ArcanistFlake8Linter extends ArcanistExternalLinter {
protected function parseLinterOutput($path, $err, $stdout, $stderr) { protected function parseLinterOutput($path, $err, $stdout, $stderr) {
$lines = phutil_split_lines($stdout, false); $lines = phutil_split_lines($stdout, false);
// stdin:2: W802 undefined name 'foo' # pyflakes
// stdin:3:1: E302 expected 2 blank lines, found 1 # pep8
$regexp =
'/^(?:.*?):(?P<line>\d+):(?:(?P<char>\d+):)? (?P<code>\S+) (?P<msg>.*)$/';
$messages = array(); $messages = array();
foreach ($lines as $line) { foreach ($lines as $line) {
$matches = null; $matches = null;
// stdin:2: W802 undefined name 'foo' # pyflakes
// stdin:3:1: E302 expected 2 blank lines, found 1 # pep8
$regexp = '/^(.*?):(\d+):(?:(\d+):)? (\S+) (.*)$/';
if (!preg_match($regexp, $line, $matches)) { if (!preg_match($regexp, $line, $matches)) {
continue; continue;
} }
@ -66,14 +68,14 @@ final class ArcanistFlake8Linter extends ArcanistExternalLinter {
$message = new ArcanistLintMessage(); $message = new ArcanistLintMessage();
$message->setPath($path); $message->setPath($path);
$message->setLine($matches[2]); $message->setLine($matches['line']);
if (!empty($matches[3])) { if (!empty($matches['char'])) {
$message->setChar($matches[3]); $message->setChar($matches['char']);
} }
$message->setCode($matches[4]); $message->setCode($matches['code']);
$message->setName($this->getLinterName().' '.$matches[3]); $message->setName($this->getLinterName().' '.$matches['code']);
$message->setDescription($matches[5]); $message->setDescription($matches['msg']);
$message->setSeverity($this->getLintMessageSeverity($matches[4])); $message->setSeverity($this->getLintMessageSeverity($matches['code']));
$messages[] = $message; $messages[] = $message;
} }

View file

@ -245,7 +245,7 @@ final class ArcanistTextLinter extends ArcanistLinter {
$matches = null; $matches = null;
$preg = preg_match_all( $preg = preg_match_all(
'/ +$/m', '/[[:blank:]]+$/m',
$data, $data,
$matches, $matches,
PREG_OFFSET_CAPTURE); PREG_OFFSET_CAPTURE);
@ -311,9 +311,7 @@ final class ArcanistTextLinter extends ArcanistLinter {
$this->raiseLintAtOffset( $this->raiseLintAtOffset(
$offset, $offset,
self::LINT_EOF_WHITESPACE, self::LINT_EOF_WHITESPACE,
pht( pht('This file contains unnecessary trailing whitespace.'),
'This file contains trailing whitespace at the end of the file. '.
'This is unnecessary and should be avoided when possible.'),
$string, $string,
''); '');
} }

View file

@ -3,11 +3,14 @@
final class ArcanistClosureLinterTestCase final class ArcanistClosureLinterTestCase
extends ArcanistExternalLinterTestCase { extends ArcanistExternalLinterTestCase {
public function testLinter() { protected function getLinter() {
$linter = new ArcanistClosureLinter(); $linter = new ArcanistClosureLinter();
$linter->setFlags(array('--additional_extensions=lint-test')); $linter->setFlags(array('--additional_extensions=lint-test'));
return $linter;
}
$this->executeTestsInDirectory(dirname(__FILE__).'/gjslint/', $linter); public function testLinter() {
$this->executeTestsInDirectory(dirname(__FILE__).'/gjslint/');
} }
} }

View file

@ -0,0 +1,15 @@
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Phasellus sodales nibh erat,
in hendrerit nulla dictum interdum.
~~~~~~~~~~
error:1:28
autofix:1:28
autofix:2:29
autofix:3:29
autofix:4:36
~~~~~~~~~~
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Phasellus sodales nibh erat,
in hendrerit nulla dictum interdum.

View file

@ -0,0 +1,15 @@
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Phasellus sodales nibh erat,
in hendrerit nulla dictum interdum.
~~~~~~~~~~
error:1:28
autofix:1:28
autofix:2:29
autofix:3:29
autofix:4:36
~~~~~~~~~~
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Phasellus sodales nibh erat,
in hendrerit nulla dictum interdum.

View file

@ -1,3 +1,4 @@
<?php ?> <?php
~~~~~~~~~~ ~~~~~~~~~~
warning:: warning::

File diff suppressed because one or more lines are too long

View file

@ -56,12 +56,14 @@ final class ArcanistPhutilXHPASTLinterStandard
} }
public function getLinterSeverityMap() { public function getLinterSeverityMap() {
$advice = ArcanistLintSeverity::SEVERITY_ADVICE; $advice = ArcanistLintSeverity::SEVERITY_ADVICE;
$error = ArcanistLintSeverity::SEVERITY_ERROR; $error = ArcanistLintSeverity::SEVERITY_ERROR;
$warning = ArcanistLintSeverity::SEVERITY_WARNING;
return array( return array(
ArcanistTodoCommentXHPASTLinterRule::ID => $advice, ArcanistTodoCommentXHPASTLinterRule::ID => $advice,
ArcanistCommentSpacingXHPASTLinterRule::ID => $error, ArcanistCommentSpacingXHPASTLinterRule::ID => $error,
ArcanistRaggedClassTreeEdgeXHPASTLinterRule::ID => $warning,
); );
} }

View file

@ -12,7 +12,7 @@ final class ArcanistPHPCloseTagXHPASTLinterRule
public function process(XHPASTNode $root) { public function process(XHPASTNode $root) {
$inline_html = $root->selectDescendantsOfType('n_INLINE_HTML'); $inline_html = $root->selectDescendantsOfType('n_INLINE_HTML');
if ($inline_html) { if (count($inline_html) > 0) {
return; return;
} }

View file

@ -369,7 +369,7 @@ final class ArcanistPHPCompatibilityXHPASTLinterRule
} }
} }
$literals = $root->selectDescendantsOftype('n_ARRAY_LITERAL'); $literals = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
foreach ($literals as $literal) { foreach ($literals as $literal) {
$open_token = head($literal->getTokens())->getValue(); $open_token = head($literal->getTokens())->getValue();
if ($open_token == '[') { if ($open_token == '[') {

View file

@ -0,0 +1,10 @@
<?php
final class ArcanistPHPCloseTagXHPASTLinterRuleTestCase
extends ArcanistXHPASTLinterRuleTestCase {
public function testLinter() {
$this->executeTestsInDirectory(dirname(__FILE__).'/php-close-tag/');
}
}

View file

@ -0,0 +1,6 @@
<?php
?>
stuff.
<?php
?>
~~~~~~~~~~

View file

@ -0,0 +1,5 @@
<?php
return;
?>
~~~~~~~~~~
error:3:1

View file

@ -6,20 +6,30 @@
final class ArcanistConsoleLintRenderer extends ArcanistLintRenderer { final class ArcanistConsoleLintRenderer extends ArcanistLintRenderer {
private $showAutofixPatches = false; private $showAutofixPatches = false;
private $testableMode;
public function setShowAutofixPatches($show_autofix_patches) { public function setShowAutofixPatches($show_autofix_patches) {
$this->showAutofixPatches = $show_autofix_patches; $this->showAutofixPatches = $show_autofix_patches;
return $this; return $this;
} }
public function setTestableMode($testable_mode) {
$this->testableMode = $testable_mode;
return $this;
}
public function getTestableMode() {
return $this->testableMode;
}
public function renderLintResult(ArcanistLintResult $result) { public function renderLintResult(ArcanistLintResult $result) {
$messages = $result->getMessages(); $messages = $result->getMessages();
$path = $result->getPath(); $path = $result->getPath();
$data = $result->getData();
$lines = explode("\n", $result->getData()); $line_map = $this->newOffsetMap($data);
$text = array(); $text = array();
foreach ($messages as $message) { foreach ($messages as $message) {
if (!$this->showAutofixPatches && $message->isAutofix()) { if (!$this->showAutofixPatches && $message->isAutofix()) {
continue; continue;
@ -57,7 +67,7 @@ final class ArcanistConsoleLintRenderer extends ArcanistLintRenderer {
phutil_console_wrap($description, 4)); phutil_console_wrap($description, 4));
if ($message->hasFileContext()) { if ($message->hasFileContext()) {
$text[] = $this->renderContext($message, $lines); $text[] = $this->renderContext($message, $data, $line_map);
} }
} }
@ -75,153 +85,183 @@ final class ArcanistConsoleLintRenderer extends ArcanistLintRenderer {
protected function renderContext( protected function renderContext(
ArcanistLintMessage $message, ArcanistLintMessage $message,
array $line_data) { $data,
array $line_map) {
$context = 3;
$message = $message->newTrimmedMessage();
$original = $message->getOriginalText();
$replacement = $message->getReplacementText();
$line = $message->getLine();
$char = $message->getChar();
$old = $data;
$old_lines = phutil_split_lines($old);
$old_impact = substr_count($original, "\n") + 1;
$start = $line;
if ($message->isPatchable()) {
$patch_offset = $line_map[$line] + ($char - 1);
$new = substr_replace(
$old,
$replacement,
$patch_offset,
strlen($original));
$new_lines = phutil_split_lines($new);
// Figure out how many "-" and "+" lines we have by counting the newlines
// for the relevant patches. This may overestimate things if we are adding
// or removing entire lines, but we'll adjust things below.
$new_impact = substr_count($replacement, "\n") + 1;
// If this is a change on a single line, we'll try to highlight the
// changed character range to make it easier to pick out.
if ($old_impact === 1 && $new_impact === 1) {
$old_lines[$start - 1] = substr_replace(
$old_lines[$start - 1],
$this->highlightText($original),
$char - 1,
strlen($original));
$new_lines[$start - 1] = substr_replace(
$new_lines[$start - 1],
$this->highlightText($replacement),
$char - 1,
strlen($replacement));
}
// If lines at the beginning of the changed line range are actually the
// same, shrink the range. This happens when a patch just adds a line.
do {
$old_line = idx($old_lines, $start - 1, null);
$new_line = idx($new_lines, $start - 1, null);
if ($old_line !== $new_line) {
break;
}
$start++;
$old_impact--;
$new_impact--;
if ($old_impact < 0 || $new_impact < 0) {
throw new Exception(
pht(
'Modified prefix line range has become negative '.
'(old = %d, new = %d).',
$old_impact,
$new_impact));
}
} while (true);
// If the lines at the end of the changed line range are actually the
// same, shrink the range. This happens when a patch just removes a
// line.
do {
$old_suffix = idx($old_lines, $start + $old_impact - 2, null);
$new_suffix = idx($new_lines, $start + $new_impact - 2, null);
if ($old_suffix !== $new_suffix) {
break;
}
$old_impact--;
$new_impact--;
// We can end up here if a patch removes a line which occurs after
// another identical line.
if ($old_impact <= 0 || $new_impact <= 0) {
break;
}
} while (true);
} else {
// If we have "original" text and it is contained on a single line,
// highlight the affected area. If we don't have any text, we'll mark
// the character with a caret (below, in rendering) instead.
if ($old_impact == 1 && strlen($original)) {
$old_lines[$start - 1] = substr_replace(
$old_lines[$start - 1],
$this->highlightText($original),
$char - 1,
strlen($original));
}
$old_impact = 0;
$new_impact = 0;
}
$lines_of_context = 3;
$out = array(); $out = array();
$num_lines = count($line_data); $head = max(1, $start - $context);
// make line numbers line up with array indexes for ($ii = $head; $ii < $start; $ii++) {
array_unshift($line_data, ''); $out[] = array(
'text' => $old_lines[$ii - 1],
$line_num = min($message->getLine(), $num_lines); 'number' => $ii,
$line_num = max(1, $line_num); );
// Print out preceding context before the impacted region.
$cursor = max(1, $line_num - $lines_of_context);
for (; $cursor < $line_num; $cursor++) {
$out[] = $this->renderLine($cursor, $line_data[$cursor]);
} }
$text = $message->getOriginalText(); for ($ii = $start; $ii < $start + $old_impact; $ii++) {
$start = $message->getChar() - 1; $out[] = array(
$patch = ''; 'text' => $old_lines[$ii - 1],
// Refine original and replacement text to eliminate start and end in common 'number' => $ii,
if ($message->isPatchable()) { 'type' => '-',
$patch = $message->getReplacementText(); 'chevron' => ($ii == $start),
$text_strlen = strlen($text); );
$patch_strlen = strlen($patch);
$min_length = min($text_strlen, $patch_strlen);
$same_at_front = 0;
for ($ii = 0; $ii < $min_length; $ii++) {
if ($text[$ii] !== $patch[$ii]) {
break;
}
$same_at_front++;
$start++;
if ($text[$ii] == "\n") {
$out[] = $this->renderLine($cursor, $line_data[$cursor]);
$cursor++;
$start = 0;
$line_num++;
}
}
// deal with shorter string ' ' longer string ' a '
$min_length -= $same_at_front;
// And check the end of the string
$same_at_end = 0;
for ($ii = 1; $ii <= $min_length; $ii++) {
if ($text[$text_strlen - $ii] !== $patch[$patch_strlen - $ii]) {
break;
}
$same_at_end++;
}
$text = substr(
$text,
$same_at_front,
$text_strlen - $same_at_end - $same_at_front);
$patch = substr(
$patch,
$same_at_front,
$patch_strlen - $same_at_end - $same_at_front);
}
// Print out the impacted region itself.
$diff = $message->isPatchable() ? '-' : null;
$text_lines = explode("\n", $text);
$text_length = count($text_lines);
$intraline = ($text != '' || $start || !preg_match('/\n$/', $patch));
if ($intraline) {
for (; $cursor < $line_num + $text_length; $cursor++) {
$chevron = ($cursor == $line_num);
// We may not have any data if, e.g., the old file does not exist.
$data = idx($line_data, $cursor, null);
// Highlight the problem substring.
$text_line = $text_lines[$cursor - $line_num];
if (strlen($text_line)) {
$data = substr_replace(
$data,
phutil_console_format('##%s##', $text_line),
($cursor == $line_num ? ($start > 0 ? $start : null) : 0),
strlen($text_line));
}
$out[] = $this->renderLine($cursor, $data, $chevron, $diff);
}
} }
// Print out replacement text. for ($ii = $start; $ii < $start + $new_impact; $ii++) {
if ($message->isPatchable()) { $out[] = array(
// Strip trailing newlines, since "explode" will create an extra patch 'text' => $new_lines[$ii - 1],
// line for these. 'type' => '+',
if (strlen($patch) && ($patch[strlen($patch) - 1] === "\n")) { 'chevron' => ($ii == $start),
$patch = substr($patch, 0, -1); );
}
$patch_lines = explode("\n", $patch);
$patch_length = count($patch_lines);
$patch_line = $patch_lines[0];
$len = isset($text_lines[0]) ? strlen($text_lines[0]) : 0;
$patched = phutil_console_format('##%s##', $patch_line);
if ($intraline) {
$patched = substr_replace(
$line_data[$line_num],
$patched,
$start,
$len);
}
$out[] = $this->renderLine(null, $patched, false, '+');
foreach (array_slice($patch_lines, 1) as $patch_line) {
$out[] = $this->renderLine(
null,
phutil_console_format('##%s##', $patch_line), false, '+');
}
} }
$end = min($num_lines, $cursor + $lines_of_context); $cursor = $start + $old_impact;
for (; $cursor < $end; $cursor++) { $foot = min(count($old_lines), $cursor + $context);
// If there is no original text, we didn't print out a chevron or any for ($ii = $cursor; $ii <= $foot; $ii++) {
// highlighted text above, so print it out here. This allows messages $out[] = array(
// which don't have any original/replacement information to still 'text' => $old_lines[$ii - 1],
// render with indicator chevrons. 'number' => $ii,
if ($text || $message->isPatchable()) { 'chevron' => ($ii == $cursor),
);
}
$result = array();
$seen_chevron = false;
foreach ($out as $spec) {
if ($seen_chevron) {
$chevron = false; $chevron = false;
} else { } else {
$chevron = ($cursor == $line_num); $chevron = !empty($spec['chevron']);
if ($chevron) {
$seen_chevron = true;
}
} }
$out[] = $this->renderLine($cursor, $line_data[$cursor], $chevron);
// With original text, we'll render the text highlighted above. If the $result[] = $this->renderLine(
// lint message only has a line/char offset there's nothing to idx($spec, 'number'),
// highlight, so print out a caret on the next line instead. $spec['text'],
if ($chevron && $message->getChar()) { $chevron,
$out[] = $this->renderCaret($message->getChar()); idx($spec, 'type'));
// If this is just a message and does not have a patch, put a little
// caret underneath the line to point out where the issue is.
if ($chevron) {
if (!$message->isPatchable() && !strlen($original)) {
$result[] = $this->renderCaret($char)."\n";
}
} }
} }
$out[] = null;
return implode("\n", $out); return implode('', $result);
} }
private function renderCaret($pos) { private function renderCaret($pos) {
@ -245,4 +285,28 @@ final class ArcanistConsoleLintRenderer extends ArcanistLintRenderer {
pht('No lint warnings.')); pht('No lint warnings.'));
} }
private function newOffsetMap($data) {
$lines = phutil_split_lines($data);
$line_map = array();
$number = 1;
$offset = 0;
foreach ($lines as $line) {
$line_map[$number] = $offset;
$number++;
$offset += strlen($line);
}
return $line_map;
}
private function highlightText($text) {
if ($this->getTestableMode()) {
return '>'.$text.'<';
} else {
return (string)tsprintf('##%s##', $text);
}
}
} }

View file

@ -0,0 +1,200 @@
<?php
final class ArcanistConsoleLintRendererTestCase
extends PhutilTestCase {
public function testRendering() {
$midline_original = <<<EOTEXT
import apple;
import banana;
import cat;
import dog;
EOTEXT;
$midline_replacement = <<<EOTEXT
import apple;
import banana;
import cat;
import dog;
EOTEXT;
$remline_original = <<<EOTEXT
import apple;
import banana;
import cat;
import dog;
EOTEXT;
$remline_replacement = <<<EOTEXT
import apple;
import banana;
import cat;
import dog;
EOTEXT;
$map = array(
'simple' => array(
'line' => 1,
'char' => 1,
'original' => 'a',
'replacement' => 'z',
),
'inline' => array(
'line' => 1,
'char' => 7,
'original' => 'cat',
'replacement' => 'dog',
),
// In this test, the original and replacement texts have a large
// amount of overlap.
'overlap' => array(
'line' => 1,
'char' => 1,
'original' => 'tantawount',
'replacement' => 'tantamount',
),
'newline' => array(
'line' => 6,
'char' => 1,
'original' => "\n",
'replacement' => '',
),
'addline' => array(
'line' => 3,
'char' => 1,
'original' => '',
'replacement' => "cherry\n",
),
'addlinesuffix' => array(
'line' => 2,
'char' => 7,
'original' => '',
'replacement' => "\ncherry",
),
'xml' => array(
'line' => 3,
'char' => 6,
'original' => '',
'replacement' => "\n",
),
'caret' => array(
'line' => 2,
'char' => 13,
'name' => 'Fruit Misinformation',
'description' => 'Arguably untrue.',
),
'original' => array(
'line' => 1,
'char' => 4,
'original' => 'should of',
),
'midline' => array(
'line' => 1,
'char' => 1,
'original' => $midline_original,
'replacement' => $midline_replacement,
),
'remline' => array(
'line' => 1,
'char' => 1,
'original' => $remline_original,
'replacement' => $remline_replacement,
),
'extrawhitespace' => array(
'line' => 2,
'char' => 1,
'original' => "\n",
'replacement' => '',
),
);
$defaults = array(
'severity' => ArcanistLintSeverity::SEVERITY_WARNING,
'name' => 'Lint Warning',
'path' => 'path/to/example.c',
'description' => 'Consider this.',
'code' => 'WARN123',
);
foreach ($map as $key => $test_case) {
$data = $this->readTestData("{$key}.txt");
$data = preg_replace('/~+\s*$/m', '', $data);
$expect = $this->readTestData("{$key}.expect");
$test_case = $test_case + $defaults;
$path = $test_case['path'];
$severity = $test_case['severity'];
$name = $test_case['name'];
$description = $test_case['description'];
$code = $test_case['code'];
$line = $test_case['line'];
$char = $test_case['char'];
$original = idx($test_case, 'original');
$replacement = idx($test_case, 'replacement');
$message = id(new ArcanistLintMessage())
->setPath($path)
->setSeverity($severity)
->setName($name)
->setDescription($description)
->setCode($code)
->setLine($line)
->setChar($char)
->setOriginalText($original)
->setReplacementText($replacement);
$result = id(new ArcanistLintResult())
->setPath($path)
->setData($data)
->addMessage($message);
$renderer = id(new ArcanistConsoleLintRenderer())
->setTestableMode(true);
try {
PhutilConsoleFormatter::disableANSI(true);
$actual = $renderer->renderLintResult($result);
PhutilConsoleFormatter::disableANSI(false);
} catch (Exception $ex) {
PhutilConsoleFormatter::disableANSI(false);
throw $ex;
}
// Trim "~" off the ends of lines. This allows the "expect" file to test
// for trailing whitespace without actually containing trailing
// whitespace.
$expect = preg_replace('/~+$/m', '', $expect);
$this->assertEqual(
$expect,
$actual,
pht(
'Lint rendering for "%s".',
$key));
}
}
private function readTestData($filename) {
$path = dirname(__FILE__).'/data/'.$filename;
return Filesystem::readFile($path);
}
}

View file

@ -0,0 +1,12 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
1 apple
2 banana
>>> + cherry
3 date
4 eclaire
5 fig

View file

@ -0,0 +1,5 @@
apple
banana
date
eclaire
fig

View file

@ -0,0 +1,12 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
1 apple
2 banana
>>> + cherry
3 date
4 eclaire
5 fig

View file

@ -0,0 +1,5 @@
apple
banana
date
eclaire
fig

View file

@ -0,0 +1,11 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Fruit Misinformation
Arguably untrue.
1 Apples are round.
>>> 2 Bananas are round.
^
3 Cherries are round.
4 Dates are round.

View file

@ -0,0 +1,4 @@
Apples are round.
Bananas are round.
Cherries are round.
Dates are round.

View file

@ -0,0 +1,8 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
1 Adrift upon the sea.
>>> - 2 ~

View file

@ -0,0 +1,3 @@
Adrift upon the sea.
~

View file

@ -0,0 +1,8 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
>>> - 1 adjudi>cat<ed
+ adjudi>dog<ed

View file

@ -0,0 +1 @@
adjudicated

View file

@ -0,0 +1,11 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
1 import apple;
2 import banana;
>>> + ~
3 import cat;
4 import dog;

View file

@ -0,0 +1,4 @@
import apple;
import banana;
import cat;
import dog;

View file

@ -0,0 +1,14 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
3 ccc
4 ddd
5 eee
>>> - 6 ~
7 fff
8 ggg
9 hhh
10 iii

View file

@ -0,0 +1,11 @@
aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii
jjj

View file

@ -0,0 +1,7 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
>>> 1 He >should of< known.

View file

@ -0,0 +1 @@
He should of known.

View file

@ -0,0 +1,8 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
>>> - 1 tanta>w<ount
+ tanta>m<ount

View file

@ -0,0 +1 @@
tantawount

View file

@ -0,0 +1,12 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
1 import apple;
2 import banana;
3 ~
>>> - 4 ~
5 import cat;
6 import dog;

View file

@ -0,0 +1,6 @@
import apple;
import banana;
import cat;
import dog;

View file

@ -0,0 +1,10 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
>>> - 1 >a<
+ >z<
2 b
3 c

View file

@ -0,0 +1,3 @@
a
b
c

View file

@ -0,0 +1,12 @@
>>> Lint for path/to/example.c:
Warning (WARN123) Lint Warning
Consider this.
1 <
2 wow
>>> - 3 xml>
+ xml
+ >
4 <xml />

View file

@ -0,0 +1,4 @@
<
wow
xml>
<xml />

View file

@ -446,6 +446,9 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
// would ship up the binaries for 'arc patch' but display the textconv // would ship up the binaries for 'arc patch' but display the textconv
// output in the visual diff. // output in the visual diff.
'--no-textconv', '--no-textconv',
// Provide a standard view of submodule changes; the 'log' and 'diff'
// values do not parse by the diff parser.
'--submodule=short',
); );
return implode(' ', $options); return implode(' ', $options);
} }
@ -544,8 +547,10 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
} }
$uri = rtrim($stdout); $uri = rtrim($stdout);
// 'origin' is what ls-remote outputs if no origin remote URI exists // ls-remote echos the remote name (ie 'origin') if no remote URI is found
if (!$uri || $uri === 'origin') { // TODO: In 2.7.0 (circa 2016) git introduced `git remote get-url`
// with saner error handling.
if (!$uri || $uri === $remote) {
return null; return null;
} }
@ -1422,7 +1427,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
* or cycle locally. * or cycle locally.
* *
* @param string Ref to start from. * @param string Ref to start from.
* @return list<wild> Path to an upstream. * @return ArcanistGitUpstreamPath Path to an upstream.
*/ */
public function getPathToUpstream($start) { public function getPathToUpstream($start) {
$cursor = $start; $cursor = $start;

View file

@ -804,7 +804,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
'commit --amend -l %s', 'commit --amend -l %s',
$tmp_file); $tmp_file);
} catch (CommandException $ex) { } catch (CommandException $ex) {
if (preg_match('/nothing changed/', $ex->getStdOut())) { if (preg_match('/nothing changed/', $ex->getStdout())) {
// NOTE: Mercurial considers it an error to make a no-op amend. Although // NOTE: Mercurial considers it an error to make a no-op amend. Although
// we generally defer to the underlying VCS to dictate behavior, this // we generally defer to the underlying VCS to dictate behavior, this
// one seems a little goofy, and we use amend as part of various // one seems a little goofy, and we use amend as part of various

View file

@ -282,7 +282,7 @@ abstract class ArcanistRepositoryAPI extends Phobject {
* Hook for implementations to dirty working copy caches after the working * Hook for implementations to dirty working copy caches after the working
* copy has been updated. * copy has been updated.
* *
* @return this * @return void
* @task status * @task status
*/ */
protected function didReloadWorkingCopy() { protected function didReloadWorkingCopy() {

View file

@ -255,7 +255,7 @@ class XUnitTestEngine extends ArcanistUnitTestEngine {
throw $exc; throw $exc;
} }
$result->setResult(ArcanistUnitTestResult::RESULT_FAIL); $result->setResult(ArcanistUnitTestResult::RESULT_FAIL);
$result->setUserdata($exc->getStdout()); $result->setUserData($exc->getStdout());
} }
$result->setDuration(microtime(true) - $regenerate_start); $result->setDuration(microtime(true) - $regenerate_start);
@ -301,7 +301,7 @@ class XUnitTestEngine extends ArcanistUnitTestEngine {
throw $exc; throw $exc;
} }
$result->setResult(ArcanistUnitTestResult::RESULT_FAIL); $result->setResult(ArcanistUnitTestResult::RESULT_FAIL);
$result->setUserdata($exc->getStdout()); $result->setUserData($exc->getStdout());
$build_failed = true; $build_failed = true;
} }

View file

@ -28,7 +28,7 @@ final class XUnitTestResultParserTestCase extends PhutilTestCase {
$parsed_results = id(new ArcanistXUnitTestResultParser()) $parsed_results = id(new ArcanistXUnitTestResultParser())
->parseTestResults(''); ->parseTestResults('');
$this->failTest(pht('Should throw on empty input')); $this->assertFailure(pht('Should throw on empty input'));
} catch (Exception $e) { } catch (Exception $e) {
// OK // OK
} }
@ -43,7 +43,7 @@ final class XUnitTestResultParserTestCase extends PhutilTestCase {
$parsed_results = id(new ArcanistXUnitTestResultParser()) $parsed_results = id(new ArcanistXUnitTestResultParser())
->parseTestResults($stubbed_results); ->parseTestResults($stubbed_results);
$this->failTest(pht('Should throw on non-xml input')); $this->assertFailure(pht('Should throw on non-xml input'));
} catch (Exception $e) { } catch (Exception $e) {
// OK // OK
} }

View file

@ -21,7 +21,8 @@ final class ArcanistUnitConsoleRenderer extends ArcanistUnitRenderer {
$this->getFormattedResult($result->getResult()).$duration, $this->getFormattedResult($result->getResult()).$duration,
$test_name); $test_name);
if ($result_code != ArcanistUnitTestResult::RESULT_PASS) { if ($result_code != ArcanistUnitTestResult::RESULT_PASS
&& strlen($result->getUserData())) {
$return .= $result->getUserData()."\n"; $return .= $result->getUserData()."\n";
} }

View file

@ -222,15 +222,6 @@ final class ArcanistFileDataRef extends Phobject {
$path)); $path));
} }
$hash = @sha1_file($path);
if ($hash === false) {
throw new Exception(
pht(
'Unable to upload file: failed to calculate file data hash for '.
'path "%s".',
$path));
}
$size = @filesize($path); $size = @filesize($path);
if ($size === false) { if ($size === false) {
throw new Exception( throw new Exception(
@ -240,11 +231,11 @@ final class ArcanistFileDataRef extends Phobject {
$path)); $path));
} }
$this->hash = $hash; $this->hash = $this->newFileHash($path);
$this->size = $size; $this->size = $size;
} else { } else {
$data = $this->data; $data = $this->data;
$this->hash = sha1($data); $this->hash = $this->newDataHash($data);
$this->size = strlen($data); $this->size = strlen($data);
} }
} }
@ -354,4 +345,24 @@ final class ArcanistFileDataRef extends Phobject {
return $data; return $data;
} }
private function newFileHash($path) {
$hash = hash_file('sha256', $path, $raw_output = false);
if ($hash === false) {
return null;
}
return $hash;
}
private function newDataHash($data) {
$hash = hash('sha256', $data, $raw_output = false);
if ($hash === false) {
return null;
}
return $hash;
}
} }

View file

@ -1355,7 +1355,7 @@ EOTEXT
'Unit testing raised errors, but all '. 'Unit testing raised errors, but all '.
'failing tests are unsound.')); 'failing tests are unsound.'));
} else { } else {
$continue = $this->console->confirm( $continue = phutil_console_confirm(
pht( pht(
'Unit test results included failures, but all failing tests '. 'Unit test results included failures, but all failing tests '.
'are known to be unsound. Ignore unsound test failures?')); 'are known to be unsound. Ignore unsound test failures?'));
@ -1962,7 +1962,13 @@ EOTEXT
$faux_message[] = pht('CC: %s', $this->getArgument('cc')); $faux_message[] = pht('CC: %s', $this->getArgument('cc'));
} }
// See T12069. After T10312, the first line of a message is always parsed
// as a title. Add a placeholder so "Reviewers" and "CC" are never the
// first line.
$placeholder_title = pht('<placeholder>');
if ($faux_message) { if ($faux_message) {
array_unshift($faux_message, $placeholder_title);
$faux_message = implode("\n\n", $faux_message); $faux_message = implode("\n\n", $faux_message);
$local = array( $local = array(
'(Flags) ' => array( '(Flags) ' => array(
@ -2034,6 +2040,10 @@ EOTEXT
continue; continue;
} }
if ($title === $placeholder_title) {
continue;
}
if (!isset($result['title'])) { if (!isset($result['title'])) {
// We don't have a title yet, so use this one. // We don't have a title yet, so use this one.
$result['title'] = $title; $result['title'] = $title;

View file

@ -79,6 +79,172 @@ EOTEXT
public function run() { public function run() {
$conduit = $this->getConduit(); $conduit = $this->getConduit();
$id = $this->id;
$display_name = 'F'.$id;
$is_show = $this->show;
$save_as = $this->saveAs;
$path = null;
try {
$file = $conduit->callMethodSynchronous(
'file.search',
array(
'constraints' => array(
'ids' => array($id),
),
));
$data = $file['data'];
if (!$data) {
throw new ArcanistUsageException(
pht(
'File "%s" is not a valid file, or not visible.',
$display_name));
}
$file = head($data);
$data_uri = idxv($file, array('fields', 'dataURI'));
if ($data_uri === null) {
throw new ArcanistUsageException(
pht(
'File "%s" can not be downloaded.',
$display_name));
}
if ($is_show) {
// Skip all the file path stuff if we're just going to echo the
// file contents.
} else {
if ($save_as !== null) {
$path = Filesystem::resolvePath($save_as);
$try_unique = false;
} else {
$path = idxv($file, array('fields', 'name'), $display_name);
$path = basename($path);
$path = Filesystem::resolvePath($path);
$try_unique = true;
}
if ($try_unique) {
$path = Filesystem::writeUniqueFile($path, '');
} else {
if (Filesystem::pathExists($path)) {
throw new ArcanistUsageException(
pht(
'File "%s" already exists.',
$save_as));
}
Filesystem::writeFile($path, '');
}
$display_path = Filesystem::readablePath($path);
}
$size = idxv($file, array('fields', 'size'), 0);
if ($is_show) {
$file_handle = null;
} else {
$file_handle = fopen($path, 'ab+');
if ($file_handle === false) {
throw new Exception(
pht(
'Failed to open file "%s" for writing.',
$path));
}
$this->writeInfo(
pht('DATA'),
pht(
'Downloading "%s" (%s byte(s))...',
$display_name,
new PhutilNumber($size)));
}
$future = new HTTPSFuture($data_uri);
// For small files, don't bother drawing a progress bar.
$minimum_bar_bytes = (1024 * 1024 * 4);
if ($is_show || ($size < $minimum_bar_bytes)) {
$bar = null;
} else {
$bar = id(new PhutilConsoleProgressBar())
->setTotal($size);
}
// TODO: We should stream responses to disk, but cURL gives us the raw
// HTTP response data and BaseHTTPFuture can not currently parse it in
// a stream-oriented way. Until this is resolved, buffer the file data
// in memory and write it to disk in one shot.
list($status, $data) = $future->resolve();
if ($status->getStatusCode() !== 200) {
throw new Exception(
pht(
'Got HTTP %d status response, expected HTTP 200.',
$status->getStatusCode()));
}
if (strlen($data)) {
if ($is_show) {
echo $data;
} else {
$ok = fwrite($file_handle, $data);
if ($ok === false) {
throw new Exception(
pht(
'Failed to write file data to "%s".',
$path));
}
}
}
if ($bar) {
$bar->update(strlen($data));
}
if ($bar) {
$bar->done();
}
if ($file_handle) {
$ok = fclose($file_handle);
if ($ok === false) {
throw new Exception(
pht(
'Failed to close file handle for "%s".',
$path));
}
}
if (!$is_show) {
$this->writeOkay(
pht('DONE'),
pht(
'Saved "%s" as "%s".',
$display_name,
$display_path));
}
return 0;
} catch (Exception $ex) {
// If we created an empty file, clean it up.
if (!$is_show) {
if ($path !== null) {
Filesystem::remove($path);
}
}
// If we fail for any reason, fall back to the older mechanism using
// "file.info" and "file.download".
}
$this->writeStatusMessage(pht('Getting file information...')."\n"); $this->writeStatusMessage(pht('Getting file information...')."\n");
$info = $conduit->callMethodSynchronous( $info = $conduit->callMethodSynchronous(
'file.info', 'file.info',

View file

@ -17,7 +17,6 @@ final class ArcanistLandWorkflow extends ArcanistWorkflow {
private $remote; private $remote;
private $useSquash; private $useSquash;
private $keepBranch; private $keepBranch;
private $shouldUpdateWithRebase;
private $branchType; private $branchType;
private $ontoType; private $ontoType;
private $preview; private $preview;
@ -197,43 +196,8 @@ EOTEXT
'conflicts' => array( 'conflicts' => array(
'keep-branch' => true, 'keep-branch' => true,
), ),
),
'update-with-rebase' => array(
'help' => pht(
"When updating the feature branch, use rebase instead of merge. ".
"This might make things work better in some cases. Set ".
"%s to '%s' to make this the default.",
'arc.land.update.default',
'rebase'),
'conflicts' => array(
'merge' => pht(
'The %s strategy does not update the feature branch.',
'--merge'),
'update-with-merge' => pht(
'Cannot be used with %s.',
'--update-with-merge'),
),
'supports' => array( 'supports' => array(
'git', 'hg',
),
),
'update-with-merge' => array(
'help' => pht(
"When updating the feature branch, use merge instead of rebase. ".
"This is the default behavior. Setting %s to '%s' can also be ".
"used to make this the default.",
'arc.land.update.default',
'merge'),
'conflicts' => array(
'merge' => pht(
'The %s strategy does not update the feature branch.',
'--merge'),
'update-with-rebase' => pht(
'Cannot be used with %s.',
'--update-with-rebase'),
),
'supports' => array(
'git',
), ),
), ),
'revision' => array( 'revision' => array(
@ -261,22 +225,6 @@ EOTEXT
if ($engine) { if ($engine) {
$this->readEngineArguments(); $this->readEngineArguments();
$obsolete = array(
'delete-remote',
'update-with-merge',
'update-with-rebase',
);
foreach ($obsolete as $flag) {
if ($this->getArgument($flag)) {
throw new ArcanistUsageException(
pht(
'Flag "%s" is no longer supported under Git.',
'--'.$flag));
}
}
$this->requireCleanWorkingCopy(); $this->requireCleanWorkingCopy();
$should_hold = $this->getArgument('hold'); $should_hold = $this->getArgument('hold');
@ -514,16 +462,6 @@ EOTEXT
$this->branch = head($branch); $this->branch = head($branch);
$this->keepBranch = $this->getArgument('keep-branch'); $this->keepBranch = $this->getArgument('keep-branch');
$update_strategy = $this->getConfigFromAnySource(
'arc.land.update.default',
'merge');
$this->shouldUpdateWithRebase = $update_strategy == 'rebase';
if ($this->getArgument('update-with-rebase')) {
$this->shouldUpdateWithRebase = true;
} else if ($this->getArgument('update-with-merge')) {
$this->shouldUpdateWithRebase = false;
}
$this->preview = $this->getArgument('preview'); $this->preview = $this->getArgument('preview');
if (!$this->branchType) { if (!$this->branchType) {
@ -888,7 +826,7 @@ EOTEXT
} }
} catch (CommandException $ex) { } catch (CommandException $ex) {
$err = $ex->getError(); $err = $ex->getError();
$stdout = $ex->getStdOut(); $stdout = $ex->getStdout();
// Copied from: PhabricatorRepositoryPullLocalDaemon.php // Copied from: PhabricatorRepositoryPullLocalDaemon.php
// NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the
@ -953,42 +891,7 @@ EOTEXT
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
chdir($repository_api->getPath()); chdir($repository_api->getPath());
if ($this->isGit) { if ($this->isHg) {
if ($this->shouldUpdateWithRebase) {
echo phutil_console_format(pht(
'Rebasing **%s** onto **%s**',
$this->branch,
$this->onto)."\n");
$err = phutil_passthru('git rebase %s', $this->onto);
if ($err) {
throw new ArcanistUsageException(pht(
"'%s' failed. You can abort with '%s', or resolve conflicts ".
"and use '%s' to continue forward. After resolving the rebase, ".
"run '%s' again.",
sprintf('git rebase %s', $this->onto),
'git rebase --abort',
'git rebase --continue',
'arc land'));
}
} else {
echo phutil_console_format(pht(
'Merging **%s** into **%s**',
$this->branch,
$this->onto)."\n");
$err = phutil_passthru(
'git merge --no-stat %s -m %s',
$this->onto,
pht("Automatic merge by '%s'", 'arc land'));
if ($err) {
throw new ArcanistUsageException(pht(
"'%s' failed. To continue: resolve the conflicts, commit ".
"the changes, then run '%s' again. To abort: run '%s'.",
sprintf('git merge %s', $this->onto),
'arc land',
'git merge --abort'));
}
}
} else if ($this->isHg) {
$onto_tip = $repository_api->getCanonicalRevisionName($this->onto); $onto_tip = $repository_api->getCanonicalRevisionName($this->onto);
$common_ancestor = $repository_api->getCanonicalRevisionName( $common_ancestor = $repository_api->getCanonicalRevisionName(
hgsprintf('ancestor(%s, %s)', $this->onto, $this->branch)); hgsprintf('ancestor(%s, %s)', $this->onto, $this->branch));
@ -1369,31 +1272,7 @@ EOTEXT
} }
if ($this->getArgument('delete-remote')) { if ($this->getArgument('delete-remote')) {
if ($this->isGit) { if ($this->isHg) {
list($err, $ref) = $repository_api->execManualLocal(
'rev-parse --verify %s/%s',
$this->remote,
$this->branch);
if ($err) {
echo pht(
'No remote feature %s to clean up.',
$this->branchType);
echo "\n";
} else {
// NOTE: In Git, you delete a remote branch by pushing it with a
// colon in front of its name:
//
// git push <remote> :<branch>
echo pht('Cleaning up remote feature %s...', $this->branchType), "\n";
$repository_api->execxLocal(
'push %s :%s',
$this->remote,
$this->branch);
}
} else if ($this->isHg) {
// named branches were closed as part of the earlier commit // named branches were closed as part of the earlier commit
// so only worry about bookmarks // so only worry about bookmarks
if ($repository_api->isBookmark($this->branch)) { if ($repository_api->isBookmark($this->branch)) {
@ -1538,7 +1417,7 @@ EOTEXT
pht('Harbormaster URI'), pht('Harbormaster URI'),
$buildable['uri']); $buildable['uri']);
if (!$console->confirm($prompt)) { if (!phutil_console_confirm($prompt)) {
throw new ArcanistUserAbortException(); throw new ArcanistUserAbortException();
} }
} }

View file

@ -227,7 +227,7 @@ EOTEXT
} }
if ($everything) { if ($everything) {
$paths = iterator_to_array($this->getRepositoryApi()->getAllFiles()); $paths = iterator_to_array($this->getRepositoryAPI()->getAllFiles());
$this->shouldLintAll = true; $this->shouldLintAll = true;
} else { } else {
$paths = $this->selectPathsForWorkflow($paths, $rev); $paths = $this->selectPathsForWorkflow($paths, $rev);
@ -518,7 +518,7 @@ EOTEXT
$prompt = pht( $prompt = pht(
'Apply this patch to %s?', 'Apply this patch to %s?',
phutil_console_format('__%s__', $result->getPath())); phutil_console_format('__%s__', $result->getPath()));
if (!$console->confirm($prompt, $default = true)) { if (!phutil_console_confirm($prompt, $default_no = false)) {
continue; continue;
} }
} }
@ -546,7 +546,7 @@ EOTEXT
pht('Automatically amending HEAD with lint patches.')); pht('Automatically amending HEAD with lint patches.'));
$amend = true; $amend = true;
} else { } else {
$amend = $console->confirm(pht('Amend HEAD with lint patches?')); $amend = phutil_console_confirm(pht('Amend HEAD with lint patches?'));
} }
if ($amend) { if ($amend) {

View file

@ -36,7 +36,7 @@ EOTEXT
'param' => 'search', 'param' => 'search',
'repeat' => true, 'repeat' => true,
'help' => pht( 'help' => pht(
'Search for linters. Search is case-insensitive, and is performed'. 'Search for linters. Search is case-insensitive, and is performed '.
'against name and description of each linter.'), 'against name and description of each linter.'),
), ),
'*' => 'exact', '*' => 'exact',

View file

@ -430,6 +430,18 @@ EOTEXT
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
$has_base_revision = $repository_api->hasLocalCommit( $has_base_revision = $repository_api->hasLocalCommit(
$bundle->getBaseRevision()); $bundle->getBaseRevision());
if (!$has_base_revision) {
if ($repository_api instanceof ArcanistGitAPI) {
echo phutil_console_format(
"<bg:blue>** %s **</bg> %s\n",
pht('INFO'),
pht('Base commit is not in local repository; trying to fetch.'));
$repository_api->execManualLocal('fetch --quiet --all');
$has_base_revision = $repository_api->hasLocalCommit(
$bundle->getBaseRevision());
}
}
if ($this->canBranch() && if ($this->canBranch() &&
($this->shouldBranch() || ($this->shouldBranch() ||
($this->shouldCommit() && $has_base_revision))) { ($this->shouldCommit() && $has_base_revision))) {
@ -710,7 +722,7 @@ EOTEXT
} }
// in case there were any submodule changes involved // in case there were any submodule changes involved
$repository_api->execpassthru('submodule update --init --recursive'); $repository_api->execPassthru('submodule update --init --recursive');
if ($this->shouldCommit()) { if ($this->shouldCommit()) {
if ($bundle->getFullAuthor()) { if ($bundle->getFullAuthor()) {
@ -763,7 +775,7 @@ EOTEXT
echo phutil_console_format( echo phutil_console_format(
"\n<bg:red>** %s **</bg>\n", "\n<bg:red>** %s **</bg>\n",
pht('Patch Failed!')); pht('Patch Failed!'));
$stderr = $ex->getStdErr(); $stderr = $ex->getStderr();
if (preg_match('/case-folding collision/', $stderr)) { if (preg_match('/case-folding collision/', $stderr)) {
echo phutil_console_wrap( echo phutil_console_wrap(
phutil_console_format( phutil_console_format(

View file

@ -69,6 +69,19 @@ EOTEXT
$settings = new ArcanistSettings(); $settings = new ArcanistSettings();
$console = PhutilConsole::getConsole();
if (!$settings->getHelp($key)) {
$warning = tsprintf(
"**%s:** %s\n",
pht('Warning'),
pht(
'The configuration key "%s" is not recognized by arc. It may '.
'be misspelled or out of date.',
$key));
$console->writeErr('%s', $warning);
}
$old = null; $old = null;
if (array_key_exists($key, $config)) { if (array_key_exists($key, $config)) {
$old = $config[$key]; $old = $config[$key];
@ -85,17 +98,18 @@ EOTEXT
$old = $settings->formatConfigValueForDisplay($key, $old); $old = $settings->formatConfigValueForDisplay($key, $old);
if ($old === null) { if ($old === null) {
echo pht( $message = pht(
"Deleted key '%s' from %s config.\n", 'Deleted key "%s" from %s config.',
$key, $key,
$which); $which);
} else { } else {
echo pht( $message = pht(
"Deleted key '%s' from %s config (was %s).\n", 'Deleted key "%s" from %s config (was %s).',
$key, $key,
$which, $which,
$old); $old);
} }
$console->writeOut('%s', tsprintf("%s\n", $message));
} else { } else {
$val = $settings->willWriteValue($key, $val); $val = $settings->willWriteValue($key, $val);
@ -110,19 +124,20 @@ EOTEXT
$old = $settings->formatConfigValueForDisplay($key, $old); $old = $settings->formatConfigValueForDisplay($key, $old);
if ($old === null) { if ($old === null) {
echo pht( $message = pht(
"Set key '%s' = %s in %s config.\n", 'Set key "%s" = %s in %s config.',
$key, $key,
$val, $val,
$which); $which);
} else { } else {
echo pht( $message = pht(
"Set key '%s' = %s in %s config (was %s).\n", 'Set key "%s" = %s in %s config (was %s).',
$key, $key,
$val, $val,
$which, $which,
$old); $old);
} }
$console->writeOut('%s', tsprintf("%s\n", $message));
} }
return 0; return 0;

View file

@ -76,7 +76,7 @@ EOTEXT
"%s: %s\n\n", "%s: %s\n\n",
pht('Started'), pht('Started'),
implode(', ', ipull($phid_query, 'fullName'))); implode(', ', ipull($phid_query, 'fullName')));
$this->printCurrentTracking(true); $this->printCurrentTracking();
} }
} }

View file

@ -105,7 +105,7 @@ EOTEXT
"%s %s\n\n", "%s %s\n\n",
pht('Stopped:'), pht('Stopped:'),
implode(', ', ipull($phid_query, 'fullName'))); implode(', ', ipull($phid_query, 'fullName')));
$this->printCurrentTracking(true); $this->printCurrentTracking();
} }
} }

View file

@ -81,7 +81,7 @@ EOTEXT
$unassigned = $this->getArgument('unassigned'); $unassigned = $this->getArgument('unassigned');
if ($owner) { if ($owner) {
$owner_phid = $this->findOwnerPhid($owner); $owner_phid = $this->findOwnerPHID($owner);
} else if ($unassigned) { } else if ($unassigned) {
$owner_phid = null; $owner_phid = null;
} else { } else {

View file

@ -140,7 +140,7 @@ EOTEXT
} }
if ($everything) { if ($everything) {
$paths = iterator_to_array($this->getRepositoryApi()->getAllFiles()); $paths = iterator_to_array($this->getRepositoryAPI()->getAllFiles());
} else { } else {
$paths = $this->selectPathsForWorkflow($paths, $rev); $paths = $this->selectPathsForWorkflow($paths, $rev);
} }

View file

@ -51,8 +51,14 @@ EOTEXT
pht('%s is not a git working copy.', $lib)); pht('%s is not a git working copy.', $lib));
} }
list($stdout) = $repository->execxLocal('log -1 --format=%s', '%H %ct'); // NOTE: Carefully execute these commands in a way that works on Windows
list($commit, $timestamp) = explode(' ', $stdout); // until T8298 is properly fixed. See PHI52.
list($commit) = $repository->execxLocal('log -1 --format=%%H');
$commit = trim($commit);
list($timestamp) = $repository->execxLocal('log -1 --format=%%ct');
$timestamp = trim($timestamp);
$console->writeOut( $console->writeOut(
"%s %s (%s)\n", "%s %s (%s)\n",

View file

@ -338,7 +338,7 @@ abstract class ArcanistWorkflow extends Phobject {
$this->conduitAuthenticated = true; $this->conduitAuthenticated = true;
return; return $this;
} catch (Exception $ex) { } catch (Exception $ex) {
$conduit->setConduitToken(null); $conduit->setConduitToken(null);
throw $ex; throw $ex;
@ -1011,9 +1011,7 @@ abstract class ArcanistWorkflow extends Phobject {
} }
$should_commit = true; $should_commit = true;
} else { } else {
$permit_autostash = $this->getConfigFromAnySource( $permit_autostash = $this->getConfigFromAnySource('arc.autostash');
'arc.autostash',
false);
if ($permit_autostash && $api->canStashChanges()) { if ($permit_autostash && $api->canStashChanges()) {
echo pht( echo pht(
'Stashing uncommitted changes. (You can restore them with `%s`).', 'Stashing uncommitted changes. (You can restore them with `%s`).',
@ -1199,6 +1197,14 @@ abstract class ArcanistWorkflow extends Phobject {
$future = $conduit->callMethod('differential.querydiffs', $params); $future = $conduit->callMethod('differential.querydiffs', $params);
$diff = head($future->resolve()); $diff = head($future->resolve());
if ($diff == null) {
throw new Exception(
phutil_console_wrap(
pht("The diff or revision you specified is either invalid or you ".
"don't have permission to view it."))
);
}
$changes = array(); $changes = array();
foreach ($diff['changes'] as $changedict) { foreach ($diff['changes'] as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict); $changes[] = ArcanistDiffChange::newFromDictionary($changedict);