Page MenuHomePhorge

No OneTemporary

diff --git a/src/lint/engine/base/ArcanistLintEngine.php b/src/lint/engine/base/ArcanistLintEngine.php
index 6f98259b..0cc6c043 100644
--- a/src/lint/engine/base/ArcanistLintEngine.php
+++ b/src/lint/engine/base/ArcanistLintEngine.php
@@ -1,273 +1,286 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Manages lint execution. When you run 'arc lint' or 'arc diff', Arcanist
* checks your .arcconfig to see if you have specified a lint engine in the
* key "lint_engine". The engine must extend this class. For example:
*
* lang=js
* {
* // ...
* "lint_engine" : "ExampleLintEngine",
* // ...
* }
*
* The lint engine is given a list of paths (generally, the paths that you
* modified in your change) and determines which linters to run on them. The
* linters themselves are responsible for actually analyzing file text and
* finding warnings and errors. For example, if the modified paths include some
* JS files and some Python files, you might want to run JSLint on the JS files
* and PyLint on the Python files.
*
* You can also run multiple linters on a single file. For instance, you might
* run one linter on all text files to make sure they don't have trailing
* whitespace, or enforce tab vs space rules, or make sure there are enough
* curse words in them.
*
* Because lint engines are pretty custom to the rules of a project, you will
* generally need to build your own. Fortunately, it's pretty easy (and you
* can use the prebuilt //linters//, you just need to write a little glue code
* to tell Arcanist which linters to run). For a simple example of how to build
* a lint engine, see @{class:ExampleLintEngine}.
*
* You can test an engine like this:
*
* arc lint --engine ExampleLintEngine --lintall some_file.py
*
* ...which will show you all the lint issues raised in the file.
*
* See @{article@phabricator:Arcanist User Guide: Customizing Lint, Unit Tests
* and Workflows} for more information about configuring lint engines.
*
* @group lint
*/
abstract class ArcanistLintEngine {
protected $workingCopy;
protected $fileData = array();
protected $charToLine = array();
protected $lineToFirstChar = array();
private $results = array();
private $minimumSeverity = ArcanistLintSeverity::SEVERITY_DISABLED;
private $changedLines = array();
+ private $textChanges = array();
private $commitHookMode = false;
public function __construct() {
}
public function setWorkingCopy(ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
public function getWorkingCopy() {
return $this->workingCopy;
}
public function setPaths($paths) {
$this->paths = $paths;
return $this;
}
public function getPaths() {
return $this->paths;
}
public function setPathChangedLines($path, array $changed) {
$this->changedLines[$path] = array_fill_keys($changed, true);
return $this;
}
public function getPathChangedLines($path) {
return idx($this->changedLines, $path);
}
+ public function setTextChange($path) {
+ $this->textChanges[$path] = true;
+ return $this;
+ }
+
+ public function isTextChange($path) {
+ return idx($this->textChanges, $path, false);
+ }
+
public function setFileData($data) {
$this->fileData = $data + $this->fileData;
return $this;
}
public function setCommitHookMode($mode) {
$this->commitHookMode = $mode;
return $this;
}
protected function loadData($path) {
if (!isset($this->fileData[$path])) {
$disk_path = $this->getFilePathOnDisk($path);
$this->fileData[$path] = Filesystem::readFile($disk_path);
}
return $this->fileData[$path];
}
public function pathExists($path) {
if ($this->getCommitHookMode()) {
return (idx($this->fileData, $path) !== null);
} else {
$disk_path = $this->getFilePathOnDisk($path);
return Filesystem::pathExists($disk_path);
}
}
public function getFilePathOnDisk($path) {
return Filesystem::resolvePath(
$path,
$this->getWorkingCopy()->getProjectRoot());
}
public function setMinimumSeverity($severity) {
$this->minimumSeverity = $severity;
return $this;
}
public function getCommitHookMode() {
return $this->commitHookMode;
}
public function run() {
$stopped = array();
$linters = $this->buildLinters();
if (!$linters) {
throw new ArcanistNoEffectException("No linters to run.");
}
$have_paths = false;
foreach ($linters as $linter) {
if ($linter->getPaths()) {
$have_paths = true;
break;
}
}
if (!$have_paths) {
throw new ArcanistNoEffectException("No paths are lintable.");
}
foreach ($linters as $linter) {
$linter->setEngine($this);
$paths = $linter->getPaths();
foreach ($paths as $key => $path) {
// Make sure each path has a result generated, even if it is empty
// (i.e., the file has no lint messages).
$result = $this->getResultForPath($path);
if (isset($stopped[$path])) {
unset($paths[$key]);
}
}
$paths = array_values($paths);
if ($paths) {
$linter->willLintPaths($paths);
foreach ($paths as $path) {
$linter->willLintPath($path);
$linter->lintPath($path);
if ($linter->didStopAllLinters()) {
$stopped[$path] = true;
}
}
}
$minimum = $this->minimumSeverity;
foreach ($linter->getLintMessages() as $message) {
if (!ArcanistLintSeverity::isAtLeastAsSevere($message, $minimum)) {
continue;
}
- // When a user runs "arc diff", we default to raising only warnings on
+ // When a user runs "arc lint", we default to raising only warnings on
// lines they have changed (errors are still raised anywhere in the
// file).
+ $text_change = $this->isTextChange($message->getPath());
$changed = $this->getPathChangedLines($message->getPath());
- if ($changed !== null && !$message->isError()) {
+ if ($text_change === true &&
+ $changed !== null &&
+ !$message->isError()) {
if (empty($changed[$message->getLine()])) {
continue;
}
}
$result = $this->getResultForPath($message->getPath());
$result->addMessage($message);
}
}
foreach ($this->results as $path => $result) {
$disk_path = $this->getFilePathOnDisk($path);
$result->setFilePathOnDisk($disk_path);
if (isset($this->fileData[$path])) {
$result->setData($this->fileData[$path]);
} else if ($disk_path && Filesystem::pathExists($disk_path)) {
// TODO: this may cause us to, e.g., load a large binary when we only
// raised an error about its filename. We could refine this by looking
// through the lint messages and doing this load only if any of them
// have original/replacement text or something like that.
try {
$this->fileData[$path] = Filesystem::readFile($disk_path);
$result->setData($this->fileData[$path]);
} catch (FilesystemException $ex) {
// Ignore this, it's noncritical that we access this data and it
// might be unreadable or a directory or whatever else for plenty
// of legitimate reasons.
}
}
}
return $this->results;
}
abstract protected function buildLinters();
private function getResultForPath($path) {
if (empty($this->results[$path])) {
$result = new ArcanistLintResult();
$result->setPath($path);
$this->results[$path] = $result;
}
return $this->results[$path];
}
public function getLineAndCharFromOffset($path, $offset) {
if (!isset($this->charToLine[$path])) {
$char_to_line = array();
$line_to_first_char = array();
$lines = explode("\n", $this->loadData($path));
$line_number = 0;
$line_start = 0;
foreach ($lines as $line) {
$len = strlen($line) + 1; // Account for "\n".
$line_to_first_char[] = $line_start;
$line_start += $len;
for ($ii = 0; $ii < $len; $ii++) {
$char_to_line[] = $line_number;
}
$line_number++;
}
$this->charToLine[$path] = $char_to_line;
$this->lineToFirstChar[$path] = $line_to_first_char;
}
$line = $this->charToLine[$path][$offset];
$char = $offset - $this->lineToFirstChar[$path][$line];
return array($line, $char);
}
}
diff --git a/src/workflow/base/ArcanistBaseWorkflow.php b/src/workflow/base/ArcanistBaseWorkflow.php
index 9bf75f58..d0008fec 100644
--- a/src/workflow/base/ArcanistBaseWorkflow.php
+++ b/src/workflow/base/ArcanistBaseWorkflow.php
@@ -1,921 +1,926 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Implements a runnable command, like "arc diff" or "arc help".
*
* = Managing Conduit =
*
* Workflows have the builtin ability to open a Conduit connection to a
* Phabricator installation, so methods can be invoked over the API. Workflows
* may either not need this (e.g., "help"), or may need a Conduit but not
* authentication (e.g., calling only public APIs), or may need a Conduit and
* authentication (e.g., "arc diff").
*
* To specify that you need an //unauthenticated// conduit, override
* @{method:requiresConduit} to return ##true##. To specify that you need an
* //authenticated// conduit, override @{method:requiresAuthentication} to
* return ##true##. You can also manually invoke @{method:establishConduit}
* and/or @{method:authenticateConduit} later in a workflow to upgrade it.
* Once a conduit is open, you can access the client by calling
* @{method:getConduit}, which allows you to invoke methods. You can get
* verified information about the user identity by calling @{method:getUserPHID}
* or @{method:getUserName} after authentication occurs.
*
* @task conduit Conduit
* @group workflow
*/
class ArcanistBaseWorkflow {
private $conduit;
private $conduitURI;
private $conduitCredentials;
private $conduitAuthenticated;
private $userPHID;
private $userName;
private $repositoryAPI;
private $workingCopy;
private $arguments;
private $command;
private $arcanistConfiguration;
private $parentWorkflow;
private $workingDirectory;
private $changeCache = array();
public function __construct() {
}
/* -( Conduit )------------------------------------------------------------ */
/**
* Set the URI which the workflow will open a conduit connection to when
* @{method:establishConduit} is called. Arcanist makes an effort to set
* this by default for all workflows (by reading ##.arcconfig## and/or the
* value of ##--conduit-uri##) even if they don't need Conduit, so a workflow
* can generally upgrade into a conduit workflow later by just calling
* @{method:establishConduit}.
*
* You generally should not need to call this method unless you are
* specifically overriding the default URI. It is normally sufficient to
* just invoke @{method:establishConduit}.
*
* NOTE: You can not call this after a conduit has been established.
*
* @param string The URI to open a conduit to when @{method:establishConduit}
* is called.
* @return this
* @task conduit
*/
final public function setConduitURI($conduit_uri) {
if ($this->conduit) {
throw new Exception(
"You can not change the Conduit URI after a conduit is already open.");
}
$this->conduitURI = $conduit_uri;
return $this;
}
/**
* Open a conduit channel to the server which was previously configured by
* calling @{method:setConduitURI}. Arcanist will do this automatically if
* the workflow returns ##true## from @{method:requiresConduit}, or you can
* later upgrade a workflow and build a conduit by invoking it manually.
*
* You must establish a conduit before you can make conduit calls.
*
* NOTE: You must call @{method:setConduitURI} before you can call this
* method.
*
* @return this
* @task conduit
*/
final public function establishConduit() {
if ($this->conduit) {
return $this;
}
if (!$this->conduitURI) {
throw new Exception(
"You must specify a Conduit URI with setConduitURI() before you can ".
"establish a conduit.");
}
$this->conduit = new ConduitClient($this->conduitURI);
return $this;
}
/**
* Set credentials which will be used to authenticate against Conduit. These
* credentials can then be used to establish an authenticated connection to
* conduit by calling @{method:authenticateConduit}. Arcanist sets some
* defaults for all workflows regardless of whether or not they return true
* from @{method:requireAuthentication}, based on the ##~/.arcrc## and
* ##.arcconf## files if they are present. Thus, you can generally upgrade a
* workflow which does not require authentication into an authenticated
* workflow by later invoking @{method:requireAuthentication}. You should not
* normally need to call this method unless you are specifically overriding
* the defaults.
*
* NOTE: You can not call this method after calling
* @{method:authenticateConduit}.
*
* @param dict A credential dictionary, see @{method:authenticateConduit}.
* @return this
* @task conduit
*/
final public function setConduitCredentials(array $credentials) {
if ($this->conduitAuthenticated) {
throw new Exception(
"You may not set new credentials after authenticating conduit.");
}
$this->conduitCredentials = $credentials;
return $this;
}
/**
* Open and authenticate a conduit connection to a Phabricator server using
* provided credentials. Normally, Arcanist does this for you automatically
* when you return true from @{method:requiresAuthentication}, but you can
* also upgrade an existing workflow to one with an authenticated conduit
* by invoking this method manually.
*
* You must authenticate the conduit before you can make authenticated conduit
* calls (almost all calls require authentication).
*
* This method uses credentials provided via @{method:setConduitCredentials}
* to authenticate to the server:
*
* - ##user## (required) The username to authenticate with.
* - ##certificate## (required) The Conduit certificate to use.
* - ##description## (optional) Description of the invoking command.
*
* Successful authentication allows you to call @{method:getUserPHID} and
* @{method:getUserName}, as well as use the client you access with
* @{method:getConduit} to make authenticated calls.
*
* NOTE: You must call @{method:setConduitURI} and
* @{method:setConduitCredentials} before you invoke this method.
*
* @return this
* @task conduit
*/
final public function authenticateConduit() {
if ($this->conduitAuthenticated) {
return $this;
}
$this->establishConduit();
$credentials = $this->conduitCredentials;
if (!$credentials) {
throw new Exception(
"Set conduit credentials with setConduitCredentials() before ".
"authenticating conduit!");
}
if (empty($credentials['user']) || empty($credentials['certificate'])) {
throw new Exception(
"Credentials must include a 'user' and a 'certificate'.");
}
$description = idx($credentials, 'description', '');
$user = $credentials['user'];
$certificate = $credentials['certificate'];
try {
$connection = $this->getConduit()->callMethodSynchronous(
'conduit.connect',
array(
'client' => 'arc',
'clientVersion' => 2,
'clientDescription' => php_uname('n').':'.$description,
'user' => $user,
'certificate' => $certificate,
'host' => $this->conduitURI,
));
} catch (ConduitClientException $ex) {
if ($ex->getErrorCode() == 'ERR-NO-CERTIFICATE' ||
$ex->getErrorCode() == 'ERR-INVALID-USER') {
$conduit_uri = $this->conduitURI;
$message =
"\n".
phutil_console_format(
"YOU NEED TO __INSTALL A CERTIFICATE__ TO LOGIN TO PHABRICATOR").
"\n\n".
phutil_console_format(
" To do this, run: **arc install-certificate**").
"\n\n".
"The server '{$conduit_uri}' rejected your request:".
"\n".
$ex->getMessage();
throw new ArcanistUsageException($message);
} else {
throw $ex;
}
}
$this->userName = $user;
$this->userPHID = $connection['userPHID'];
$this->conduitAuthenticated = true;
return $this;
}
/**
* Override this to return true if your workflow requires a conduit channel.
* Arc will build the channel for you before your workflow executes. This
* implies that you only need an unauthenticated channel; if you need
* authentication, override @{method:requiresAuthentication}.
*
* @return bool True if arc should build a conduit channel before running
* the workflow.
* @task conduit
*/
public function requiresConduit() {
return false;
}
/**
* Override this to return true if your workflow requires an authenticated
* conduit channel. This implies that it requires a conduit. Arc will build
* and authenticate the channel for you before the workflow executes.
*
* @return bool True if arc should build an authenticated conduit channel
* before running the workflow.
* @task conduit
*/
public function requiresAuthentication() {
return false;
}
/**
* Returns the PHID for the user once they've authenticated via Conduit.
*
* @return phid Authenticated user PHID.
* @task conduit
*/
final public function getUserPHID() {
if (!$this->userPHID) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires authentication, override ".
"requiresAuthentication() to return true.");
}
return $this->userPHID;
}
/**
* Deprecated. See @{method:getUserPHID}.
*
* @deprecated
*/
final public function getUserGUID() {
phutil_deprecated(
'ArcanistBaseWorkflow::getUserGUID',
'This method has been renamed to getUserPHID().');
return $this->getUserPHID();
}
/**
* Return the username for the user once they've authenticated via Conduit.
*
* @return string Authenticated username.
* @task conduit
*/
final public function getUserName() {
return $this->userName;
}
/**
* Get the established @{class@libphutil:ConduitClient} in order to make
* Conduit method calls. Before the client is available it must be connected,
* either implicitly by making @{method:requireConduit} or
* @{method:requireAuthentication} return true, or explicitly by calling
* @{method:establishConduit} or @{method:authenticateConduit}.
*
* @return @{class@libphutil:ConduitClient} Live conduit client.
* @task conduit
*/
final public function getConduit() {
if (!$this->conduit) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a Conduit, override ".
"requiresConduit() to return true.");
}
return $this->conduit;
}
public function setArcanistConfiguration($arcanist_configuration) {
$this->arcanistConfiguration = $arcanist_configuration;
return $this;
}
public function getArcanistConfiguration() {
return $this->arcanistConfiguration;
}
public function getCommandHelp() {
return get_class($this).": Undocumented";
}
public function requiresWorkingCopy() {
return false;
}
public function requiresRepositoryAPI() {
return false;
}
public function setCommand($command) {
$this->command = $command;
return $this;
}
public function getCommand() {
return $this->command;
}
public function getArguments() {
return array();
}
public function setWorkingDirectory($working_directory) {
$this->workingDirectory = $working_directory;
return $this;
}
public function getWorkingDirectory() {
return $this->workingDirectory;
}
private function setParentWorkflow($parent_workflow) {
$this->parentWorkflow = $parent_workflow;
return $this;
}
protected function getParentWorkflow() {
return $this->parentWorkflow;
}
public function buildChildWorkflow($command, array $argv) {
$arc_config = $this->getArcanistConfiguration();
$workflow = $arc_config->buildWorkflow($command);
$workflow->setParentWorkflow($this);
$workflow->setCommand($command);
if ($this->repositoryAPI) {
$workflow->setRepositoryAPI($this->repositoryAPI);
}
if ($this->userPHID) {
$workflow->userPHID = $this->getUserPHID();
$workflow->userName = $this->getUserName();
}
if ($this->conduit) {
$workflow->conduit = $this->conduit;
}
if ($this->workingCopy) {
$workflow->setWorkingCopy($this->workingCopy);
}
$workflow->setArcanistConfiguration($arc_config);
$workflow->parseArguments(array_values($argv));
return $workflow;
}
public function getArgument($key, $default = null) {
$args = $this->arguments;
if (!array_key_exists($key, $args)) {
return $default;
}
return $args[$key];
}
final public function getCompleteArgumentSpecification() {
$spec = $this->getArguments();
$arc_config = $this->getArcanistConfiguration();
$command = $this->getCommand();
$spec += $arc_config->getCustomArgumentsForCommand($command);
return $spec;
}
public function parseArguments(array $args) {
$spec = $this->getCompleteArgumentSpecification();
$dict = array();
$more_key = null;
if (!empty($spec['*'])) {
$more_key = $spec['*'];
unset($spec['*']);
$dict[$more_key] = array();
}
$short_to_long_map = array();
foreach ($spec as $long => $options) {
if (!empty($options['short'])) {
$short_to_long_map[$options['short']] = $long;
}
}
$more = array();
for ($ii = 0; $ii < count($args); $ii++) {
$arg = $args[$ii];
$arg_name = null;
$arg_key = null;
if ($arg == '--') {
$more = array_merge(
$more,
array_slice($args, $ii + 1));
break;
} else if (!strncmp($arg, '--', 2)) {
$arg_key = substr($arg, 2);
if (!array_key_exists($arg_key, $spec)) {
throw new ArcanistUsageException(
"Unknown argument '{$arg_key}'. Try 'arc help'.");
}
} else if (!strncmp($arg, '-', 1)) {
$arg_key = substr($arg, 1);
if (empty($short_to_long_map[$arg_key])) {
throw new ArcanistUsageException(
"Unknown argument '{$arg_key}'. Try 'arc help'.");
}
$arg_key = $short_to_long_map[$arg_key];
} else {
$more[] = $arg;
continue;
}
$options = $spec[$arg_key];
if (empty($options['param'])) {
$dict[$arg_key] = true;
} else {
if ($ii == count($args) - 1) {
throw new ArcanistUsageException(
"Option '{$arg}' requires a parameter.");
}
$dict[$arg_key] = $args[$ii + 1];
$ii++;
}
}
if ($more) {
if ($more_key) {
$dict[$more_key] = $more;
} else {
$example = reset($more);
throw new ArcanistUsageException(
"Unrecognized argument '{$example}'. Try 'arc help'.");
}
}
foreach ($dict as $key => $value) {
if (empty($spec[$key]['conflicts'])) {
continue;
}
foreach ($spec[$key]['conflicts'] as $conflict => $more) {
if (isset($dict[$conflict])) {
if ($more) {
$more = ': '.$more;
} else {
$more = '.';
}
// TODO: We'll always display these as long-form, when the user might
// have typed them as short form.
throw new ArcanistUsageException(
"Arguments '--{$key}' and '--{$conflict}' are mutually exclusive".
$more);
}
}
}
$this->arguments = $dict;
$this->didParseArguments();
return $this;
}
protected function didParseArguments() {
// Override this to customize workflow argument behavior.
}
public function getWorkingCopy() {
if (!$this->workingCopy) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a working copy, override ".
"requiresWorkingCopy() to return true.");
}
return $this->workingCopy;
}
public function setWorkingCopy(
ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
public function setRepositoryAPI($api) {
$this->repositoryAPI = $api;
return $this;
}
public function getRepositoryAPI() {
if (!$this->repositoryAPI) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a Repository API, override ".
"requiresRepositoryAPI() to return true.");
}
return $this->repositoryAPI;
}
protected function shouldRequireCleanUntrackedFiles() {
return empty($this->arguments['allow-untracked']);
}
public function requireCleanWorkingCopy() {
$api = $this->getRepositoryAPI();
$working_copy_desc = phutil_console_format(
" Working copy: __%s__\n\n",
$api->getPath());
$untracked = $api->getUntrackedChanges();
if ($this->shouldRequireCleanUntrackedFiles()) {
if (!empty($untracked)) {
echo "You have untracked files in this working copy.\n\n".
$working_copy_desc.
" Untracked files in working copy:\n".
" ".implode("\n ", $untracked)."\n\n";
if ($api instanceof ArcanistGitAPI) {
echo phutil_console_wrap(
"Since you don't have '.gitignore' rules for these files and have ".
"not listed them in '.git/info/exclude', you may have forgotten ".
"to 'git add' them to your commit.");
} else if ($api instanceof ArcanistSubversionAPI) {
echo phutil_console_wrap(
"Since you don't have 'svn:ignore' rules for these files, you may ".
"have forgotten to 'svn add' them.");
} else if ($api instanceof ArcanistMercurialAPI) {
echo phutil_console_wrap(
"Since you don't have '.hgignore' rules for these files, you ".
"may have forgotten to 'hg add' them to your commit.");
}
$prompt = "Do you want to continue without adding these files?";
if (!phutil_console_confirm($prompt, $default_no = false)) {
throw new ArcanistUserAbortException();
}
}
}
$incomplete = $api->getIncompleteChanges();
if ($incomplete) {
throw new ArcanistUsageException(
"You have incompletely checked out directories in this working copy. ".
"Fix them before proceeding.\n\n".
$working_copy_desc.
" Incomplete directories in working copy:\n".
" ".implode("\n ", $incomplete)."\n\n".
"You can fix these paths by running 'svn update' on them.");
}
$conflicts = $api->getMergeConflicts();
if ($conflicts) {
throw new ArcanistUsageException(
"You have merge conflicts in this working copy. Resolve merge ".
"conflicts before proceeding.\n\n".
$working_copy_desc.
" Conflicts in working copy:\n".
" ".implode("\n ", $conflicts)."\n");
}
$unstaged = $api->getUnstagedChanges();
if ($unstaged) {
throw new ArcanistUsageException(
"You have unstaged changes in this working copy. Stage and commit (or ".
"revert) them before proceeding.\n\n".
$working_copy_desc.
" Unstaged changes in working copy:\n".
" ".implode("\n ", $unstaged)."\n");
}
$uncommitted = $api->getUncommittedChanges();
if ($uncommitted) {
throw new ArcanistUsageException(
"You have uncommitted changes in this branch. Commit (or revert) them ".
"before proceeding.\n\n".
$working_copy_desc.
" Uncommitted changes in working copy\n".
" ".implode("\n ", $uncommitted)."\n");
}
}
protected function chooseRevision(
array $revision_data,
$revision_id,
$prompt = null) {
$revisions = array();
foreach ($revision_data as $data) {
$ref = ArcanistDifferentialRevisionRef::newFromDictionary($data);
$revisions[$ref->getID()] = $ref;
}
if ($revision_id) {
$revision_id = $this->normalizeRevisionID($revision_id);
if (empty($revisions[$revision_id])) {
throw new ArcanistChooseInvalidRevisionException();
}
return $revisions[$revision_id];
}
if (!count($revisions)) {
throw new ArcanistChooseNoRevisionsException();
}
$repository_api = $this->getRepositoryAPI();
$candidates = array();
$cur_path = $repository_api->getPath();
foreach ($revisions as $revision) {
$source_path = $revision->getSourcePath();
if ($source_path == $cur_path) {
$candidates[] = $revision;
}
}
if (count($candidates) == 1) {
$candidate = reset($candidates);
$revision_id = $candidate->getID();
}
if ($revision_id) {
return $revisions[$revision_id];
}
$revision_indexes = array_keys($revisions);
echo "\n";
$ii = 1;
foreach ($revisions as $revision) {
echo ' ['.$ii++.'] D'.$revision->getID().' '.$revision->getName()."\n";
}
while (true) {
$id = phutil_console_prompt($prompt);
$id = trim(strtoupper($id), 'D');
if (isset($revisions[$id])) {
return $revisions[$id];
}
if (isset($revision_indexes[$id - 1])) {
return $revisions[$revision_indexes[$id - 1]];
}
}
}
protected function loadDiffBundleFromConduit(
ConduitClient $conduit,
$diff_id) {
return $this->loadBundleFromConduit(
$conduit,
array(
'diff_id' => $diff_id,
));
}
protected function loadRevisionBundleFromConduit(
ConduitClient $conduit,
$revision_id) {
return $this->loadBundleFromConduit(
$conduit,
array(
'revision_id' => $revision_id,
));
}
private function loadBundleFromConduit(
ConduitClient $conduit,
$params) {
$future = $conduit->callMethod('differential.getdiff', $params);
$diff = $future->resolve();
$changes = array();
foreach ($diff['changes'] as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setConduit($conduit);
return $bundle;
}
+ protected function isTextChange($path) {
+ $change = $this->getChange($path);
+ return $change->getFileType() == ArcanistDiffChangeType::FILE_TEXT;
+ }
+
protected function getChangedLines($path, $mode) {
if (is_dir($path)) {
return array();
}
$change = $this->getChange($path);
$lines = $change->getChangedLines($mode);
return array_keys($lines);
}
private function getChange($path) {
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) {
// NOTE: In SVN, we don't currently support a "get all local changes"
// operation, so special case it.
if (empty($this->changeCache[$path])) {
$diff = $repository_api->getRawDiffText($path);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
if (count($changes) != 1) {
throw new Exception("Expected exactly one change.");
}
$this->changeCache[$path] = reset($changes);
}
} else if ($repository_api->supportsRelativeLocalCommits()) {
if (empty($this->changeCache)) {
$changes = $repository_api->getAllLocalChanges();
foreach ($changes as $change) {
$this->changeCache[$change->getCurrentPath()] = $change;
}
}
} else {
throw new Exception("Missing VCS support.");
}
if (empty($this->changeCache[$path])) {
if ($repository_api instanceof ArcanistGitAPI) {
// This can legitimately occur under git if you make a change, "git
// commit" it, and then revert the change in the working copy and run
// "arc lint".
$change = new ArcanistDiffChange();
$change->setCurrentPath($path);
return $change;
} else {
throw new Exception(
"Trying to get change for unchanged path '{$path}'!");
}
}
return $this->changeCache[$path];
}
final public function willRunWorkflow() {
$spec = $this->getCompleteArgumentSpecification();
foreach ($this->arguments as $arg => $value) {
if (empty($spec[$arg])) {
continue;
}
$options = $spec[$arg];
if (!empty($options['supports'])) {
$system_name = $this->getRepositoryAPI()->getSourceControlSystemName();
if (!in_array($system_name, $options['supports'])) {
$extended_info = null;
if (!empty($options['nosupport'][$system_name])) {
$extended_info = ' '.$options['nosupport'][$system_name];
}
throw new ArcanistUsageException(
"Option '--{$arg}' is not supported under {$system_name}.".
$extended_info);
}
}
}
}
protected function normalizeRevisionID($revision_id) {
return ltrim(strtoupper($revision_id), 'D');
}
protected function shouldShellComplete() {
return true;
}
protected function getShellCompletions(array $argv) {
return array();
}
protected function getSupportedRevisionControlSystems() {
return array('any');
}
protected function getPassthruArgumentsAsMap($command) {
$map = array();
foreach ($this->getCompleteArgumentSpecification() as $key => $spec) {
if (!empty($spec['passthru'][$command])) {
if (isset($this->arguments[$key])) {
$map[$key] = $this->arguments[$key];
}
}
}
return $map;
}
protected function getPassthruArgumentsAsArgv($command) {
$spec = $this->getCompleteArgumentSpecification();
$map = $this->getPassthruArgumentsAsMap($command);
$argv = array();
foreach ($map as $key => $value) {
$argv[] = '--'.$key;
if (!empty($spec[$key]['param'])) {
$argv[] = $value;
}
}
return $argv;
}
public static function getUserConfigurationFileLocation() {
return getenv('HOME').'/.arcrc';
}
public static function readUserConfigurationFile() {
$user_config = array();
$user_config_path = self::getUserConfigurationFileLocation();
if (Filesystem::pathExists($user_config_path)) {
$mode = fileperms($user_config_path);
if (!$mode) {
throw new Exception("Unable to get perms of '{$user_config_path}'!");
}
if ($mode & 0177) {
// Mode should allow only owner access.
$prompt = "File permissions on your ~/.arcrc are too open. ".
"Fix them by chmod'ing to 600?";
if (!phutil_console_confirm($prompt, $default_no = false)) {
throw new ArcanistUsageException("Set ~/.arcrc to file mode 600.");
}
execx('chmod 600 %s', $user_config_path);
}
$user_config_data = Filesystem::readFile($user_config_path);
$user_config = json_decode($user_config_data, true);
if (!is_array($user_config)) {
throw new ArcanistUsageException(
"Your '~/.arcrc' file is not a valid JSON file.");
}
}
return $user_config;
}
/**
* Write a message to stderr so that '--json' flags or stdout which is meant
* to be piped somewhere aren't disrupted.
*
* @param string Message to write to stderr.
* @return void
*/
protected function writeStatusMessage($msg) {
file_put_contents('php://stderr', $msg);
}
protected function isHistoryImmutable() {
$working_copy = $this->getWorkingCopy();
return ($working_copy->getConfig('immutable_history') === true);
}
}
diff --git a/src/workflow/base/__init__.php b/src/workflow/base/__init__.php
index a596254a..b59c83a1 100644
--- a/src/workflow/base/__init__.php
+++ b/src/workflow/base/__init__.php
@@ -1,25 +1,26 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'differential/revision');
phutil_require_module('arcanist', 'exception');
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'exception/usage/userabort');
phutil_require_module('arcanist', 'parser/bundle');
phutil_require_module('arcanist', 'parser/diff');
phutil_require_module('arcanist', 'parser/diff/change');
+phutil_require_module('arcanist', 'parser/diff/changetype');
phutil_require_module('phutil', 'conduit/client');
phutil_require_module('phutil', 'console');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'moduleutils');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistBaseWorkflow.php');
diff --git a/src/workflow/lint/ArcanistLintWorkflow.php b/src/workflow/lint/ArcanistLintWorkflow.php
index c98d6a26..ccb1c3ed 100644
--- a/src/workflow/lint/ArcanistLintWorkflow.php
+++ b/src/workflow/lint/ArcanistLintWorkflow.php
@@ -1,303 +1,311 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Runs lint rules on changes.
*
* @group workflow
*/
class ArcanistLintWorkflow extends ArcanistBaseWorkflow {
const RESULT_OKAY = 0;
const RESULT_WARNINGS = 1;
const RESULT_ERRORS = 2;
const RESULT_SKIP = 3;
private $unresolvedMessages;
private $shouldAmendChanges = false;
public function setShouldAmendChanges($should_amend) {
$this->shouldAmendChanges = $should_amend;
return $this;
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**lint** [__options__] [__paths__] (svn)
**lint** [__options__] [__commit_range__] (git)
Supports: git, svn
Run static analysis on changes to check for mistakes. If no files
are specified, lint will be run on all files which have been modified.
EOTEXT
);
}
public function getArguments() {
return array(
'lintall' => array(
'help' =>
"Show all lint warnings, not just those on changed lines."
),
'output' => array(
'param' => 'format',
'help' =>
"With 'summary', show lint warnings in a more compact format. ".
"With 'json', show lint warnings in machine-readable JSON format."
),
'advice' => array(
'help' =>
"Show lint advice, not just warnings and errors."
),
'engine' => array(
'param' => 'classname',
'help' =>
"Override configured lint engine for this project."
),
'apply-patches' => array(
'help' =>
'Apply patches suggested by lint to the working copy without '.
'prompting.',
'conflicts' => array(
'never-apply-patches' => true,
),
),
'never-apply-patches' => array(
'help' => 'Never apply patches suggested by lint.',
'conflicts' => array(
'apply-patches' => true,
),
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function run() {
$working_copy = $this->getWorkingCopy();
$engine = $this->getArgument('engine');
if (!$engine) {
$engine = $working_copy->getConfig('lint_engine');
if (!$engine) {
throw new ArcanistNoEngineException(
"No lint engine configured for this project. Edit .arcconfig to ".
"specify a lint engine.");
}
}
$should_lint_all = $this->getArgument('lintall');
$repository_api = null;
if (!$should_lint_all) {
try {
$repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
$working_copy);
$this->setRepositoryAPI($repository_api);
} catch (ArcanistUsageException $ex) {
throw new ArcanistUsageException(
$ex->getMessage()."\n\n".
"Use '--lintall' to ignore working copy changes when running lint.");
}
if ($repository_api instanceof ArcanistSubversionAPI) {
$paths = $repository_api->getWorkingCopyStatus();
$list = new FileList($this->getArgument('paths'));
foreach ($paths as $path => $flags) {
if (!$list->contains($path)) {
unset($paths[$path]);
}
}
} else if ($repository_api->supportsRelativeLocalCommits()) {
$repository_api->parseRelativeLocalCommit(
$this->getArgument('paths', array()));
$paths = $repository_api->getWorkingCopyStatus();
} else {
throw new Exception("Unknown VCS!");
}
foreach ($paths as $path => $flags) {
if ($flags & ArcanistRepositoryAPI::FLAG_UNTRACKED) {
unset($paths[$path]);
}
}
$paths = array_keys($paths);
} else {
$paths = $this->getArgument('paths');
if (empty($paths)) {
throw new ArcanistUsageException(
"You must specify one or more files to lint when using '--lintall'.");
}
foreach ($paths as $key => $path) {
$full_path = Filesystem::resolvePath($path);
if (!Filesystem::pathExists($full_path)) {
throw new ArcanistUsageException("Path '{$path}' does not exist!");
}
$relative_path = Filesystem::readablePath(
$full_path,
$working_copy->getProjectRoot());
$paths[$key] = $relative_path;
}
}
PhutilSymbolLoader::loadClass($engine);
$engine = newv($engine, array());
$engine->setWorkingCopy($working_copy);
if ($this->getArgument('advice')) {
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ADVICE);
} else {
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_WARNING);
}
+ // Propagate information about which lines changed to the lint engine.
+ // This is used so that the lint engine can drop messages concerning
+ // lines that weren't in the change.
$engine->setPaths($paths);
if (!$should_lint_all) {
foreach ($paths as $path) {
- $engine->setPathChangedLines(
- $path,
- $this->getChangedLines($path, 'new'));
+ // Explicitly flag text changes, as line information doesn't apply
+ // to non-text files.
+ if ($this->isTextChange($path)) {
+ $engine->setTextChange($path);
+ $engine->setPathChangedLines(
+ $path,
+ $this->getChangedLines($path, 'new'));
+ }
}
}
$results = $engine->run();
if ($this->getArgument('never-apply-patches')) {
$apply_patches = false;
} else {
$apply_patches = true;
}
if ($this->getArgument('apply-patches')) {
$prompt_patches = false;
} else {
$prompt_patches = true;
}
$wrote_to_disk = false;
switch ($this->getArgument('output')) {
case 'json':
$renderer = new ArcanistLintJSONRenderer();
break;
case 'summary':
$renderer = new ArcanistLintSummaryRenderer();
break;
default:
$renderer = new ArcanistLintRenderer();
break;
}
foreach ($results as $result) {
if (!$result->getMessages()) {
continue;
}
echo $renderer->renderLintResult($result);
if ($apply_patches && $result->isPatchable()) {
$patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
$old = $patcher->getUnmodifiedFileContent();
$new = $patcher->getModifiedFileContent();
if ($prompt_patches) {
$old_file = $result->getFilePathOnDisk();
if (!Filesystem::pathExists($old_file)) {
$old_file = '/dev/null';
}
$new_file = new TempFile();
Filesystem::writeFile($new_file, $new);
// TODO: Improve the behavior here, make it more like
// difference_render().
passthru(csprintf("diff -u %s %s", $old_file, $new_file));
$prompt = phutil_console_format(
"Apply this patch to __%s__?",
$result->getPath());
if (!phutil_console_confirm($prompt, $default_no = false)) {
continue;
}
}
$patcher->writePatchToDisk();
$wrote_to_disk = true;
}
}
if ($wrote_to_disk &&
($repository_api instanceof ArcanistGitAPI) &&
$this->shouldAmendChanges) {
$amend = phutil_console_confirm("Amend HEAD with lint patches?");
if ($amend) {
execx(
'(cd %s; git commit -a --amend -C HEAD)',
$repository_api->getPath());
} else {
throw new ArcanistUsageException(
"Sort out the lint changes that were applied to the working ".
"copy and relint.");
}
}
$unresolved = array();
$has_warnings = false;
$has_errors = false;
foreach ($results as $result) {
foreach ($result->getMessages() as $message) {
if (!$message->isPatchApplied()) {
if ($message->isError()) {
$has_errors = true;
} else if ($message->isWarning()) {
$has_warnings = true;
}
$unresolved[] = $message;
}
}
}
$this->unresolvedMessages = $unresolved;
// Take the most severe lint message severity and use that
// as the result code.
if ($has_errors) {
$result_code = self::RESULT_ERRORS;
} else if ($has_warnings) {
$result_code = self::RESULT_WARNINGS;
} else {
$result_code = self::RESULT_OKAY;
}
if (!$this->getParentWorkflow()) {
if ($result_code == self::RESULT_OKAY) {
echo phutil_console_format(
"<bg:green>** OKAY **</bg> No lint warnings.\n");
}
}
return $result_code;
}
public function getUnresolvedMessages() {
return $this->unresolvedMessages;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Jan 19 2025, 22:51 (6 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129570
Default Alt Text
(48 KB)

Event Timeline