Page MenuHomeSealhub

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/.arcconfig b/.arcconfig
index 5c31439a..02e401e6 100644
--- a/.arcconfig
+++ b/.arcconfig
@@ -1,10 +1,9 @@
{
"project_id" : "arcanist",
"conduit_uri" : "https://secure.phabricator.com/",
"lint.engine" : "PhutilLintEngine",
"unit.engine" : "PhutilUnitTestEngine",
- "copyright_holder" : "Facebook, Inc.",
"phutil_libraries" : {
"arcanist" : "src/"
}
}
diff --git a/LICENSE b/LICENSE
index 60686e6c..f433b1a5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,13 +1,177 @@
-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
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
- http://www.apache.org/licenses/LICENSE-2.0
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-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.
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 00000000..346d6bfb
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,7 @@
+Arcanist
+Copyright 2012 Facebook, Inc.
+
+This product includes software developed at
+Facebook, Inc. (http://www.facebook.com/facebook).
+
+Libraries in externals/ have their own licenses and copyright holders.
diff --git a/README b/README
index e4866f27..ebf16161 100644
--- a/README
+++ b/README
@@ -1,12 +1,16 @@
WHAT IS ARCANIST?
Arcanist is the command-line tool for Phabricator. It allows you to interact
with Phabricator installs to send code for review, download patches, transfer
files, view status, make API calls, and various other things. You can find
a complete user guide here:
http://www.phabricator.com/docs/phabricator/article/Arcanist_User_Guide.html
For more information about Phabricator, see:
http://phabricator.org/
+
+LICENSE
+
+Arcanist is released under the Apache 2.0 license except as otherwise noted.
diff --git a/resources/test/diverse_symbols.php.example b/resources/test/diverse_symbols.php.example
index 88b16daf..1049c180 100644
--- a/resources/test/diverse_symbols.php.example
+++ b/resources/test/diverse_symbols.php.example
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
// This file has diverse symbol declarations and requirements, and can be used
// to test changes to phutil_symbols.php.
/**
* @phutil-external-symbol function ext_func
* @phutil-external-symbol class ExtClass
* @phutil-external-symbol interface ExtInterface
*/
ext_func();
new ExtClass();
class L implements ExtInterface { }
function f() { }
(function () {
// Anonymous function.
});
g();
$g();
$$g();
X::f();
call_user_func();
call_user_func('h');
call_user_func($var);
class A { }
class C extends B { }
class D extends C { }
new U();
V::m();
W::$n;
P::CONST;
interface ILocal extends IForeign { }
class CLocal extends INonlocal { }
strtoupper('');
// Various magic things.
die($x);
empty($x);
isset($x);
echo($x);
print($x);
exit($x);
include($x);
include_once($x);
require($x);
require_once($x);
\ No newline at end of file
diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php
index 05118b59..8f82ded6 100644
--- a/scripts/__init_script__.php
+++ b/scripts/__init_script__.php
@@ -1,110 +1,94 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Adjust 'include_path' to add locations where we'll search for libphutil.
* We look in these places:
*
* - Next to 'arcanist/'.
* - Anywhere in the normal PHP 'include_path'.
* - Inside 'arcanist/externals/includes/'.
*
* When looking in these places, we expect to find a 'libphutil/' directory.
*/
function arcanist_adjust_php_include_path() {
// The 'arcanist/' directory.
$arcanist_dir = dirname(dirname(__FILE__));
// The parent directory of 'arcanist/'.
$parent_dir = dirname($arcanist_dir);
// The 'arcanist/externals/includes/' directory.
$include_dir = implode(
DIRECTORY_SEPARATOR,
array(
$arcanist_dir,
'externals',
'includes',
));
$php_include_path = ini_get('include_path');
$php_include_path = implode(
PATH_SEPARATOR,
array(
$parent_dir,
$php_include_path,
$include_dir,
));
ini_set('include_path', $php_include_path);
}
arcanist_adjust_php_include_path();
@include_once 'libphutil/scripts/__init_script__.php';
if (!@constant('__LIBPHUTIL__')) {
echo "ERROR: Unable to load libphutil. Put libphutil/ next to arcanist/, or ".
"update your PHP 'include_path' to include the parent directory of ".
"libphutil/, or symlink libphutil/ into arcanist/externals/includes/.\n";
exit(1);
}
PhutilTranslator::getInstance()
->addTranslations(array(
'Locally modified path(s) are not included in this revision:' => array(
'A locally modified path is not included in this revision:',
'Locally modified paths are not included in this revision:',
),
'They will NOT be committed. Commit this revision anyway?' => array(
'It will NOT be committed. Commit this revision anyway?',
'They will NOT be committed. Commit this revision anyway?',
),
'Revision includes changes to path(s) that do not exist:' => array(
'Revision includes changes to a path that does not exist:',
'Revision includes changes to paths that do not exist:',
),
'This diff includes file(s) which are not valid UTF-8 (they contain '.
'invalid byte sequences). You can either stop this workflow and fix '.
'these files, or continue. If you continue, these files will be '.
'marked as binary.' => array(
'This diff includes a file which is not valid UTF-8 (it has invalid '.
'byte sequences). You can either stop this workflow and fix it, or '.
'continue. If you continue, this file will be marked as binary.',
'This diff includes files which are not valid UTF-8 (they contain '.
'invalid byte sequences). You can either stop this workflow and fix '.
'these files, or continue. If you continue, these files will be '.
'marked as binary.',
),
'AFFECTED FILE(S)' => array('AFFECTED FILE', 'AFFECTED FILES'),
'Do you want to mark these files as binary and continue?' => array(
'Do you want to mark this file as binary and continue?',
'Do you want to mark these files as binary and continue?',
),
'line(s)' => array('line', 'lines'),
'%d test(s)' => array('%d test', '%d tests'),
'%d assertion(s) passed.' => array(
'%d assertion passed.',
'%d assertions passed.',
),
));
phutil_load_library(dirname(dirname(__FILE__)).'/src/');
diff --git a/scripts/arcanist.php b/scripts/arcanist.php
index d51baaac..01a3a0e1 100755
--- a/scripts/arcanist.php
+++ b/scripts/arcanist.php
@@ -1,546 +1,530 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 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.
- */
-
sanity_check_environment();
require_once dirname(__FILE__).'/__init_script__.php';
ini_set('memory_limit', -1);
$original_argv = $argv;
$args = new PhutilArgumentParser($argv);
$args->parseStandardArguments();
$args->parsePartial(
array(
array(
'name' => 'load-phutil-library',
'param' => 'path',
'help' => 'Load a libphutil library.',
'repeat' => true,
),
array(
'name' => 'conduit-uri',
'param' => 'uri',
'help' => 'Connect to Phabricator install specified by __uri__.',
),
array(
'name' => 'conduit-version',
'param' => 'version',
'help' => '(Developers) Mock client version in protocol handshake.',
),
array(
'name' => 'conduit-timeout',
'param' => 'timeout',
'help' => 'Set Conduit timeout (in seconds).',
),
));
$config_trace_mode = $args->getArg('trace');
$force_conduit = $args->getArg('conduit-uri');
$force_conduit_version = $args->getArg('conduit-version');
$conduit_timeout = $args->getArg('conduit-timeout');
$load = $args->getArg('load-phutil-library');
$argv = $args->getUnconsumedArgumentVector();
$args = array_values($argv);
$working_directory = getcwd();
$console = PhutilConsole::getConsole();
$config = null;
try {
$console->writeLog(
"libphutil loaded from '%s'.\n",
phutil_get_library_root('phutil'));
$console->writeLog(
"arcanist loaded from '%s'.\n",
phutil_get_library_root('arcanist'));
if (!$args) {
throw new ArcanistUsageException("No command provided. Try 'arc help'.");
}
$global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
$system_config = ArcanistBaseWorkflow::readSystemArcConfig();
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($working_directory);
// Load additional libraries, which can provide new classes like configuration
// overrides, linters and lint engines, unit test engines, etc.
// If the user specified "--load-phutil-library" one or more times from
// the command line, we load those libraries **instead** of whatever else
// is configured. This is basically a debugging feature to let you force
// specific libraries to load regardless of the state of the world.
if ($load) {
// Load the flag libraries. These must load, since the user specified them
// explicitly.
arcanist_load_libraries(
$load,
$must_load = true,
$lib_source = 'a "--load-phutil-library" flag',
$working_copy);
} else {
// Load libraries in system 'load' config. In contrast to global config, we
// fail hard here because this file is edited manually, so if 'arc' breaks
// that doesn't make it any more difficult to correct.
arcanist_load_libraries(
idx($system_config, 'load', array()),
$must_load = true,
$lib_source = 'the "load" setting in system config',
$working_copy);
// Load libraries in global 'load' config, as per "arc set-config load". We
// need to fail softly if these break because errors would prevent the user
// from running "arc set-config" to correct them.
arcanist_load_libraries(
idx($global_config, 'load', array()),
$must_load = false,
$lib_source = 'the "load" setting in global config',
$working_copy);
// Load libraries in ".arcconfig". Libraries here must load.
arcanist_load_libraries(
$working_copy->getConfig('load'),
$must_load = true,
$lib_source = 'the "load" setting in ".arcconfig"',
$working_copy);
}
$user_config = ArcanistBaseWorkflow::readUserConfigurationFile();
$config_class = $working_copy->getConfig('arcanist_configuration');
if ($config_class) {
$config = new $config_class();
} else {
$config = new ArcanistConfiguration();
}
$command = strtolower($args[0]);
$args = array_slice($args, 1);
$workflow = $config->buildWorkflow($command);
if (!$workflow) {
// If the user has an alias, like 'arc alias dhelp diff help', look it up
// and substitute it. We do this only after trying to resolve the workflow
// normally to prevent you from doing silly things like aliasing 'alias'
// to something else.
$aliases = ArcanistAliasWorkflow::getAliases($working_copy);
list($new_command, $args) = ArcanistAliasWorkflow::resolveAliases(
$command,
$config,
$args,
$working_copy);
$full_alias = idx($aliases, $command, array());
$full_alias = implode(' ', $full_alias);
// Run shell command aliases.
if (ArcanistAliasWorkflow::isShellCommandAlias($new_command)) {
$shell_cmd = substr($full_alias, 1);
$console->writeLog(
"[alias: 'arc %s' -> $ %s]",
$command,
$shell_cmd);
if ($args) {
$err = phutil_passthru('%C %Ls', $shell_cmd, $args);
} else {
$err = phutil_passthru('%C', $shell_cmd);
}
exit($err);
}
// Run arc command aliases.
if ($new_command) {
$workflow = $config->buildWorkflow($new_command);
if ($workflow) {
$console->writeLog(
"[alias: 'arc %s' -> 'arc %s']\n",
$command,
$full_alias);
$command = $new_command;
}
}
if (!$workflow) {
throw new ArcanistUsageException(
"Unknown command '{$command}'. Try 'arc help'.");
}
}
$workflow->setArcanistConfiguration($config);
$workflow->setCommand($command);
$workflow->setWorkingDirectory($working_directory);
$workflow->parseArguments($args);
if ($force_conduit_version) {
$workflow->forceConduitVersion($force_conduit_version);
}
if ($conduit_timeout) {
$workflow->setConduitTimeout($conduit_timeout);
}
$need_working_copy = $workflow->requiresWorkingCopy();
$need_conduit = $workflow->requiresConduit();
$need_auth = $workflow->requiresAuthentication();
$need_repository_api = $workflow->requiresRepositoryAPI();
$want_repository_api = $workflow->desiresRepositoryAPI();
$want_working_copy = $workflow->desiresWorkingCopy() ||
$want_repository_api;
$need_conduit = $need_conduit ||
$need_auth;
$need_working_copy = $need_working_copy ||
$need_repository_api;
if ($need_working_copy || $want_working_copy) {
if ($need_working_copy && !$working_copy->getProjectRoot()) {
throw new ArcanistUsageException(
"This command must be run in a Git, Mercurial or Subversion working ".
"copy.");
}
$workflow->setWorkingCopy($working_copy);
}
if ($force_conduit) {
$conduit_uri = $force_conduit;
} else {
if ($working_copy->getConduitURI()) {
$conduit_uri = $working_copy->getConduitURI();
} else {
$conduit_uri = idx($global_config, 'default');
}
}
if ($conduit_uri) {
// Set the URI path to '/api/'. TODO: Originally, I contemplated letting
// you deploy Phabricator somewhere other than the domain root, but ended
// up never pursuing that. We should get rid of all "/api/" silliness
// in things users are expected to configure. This is already happening
// to some degree, e.g. "arc install-certificate" does it for you.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_uri->setPath('/api/');
$conduit_uri = (string)$conduit_uri;
}
$workflow->setConduitURI($conduit_uri);
if ($need_conduit) {
if (!$conduit_uri) {
$message = phutil_console_format(
"This command requires arc to connect to a Phabricator install, but ".
"no Phabricator installation is configured. To configure a ".
"Phabricator URI:\n\n".
" - set a default location with `arc set-config default <uri>`; or\n".
" - specify '--conduit-uri=uri' explicitly; or\n".
" - run 'arc' in a working copy with an '.arcconfig'.\n");
$message = phutil_console_wrap($message);
throw new ArcanistUsageException($message);
}
$workflow->establishConduit();
}
$hosts_config = idx($user_config, 'hosts', array());
$host_config = idx($hosts_config, $conduit_uri, array());
$user_name = idx($host_config, 'user');
$certificate = idx($host_config, 'cert');
$description = implode(' ', $original_argv);
$credentials = array(
'user' => $user_name,
'certificate' => $certificate,
'description' => $description,
);
$workflow->setConduitCredentials($credentials);
if ($need_auth) {
if (!$user_name || !$certificate) {
throw new ArcanistUsageException(
phutil_console_format(
"YOU NEED TO __INSTALL A CERTIFICATE__ TO LOGIN TO PHABRICATOR\n\n".
"You are trying to connect to '{$conduit_uri}' but do not have ".
"a certificate installed for this host. Run:\n\n".
" $ **arc install-certificate**\n\n".
"...to install one."));
}
$workflow->authenticateConduit();
}
if ($need_repository_api || ($want_repository_api && $working_copy)) {
$repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
$working_copy);
$workflow->setRepositoryAPI($repository_api);
}
$listeners = $working_copy->getConfigFromAnySource('events.listeners');
if ($listeners) {
foreach ($listeners as $listener) {
$console->writeLog(
"Registering event listener '%s'.\n",
$listener);
try {
id(new $listener())->register();
} catch (PhutilMissingSymbolException $ex) {
// Continue anwyay, since you may otherwise be unable to run commands
// like `arc set-config events.listeners` in order to repair the damage
// you've caused.
$console->writeErr(
"ERROR: Failed to load event listener '%s'!\n",
$listener);
}
}
}
$config->willRunWorkflow($command, $workflow);
$workflow->willRunWorkflow();
$err = $workflow->run();
$config->didRunWorkflow($command, $workflow, $err);
exit((int)$err);
} catch (Exception $ex) {
$is_usage = ($ex instanceof ArcanistUsageException);
if ($is_usage) {
echo phutil_console_format(
"**Usage Exception:** %s\n",
$ex->getMessage());
}
if ($config) {
$config->didAbortWorkflow($command, $workflow, $ex);
}
if ($config_trace_mode) {
echo "\n";
throw $ex;
}
if (!$is_usage) {
echo phutil_console_format(
"**Exception**\n%s\n%s\n",
$ex->getMessage(),
"(Run with --trace for a full exception trace.)");
}
exit(1);
}
/**
* Perform some sanity checks against the possible diversity of PHP builds in
* the wild, like very old versions and builds that were compiled with flags
* that exclude core functionality.
*/
function sanity_check_environment() {
// NOTE: We don't have phutil_is_windows() yet here.
$is_windows = (DIRECTORY_SEPARATOR != '/');
// We use stream_socket_pair() which is not available on Windows earlier.
$min_version = ($is_windows ? '5.3.0' : '5.2.3');
$cur_version = phpversion();
if (version_compare($cur_version, $min_version, '<')) {
die_with_bad_php(
"You are running PHP version '{$cur_version}', which is older than ".
"the minimum version, '{$min_version}'. Update to at least ".
"'{$min_version}'.");
}
if ($is_windows) {
$need_functions = array(
'curl_init' => array('builtin-dll', 'php_curl.dll'),
);
} else {
$need_functions = array(
'curl_init' => array(
'text',
"You need to install the cURL PHP extension, maybe with ".
"'apt-get install php5-curl' or 'yum install php53-curl' or ".
"something similar."),
'json_decode' => array('flag', '--without-json'),
);
}
$problems = array();
$config = null;
$show_config = false;
foreach ($need_functions as $fname => $resolution) {
if (function_exists($fname)) {
continue;
}
static $info;
if ($info === null) {
ob_start();
phpinfo(INFO_GENERAL);
$info = ob_get_clean();
$matches = null;
if (preg_match('/^Configure Command =>\s*(.*?)$/m', $info, $matches)) {
$config = $matches[1];
}
}
$generic = true;
list($what, $which) = $resolution;
if ($what == 'flag' && strpos($config, $which) !== false) {
$show_config = true;
$generic = false;
$problems[] =
"This build of PHP was compiled with the configure flag '{$which}', ".
"which means it does not have the function '{$fname}()'. This ".
"function is required for arc to run. Rebuild PHP without this flag. ".
"You may also be able to build or install the relevant extension ".
"separately.";
}
if ($what == 'builtin-dll') {
$generic = false;
$problems[] =
"Your install of PHP does not have the '{$which}' extension enabled. ".
"Edit your php.ini file and uncomment the line which reads ".
"'extension={$which}'.";
}
if ($what == 'text') {
$generic = false;
$problems[] = $which;
}
if ($generic) {
$problems[] =
"This build of PHP is missing the required function '{$fname}()'. ".
"Rebuild PHP or install the extension which provides '{$fname}()'.";
}
}
if ($problems) {
if ($show_config) {
$problems[] = "PHP was built with this configure command:\n\n{$config}";
}
die_with_bad_php(implode("\n\n", $problems));
}
}
function die_with_bad_php($message) {
echo "\nPHP CONFIGURATION ERRORS\n\n";
echo $message;
echo "\n\n";
exit(1);
}
function arcanist_load_libraries(
$load,
$must_load,
$lib_source,
ArcanistWorkingCopyIdentity $working_copy) {
if (!$load) {
return;
}
if (!is_array($load)) {
$error = "Libraries specified by {$lib_source} are invalid; expected ".
"a list. Check your configuration.";
$console = PhutilConsole::getConsole();
$console->writeErr("WARNING: %s\n", $error);
return;
}
foreach ($load as $location) {
// Try to resolve the library location. We look in several places, in
// order:
//
// 1. Inside the working copy. This is for phutil libraries within the
// project. For instance "library/src" will resolve to
// "./library/src" if it exists.
// 2. In the same directory as the working copy. This allows you to
// check out a library alongside a working copy and reference it.
// If we haven't resolved yet, "library/src" will try to resolve to
// "../library/src" if it exists.
// 3. Using normal libphutil resolution rules. Generally, this means
// that it checks for libraries next to libphutil, then libraries
// in the PHP include_path.
//
// Note that absolute paths will just resolve absolutely through rule (1).
$resolved = false;
// Check inside the working copy. This also checks absolute paths, since
// they'll resolve absolute and just ignore the project root.
$resolved_location = Filesystem::resolvePath(
$location,
$working_copy->getProjectRoot());
if (Filesystem::pathExists($resolved_location)) {
$location = $resolved_location;
$resolved = true;
}
// If we didn't find anything, check alongside the working copy.
if (!$resolved) {
$resolved_location = Filesystem::resolvePath(
$location,
dirname($working_copy->getProjectRoot()));
if (Filesystem::pathExists($resolved_location)) {
$location = $resolved_location;
$resolved = true;
}
}
$console = PhutilConsole::getConsole();
$console->writeLog(
"Loading phutil library from '%s'...\n",
$location);
$error = null;
try {
phutil_load_library($location);
} catch (PhutilBootloaderException $ex) {
$error = "Failed to load phutil library at location '{$location}'. ".
"This library is specified by {$lib_source}. Check that the ".
"setting is correct and the library is located in the right ".
"place.";
if ($must_load) {
throw new ArcanistUsageException($error);
} else {
fwrite(STDERR, phutil_console_wrap('WARNING: '.$error."\n\n"));
}
} catch (PhutilLibraryConflictException $ex) {
if ($ex->getLibrary() != 'arcanist') {
throw $ex;
}
$arc_dir = dirname(dirname(__FILE__));
$error =
"You are trying to run one copy of Arcanist on another copy of ".
"Arcanist. This operation is not supported. To execute Arcanist ".
"operations against this working copy, run './bin/arc' (from the ".
"current working copy) not some other copy of 'arc' (you ran one ".
"from '{$arc_dir}').";
throw new ArcanistUsageException($error);
}
}
}
diff --git a/scripts/hgdaemon/hgdaemon_client.php b/scripts/hgdaemon/hgdaemon_client.php
index 9096c5ab..dab5702a 100755
--- a/scripts/hgdaemon/hgdaemon_client.php
+++ b/scripts/hgdaemon/hgdaemon_client.php
@@ -1,56 +1,40 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 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.
- */
-
require_once dirname(dirname(__FILE__)).'/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'skip-hello',
'help' => 'Do not expect "capability" message when connecting. '.
'The server must be configured not to send the message. '.
'This deviates from the Mercurial protocol, but slightly '.
'improves performance.',
),
array(
'name' => 'repository',
'wildcard' => true,
),
));
$repo = $args->getArg('repository');
if (count($repo) !== 1) {
throw new Exception("Specify exactly one working copy!");
}
$repo = head($repo);
$client = new ArcanistHgProxyClient($repo);
$client->setSkipHello($args->getArg('skip-hello'));
$t_start = microtime(true);
$result = $client->executeCommand(
array('log', '--template', '{node}', '--rev', 2));
$t_end = microtime(true);
var_dump($result);
echo "\nExecuted in ".((int)(1000000 * ($t_end - $t_start)))."us.\n";
diff --git a/scripts/hgdaemon/hgdaemon_server.php b/scripts/hgdaemon/hgdaemon_server.php
index 3bcb88cf..d1cd90e3 100755
--- a/scripts/hgdaemon/hgdaemon_server.php
+++ b/scripts/hgdaemon/hgdaemon_server.php
@@ -1,69 +1,53 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 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.
- */
-
require_once dirname(dirname(__FILE__)).'/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'quiet',
'help' => 'Do not print status messages to stdout.',
),
array(
'name' => 'skip-hello',
'help' => 'Do not send "capability" message when clients connect. '.
'Clients must be configured not to expect the message. '.
'This deviates from the Mercurial protocol, but slightly '.
'improves performance.',
),
array(
'name' => 'do-not-daemonize',
'help' => 'Remain in the foreground instead of daemonizing.',
),
array(
'name' => 'client-limit',
'param' => 'limit',
'help' => 'Exit after serving __limit__ clients.',
),
array(
'name' => 'idle-limit',
'param' => 'seconds',
'help' => 'Exit after __seconds__ spent idle.',
),
array(
'name' => 'repository',
'wildcard' => true,
),
));
$repo = $args->getArg('repository');
if (count($repo) !== 1) {
throw new Exception("Specify exactly one working copy!");
}
$repo = head($repo);
id(new ArcanistHgProxyServer($repo))
->setQuiet($args->getArg('quiet'))
->setClientLimit($args->getArg('client-limit'))
->setIdleLimit($args->getArg('idle-limit'))
->setDoNotDaemonize($args->getArg('do-not-daemonize'))
->setSkipHello($args->getArg('skip-hello'))
->start();
diff --git a/scripts/lib/PhutilLibraryMapBuilder.php b/scripts/lib/PhutilLibraryMapBuilder.php
index 08646a2c..45fbdd01 100755
--- a/scripts/lib/PhutilLibraryMapBuilder.php
+++ b/scripts/lib/PhutilLibraryMapBuilder.php
@@ -1,521 +1,505 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Build maps of libphutil libraries. libphutil uses the library map to locate
* and load classes and functions in the library.
*
* @task map Mapping libphutil Libraries
* @task path Path Management
* @task symbol Symbol Analysis and Caching
* @task source Source Management
*/
final class PhutilLibraryMapBuilder {
private $root;
private $quiet;
private $subprocessLimit = 8;
private $ugly;
private $showMap;
const LIBRARY_MAP_VERSION_KEY = '__library_version__';
const LIBRARY_MAP_VERSION = 2;
const SYMBOL_CACHE_VERSION_KEY = '__symbol_cache_version__';
const SYMBOL_CACHE_VERSION = 8;
/* -( Mapping libphutil Libraries )---------------------------------------- */
/**
* Create a new map builder for a library.
*
* @param string Path to the library root.
*
* @task map
*/
public function __construct($root) {
$this->root = $root;
}
/**
* Control status output. Use --quiet to set this.
*
* @param bool If true, don't show status output.
* @return this
*
* @task map
*/
public function setQuiet($quiet) {
$this->quiet = $quiet;
return $this;
}
/**
* Control subprocess parallelism limit. Use --limit to set this.
*
* @param int Maximum number of subprocesses to run in parallel.
* @return this
*
* @task map
*/
public function setSubprocessLimit($limit) {
$this->subprocessLimit = $limit;
return $this;
}
/**
* Control whether the ugly (but fast) or pretty (but slower) JSON formatter
* is used.
*
* @param bool If true, use the fastest formatter.
* @return this
*
* @task map
*/
public function setUgly($ugly) {
$this->ugly = $ugly;
return $this;
}
/**
* Control whether the map should be rebuilt, or just shown (printed to
* stdout in JSON).
*
* @param bool If true, show map instead of updating.
* @return this
*
* @task map
*/
public function setShowMap($show_map) {
$this->showMap = $show_map;
return $this;
}
/**
* Build or rebuild the library map.
*
* @return this
*
* @task map
*/
public function buildMap() {
// Identify all the ".php" source files in the library.
$this->log("Finding source files...\n");
$source_map = $this->loadSourceFileMap();
$this->log("Found ".number_format(count($source_map))." files.\n");
// Load the symbol cache with existing parsed symbols. This allows us
// to remap libraries quickly by analyzing only changed files.
$this->log("Loading symbol cache...\n");
$symbol_cache = $this->loadSymbolCache();
// Build out the symbol analysis for all the files in the library. For
// each file, check if it's in cache. If we miss in the cache, do a fresh
// analysis.
$symbol_map = array();
$futures = array();
foreach ($source_map as $file => $hash) {
if (!empty($symbol_cache[$hash])) {
$symbol_map[$file] = $symbol_cache[$hash];
continue;
}
$futures[$file] = $this->buildSymbolAnalysisFuture($file);
}
$this->log("Found ".number_format(count($symbol_map))." files in cache.\n");
// Run the analyzer on any files which need analysis.
if ($futures) {
$limit = $this->subprocessLimit;
$count = number_format(count($futures));
$this->log("Analyzing {$count} files with {$limit} subprocesses...\n");
foreach (Futures($futures)->limit($limit) as $file => $future) {
$result = $future->resolveJSON();
if (empty($result['error'])) {
$symbol_map[$file] = $result;
} else {
echo phutil_console_format(
"\n**SYNTAX ERROR!**\nFile: %s\nLine: %d\n\n%s\n",
Filesystem::readablePath($result['file']),
$result['line'],
$result['error']);
exit(1);
}
$this->log(".");
}
$this->log("\nDone.\n");
}
// We're done building the cache, so write it out immediately. Note that
// we've only retained entries for files we found, so this implicitly cleans
// out old cache entries.
$this->writeSymbolCache($symbol_map, $source_map);
// Our map is up to date, so either show it on stdout or write it to disk.
if ($this->showMap) {
$this->log("Showing map...\n");
if ($this->ugly) {
echo json_encode($symbol_map);
} else {
$json = new PhutilJSON();
echo $json->encodeFormatted($symbol_map);
}
} else {
$this->log("Building library map...\n");
$library_map = $this->buildLibraryMap($symbol_map);
$this->log("Writing map...\n");
$this->writeLibraryMap($library_map);
}
$this->log("Done.\n");
return $this;
}
/**
* Write a status message to the user, if not running in quiet mode.
*
* @param string Message to write.
* @return this
*
* @task map
*/
private function log($message) {
if (!$this->quiet) {
@fwrite(STDERR, $message);
}
return $this;
}
/* -( Path Management )---------------------------------------------------- */
/**
* Get the path to some file in the library.
*
* @param string A library-relative path. If omitted, returns the library
* root path.
* @return string An absolute path.
*
* @task path
*/
private function getPath($path = '') {
return $this->root.'/'.$path;
}
/**
* Get the path to the symbol cache file.
*
* @return string Absolute path to symbol cache.
*
* @task path
*/
private function getPathForSymbolCache() {
return $this->getPath('.phutil_module_cache');
}
/**
* Get the path to the map file.
*
* @return string Absolute path to the library map.
*
* @task path
*/
private function getPathForLibraryMap() {
return $this->getPath('__phutil_library_map__.php');
}
/**
* Get the path to the library init file.
*
* @return string Absolute path to the library init file
*
* @task path
*/
private function getPathForLibraryInit() {
return $this->getPath('__phutil_library_init__.php');
}
/* -( Symbol Analysis and Caching )---------------------------------------- */
/**
* Load the library symbol cache, if it exists and is readable and valid.
*
* @return dict Map of content hashes to cache of output from
* `phutil_symbols.php`.
*
* @task symbol
*/
private function loadSymbolCache() {
$cache_file = $this->getPathForSymbolCache();
try {
$cache = Filesystem::readFile($cache_file);
} catch (Exception $ex) {
$cache = null;
}
$symbol_cache = array();
if ($cache) {
$symbol_cache = json_decode($cache, true);
if (!is_array($symbol_cache)) {
$symbol_cache = array();
}
}
$version = idx($symbol_cache, self::SYMBOL_CACHE_VERSION_KEY);
if ($version != self::SYMBOL_CACHE_VERSION) {
// Throw away caches from a different version of the library.
$symbol_cache = array();
}
unset($symbol_cache[self::SYMBOL_CACHE_VERSION_KEY]);
return $symbol_cache;
}
/**
* Write a symbol map to disk cache.
*
* @param dict Symbol map of relative paths to symbols.
* @param dict Source map (like @{method:loadSourceFileMap}).
* @return void
*
* @task symbol
*/
private function writeSymbolCache(array $symbol_map, array $source_map) {
$cache_file = $this->getPathForSymbolCache();
$cache = array(
self::SYMBOL_CACHE_VERSION_KEY => self::SYMBOL_CACHE_VERSION,
);
foreach ($symbol_map as $file => $symbols) {
$cache[$source_map[$file]] = $symbols;
}
$json = json_encode($cache);
try {
Filesystem::writeFile($cache_file, $json);
} catch (FilesystemException $ex) {
$this->log("Unable to save the cache!\n");
}
}
/**
* Drop the symbol cache, forcing a clean rebuild.
*
* @return this
*
* @task symbol
*/
public function dropSymbolCache() {
$this->log("Dropping symbol cache...\n");
Filesystem::remove($this->getPathForSymbolCache());
}
/**
* Build a future which returns a `phutil_symbols.php` analysis of a source
* file.
*
* @param string Relative path to the source file to analyze.
* @return Future Analysis future.
*
* @task symbol
*/
private function buildSymbolAnalysisFuture($file) {
$absolute_file = $this->getPath($file);
$bin = dirname(dirname(__FILE__)).'/phutil_symbols.php';
return new ExecFuture('%s --ugly -- %s', $bin, $absolute_file);
}
/* -( Source Management )-------------------------------------------------- */
/**
* Build a map of all source files in a library to hashes of their content.
* Returns an array like this:
*
* array(
* 'src/parser/ExampleParser.php' => '60b725f10c9c85c70d97880dfe8191b3',
* // ...
* );
*
* @return dict Map of library-relative paths to content hashes.
* @task source
*/
private function loadSourceFileMap() {
$root = $this->getPath();
$init = $this->getPathForLibraryInit();
if (!Filesystem::pathExists($init)) {
throw new Exception("Provided path '{$root}' is not a phutil library.");
}
$files = id(new FileFinder($root))
->withType('f')
->withSuffix('php')
->excludePath('*/.*')
->setGenerateChecksums(true)
->find();
$map = array();
foreach ($files as $file => $hash) {
if (basename($file) == '__init__.php') {
// TODO: Remove this once we kill __init__.php. This just makes the
// script run faster until we do, so testing and development is less
// annoying.
continue;
}
$file = Filesystem::readablePath($file, $root);
$file = ltrim($file, '/');
if (dirname($file) == '.') {
// We don't permit normal source files at the root level, so just ignore
// them; they're special library files.
continue;
}
// We include also filename in the hash to handle cases when the file is
// moved without modifying its content.
$map[$file] = md5($hash.$file);
}
return $map;
}
/**
* Convert the symbol analysis of all the source files in the library into
* a library map.
*
* @param dict Symbol analysis of all source files.
* @return dict Library map.
* @task source
*/
private function buildLibraryMap(array $symbol_map) {
$library_map = array(
'class' => array(),
'function' => array(),
'xmap' => array(),
);
// Detect duplicate symbols within the library.
foreach ($symbol_map as $file => $info) {
foreach ($info['have'] as $type => $symbols) {
foreach ($symbols as $symbol => $declaration) {
$lib_type = ($type == 'interface') ? 'class' : $type;
if (!empty($library_map[$lib_type][$symbol])) {
$prior = $library_map[$lib_type][$symbol];
throw new Exception(
"Definition of {$type} '{$symbol}' in file '{$file}' duplicates ".
"prior definition in file '{$prior}'. You can not declare the ".
"same symbol twice.");
}
$library_map[$lib_type][$symbol] = $file;
}
}
$library_map['xmap'] += $info['xmap'];
}
// Simplify the common case (one parent) to make the file a little easier
// to deal with.
foreach ($library_map['xmap'] as $class => $extends) {
if (count($extends) == 1) {
$library_map['xmap'][$class] = reset($extends);
}
}
// Sort the map so it is relatively stable across changes.
foreach ($library_map as $key => $symbols) {
ksort($symbols);
$library_map[$key] = $symbols;
}
ksort($library_map);
return $library_map;
}
/**
* Write a finalized library map.
*
* @param dict Library map structure to write.
* @return void
*
* @task source
*/
private function writeLibraryMap(array $library_map) {
$map_file = $this->getPathForLibraryMap();
$version = self::LIBRARY_MAP_VERSION;
$library_map = array(
self::LIBRARY_MAP_VERSION_KEY => $version,
) + $library_map;
$library_map = var_export($library_map, $return_string = true);
$library_map = preg_replace('/\s+$/m', '', $library_map);
$library_map = preg_replace('/array \(/', 'array(', $library_map);
$at = '@';
$source_file = <<<EOPHP
<?php
/**
* This file is automatically generated. Use 'arc liberate' to rebuild it.
* {$at}generated
* {$at}phutil-library-version {$version}
*/
phutil_register_library_map({$library_map});
EOPHP;
Filesystem::writeFile($map_file, $source_file);
}
}
diff --git a/scripts/phutil_rebuild_map.php b/scripts/phutil_rebuild_map.php
index c562947f..ae254bcb 100755
--- a/scripts/phutil_rebuild_map.php
+++ b/scripts/phutil_rebuild_map.php
@@ -1,88 +1,72 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 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.
- */
-
require_once dirname(__FILE__).'/__init_script__.php';
require_once dirname(__FILE__).'/lib/PhutilLibraryMapBuilder.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('rebuild the library map file');
$args->setSynopsis(<<<EOHELP
**phutil_rebuild_map.php** [__options__] __root__
Rebuild the library map file for a libphutil library.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'quiet',
'help' => 'Do not write status messages to stderr.',
),
array(
'name' => 'drop-cache',
'help' => 'Drop the symbol cache and rebuild the entire map from '.
'scratch.',
),
array(
'name' => 'limit',
'param' => 'N',
'default' => 8,
'help' => 'Controls the number of symbol mapper subprocesses run '.
'at once. Defaults to 8.',
),
array(
'name' => 'show',
'help' => 'Print symbol map to stdout instead of writing it to the '.
'map file.',
),
array(
'name' => 'ugly',
'help' => 'Use faster but less readable serialization for --show.',
),
array(
'name' => 'root',
'wildcard' => true,
)
));
$root = $args->getArg('root');
if (count($root) !== 1) {
throw new Exception("Provide exactly one library root!");
}
$root = Filesystem::resolvePath(head($root));
$builder = new PhutilLibraryMapBuilder($root);
$builder->setQuiet($args->getArg('quiet'));
$builder->setSubprocessLimit($args->getArg('limit'));
if ($args->getArg('drop-cache')) {
$builder->dropSymbolCache();
}
if ($args->getArg('show')) {
$builder->setShowMap(true);
$builder->setUgly($args->getArg('ugly'));
}
$builder->buildMap();
exit(0);
diff --git a/scripts/phutil_symbols.php b/scripts/phutil_symbols.php
index 9df312f4..18ab479e 100755
--- a/scripts/phutil_symbols.php
+++ b/scripts/phutil_symbols.php
@@ -1,455 +1,439 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 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.
- */
-
// We have to do this first before we load any symbols, because we define the
// builtin symbol list through introspection.
$builtins = phutil_symbols_get_builtins();
require_once dirname(__FILE__).'/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('identify symbols in a PHP source file');
$args->setSynopsis(<<<EOHELP
**phutil_symbols.php** [__options__] __path.php__
Identify the symbols (clases, functions and interfaces) in a PHP
source file. Symbols are divided into "have" symbols (symbols the file
declares) and "need" symbols (symbols the file depends on). For example,
class declarations are "have" symbols, while object instantiations
with "new X()" are "need" symbols.
Dependencies on builtins and symbols marked '@phutil-external-symbol'
in docblocks are omitted without __--all__.
Symbols are reported in JSON on stdout.
This script is used internally by libphutil/arcanist to build maps of
library symbols.
It would be nice to eventually implement this as a C++ xhpast binary,
as it's relatively stable and performance is currently awful
(500ms+ for moderately large files).
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'all',
'help' => 'Report all symbols, including builtins and declared '.
'externals.',
),
array(
'name' => 'ugly',
'help' => 'Do not prettify JSON output.',
),
array(
'name' => 'path',
'wildcard' => true,
'help' => 'PHP Source file to analyze.',
),
));
$paths = $args->getArg('path');
if (count($paths) !== 1) {
throw new Exception("Specify exactly one path!");
}
$path = Filesystem::resolvePath(head($paths));
$show_all = $args->getArg('all');
$source_code = Filesystem::readFile($path);
try {
$tree = XHPASTTree::newFromData($source_code);
} catch (XHPASTSyntaxErrorException $ex) {
$result = array(
'error' => $ex->getMessage(),
'line' => $ex->getErrorLine(),
'file' => $path,
);
$json = new PhutilJSON();
echo $json->encodeFormatted($result);
exit(0);
}
$root = $tree->getRootNode();
$root->buildSelectCache();
// -( Marked Externals )------------------------------------------------------
// Identify symbols marked with "@phutil-external-symbol", so we exclude them
// from the dependency list.
$externals = array();
$doc_parser = new PhutilDocblockParser();
foreach ($root->getTokens() as $token) {
if ($token->getTypeName() == 'T_DOC_COMMENT') {
list($block, $special) = $doc_parser->parse($token->getValue());
$ext_list = idx($special, 'phutil-external-symbol');
$ext_list = explode("\n", $ext_list);
$ext_list = array_filter($ext_list);
foreach ($ext_list as $ext_ref) {
$matches = null;
if (preg_match('/^\s*(\S+)\s+(\S+)/', $ext_ref, $matches)) {
$externals[$matches[1]][$matches[2]] = true;
}
}
}
}
// -( Declarations and Dependencies )-----------------------------------------
// The first stage of analysis is to find all the symbols we declare in the
// file (like functions and classes) and all the symbols we use in the file
// (like calling functions and invoking classes). Later, we filter this list
// to exclude builtins.
$have = array(); // For symbols we declare.
$need = array(); // For symbols we use.
$xmap = array(); // For extended classes and implemented interfaces.
// -( Functions )-------------------------------------------------------------
// Find functions declared in this file.
// This is "function f() { ... }".
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($functions as $function) {
$name = $function->getChildByIndex(2);
if ($name->getTypeName() == 'n_EMPTY') {
// This is an anonymous function; don't record it into the symbol
// index.
continue;
}
$have[] = array(
'type' => 'function',
'symbol' => $name,
);
}
// Find functions used by this file. Uses:
//
// - Explicit Call
// - String literal passed to call_user_func() or call_user_func_array()
//
// TODO: Possibly support these:
//
// - String literal in ReflectionFunction().
// This is "f();".
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = $call->getChildByIndex(0);
if ($name->getTypeName() == 'n_VARIABLE' ||
$name->getTypeName() == 'n_VARIABLE_VARIABLE') {
// Ignore these, we can't analyze them.
continue;
}
if ($name->getTypeName() == 'n_CLASS_STATIC_ACCESS') {
// These are "C::f()", we'll pick this up later on.
continue;
}
$call_name = $name->getConcreteString();
if ($call_name == 'call_user_func' ||
$call_name == 'call_user_func_array') {
$params = $call->getChildByIndex(1)->getChildren();
if (!count($params)) {
// This is a bare call_user_func() with no arguments; just ignore it.
continue;
}
$symbol = array_shift($params);
$symbol_value = $symbol->getStringLiteralValue();
if ($symbol_value) {
$need[] = array(
'type' => 'function',
'name' => $symbol_value,
'symbol' => $symbol,
);
}
} else {
$need[] = array(
'type' => 'function',
'symbol' => $name,
);
}
}
// -( Classes )---------------------------------------------------------------
// Find classes declared by this file.
// This is "class X ... { ... }".
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1);
$have[] = array(
'type' => 'class',
'symbol' => $class_name,
);
}
// Find classes used by this file. We identify these:
//
// - class ... extends X
// - new X
// - Static method call
// - Static property access
// - Use of class constant
//
// TODO: Possibly support these:
//
// - typehints
// - instanceof
// - catch
// - String literal in ReflectionClass().
// - String literal in array literal in call_user_func()/call_user_func_array()
// This is "class X ... { ... }".
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1)->getConcreteString();
$extends = $class->getChildByIndex(2);
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) {
$need[] = array(
'type' => 'class',
'symbol' => $parent,
);
// Track all 'extends' in the extension map.
$xmap[$class_name][] = $parent->getConcreteString();
}
}
// This is "new X()".
$uses_of_new = $root->selectDescendantsOfType('n_NEW');
foreach ($uses_of_new as $new_operator) {
$name = $new_operator->getChildByIndex(0);
if ($name->getTypeName() == 'n_VARIABLE' ||
$name->getTypeName() == 'n_VARIABLE_VARIABLE') {
continue;
}
$need[] = array(
'type' => 'class',
'symbol' => $name,
);
}
// This covers all of "X::$y", "X::y()" and "X::CONST".
$static_uses = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($static_uses as $static_use) {
$name = $static_use->getChildByIndex(0);
if ($name->getTypeName() != 'n_CLASS_NAME') {
continue;
}
$name_concrete = $name->getConcreteString();
$magic_names = array(
'static' => true,
'parent' => true,
'self' => true,
);
if (isset($magic_names[$name_concrete])) {
continue;
}
$need[] = array(
'type' => 'class',
'symbol' => $name,
);
}
// -( Interfaces )------------------------------------------------------------
// Find interfaces declared in ths file.
// This is "interface X .. { ... }".
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
foreach ($interfaces as $interface) {
$interface_name = $interface->getChildByIndex(1);
$have[] = array(
'type' => 'interface',
'symbol' => $interface_name,
);
}
// Find interfaces used by this file. We identify these:
//
// - class ... implements X
// - interface ... extends X
// This is "class X ... { ... }".
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1)->getConcreteString();
$implements = $class->getChildByIndex(3);
$interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME');
foreach ($interfaces as $interface) {
$need[] = array(
'type' => 'interface',
'symbol' => $interface,
);
// Track 'class ... implements' in the extension map.
$xmap[$class_name][] = $interface->getConcreteString();
}
}
// This is "interface X ... { ... }".
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
foreach ($interfaces as $interface) {
$interface_name = $interface->getChildByIndex(1)->getConcreteString();
$extends = $interface->getChildByIndex(2);
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) {
$need[] = array(
'type' => 'interface',
'symbol' => $parent,
);
// Track 'interface ... extends' in the extension map.
$xmap[$interface_name][] = $parent->getConcreteString();
}
}
// -( Analysis )--------------------------------------------------------------
$declared_symbols = array();
foreach ($have as $key => $spec) {
$name = $spec['symbol']->getConcreteString();
$declared_symbols[$spec['type']][$name] = $spec['symbol']->getOffset();
}
$required_symbols = array();
foreach ($need as $key => $spec) {
$name = idx($spec, 'name');
if (!$name) {
$name = $spec['symbol']->getConcreteString();
}
$type = $spec['type'];
if (!$show_all) {
if (!empty($externals[$type][$name])) {
// Ignore symbols declared as externals.
continue;
}
if (!empty($builtins[$type][$name])) {
// Ignore symbols declared as builtins.
continue;
}
}
if (!empty($required_symbols[$type][$name])) {
// Report only the first use of a symbol, since reporting all of them
// isn't terribly informative.
continue;
}
if (!empty($declared_symbols[$type][$name])) {
// We declare this symbol, so don't treat it as a requirement.
continue;
}
$required_symbols[$type][$name] = $spec['symbol']->getOffset();
}
$result = array(
'have' => $declared_symbols,
'need' => $required_symbols,
'xmap' => $xmap,
);
// -( Output )----------------------------------------------------------------
if ($args->getArg('ugly')) {
echo json_encode($result);
} else {
$json = new PhutilJSON();
echo $json->encodeFormatted($result);
}
// -( Library )---------------------------------------------------------------
function phutil_symbols_get_builtins() {
$builtin = array();
$builtin['classes'] = get_declared_classes();
$builtin['interfaces'] = get_declared_interfaces();
$funcs = get_defined_functions();
$builtin['functions'] = $funcs['internal'];
foreach (array('functions', 'classes') as $type) {
// Developers may not have every extension that a library potentially uses
// installed. We supplement the list of declared functions and classses with
// a list of known extension functions to avoid raising false positives just
// because you don't have pcntl, etc.
$list = dirname(__FILE__)."/php_extension_{$type}.txt";
$extensions = file_get_contents($list);
$extensions = explode("\n", trim($extensions));
$builtin[$type] = array_merge($builtin[$type], $extensions);
}
return array(
'class' => array_fill_keys($builtin['classes'], true) + array(
'PhutilBootloader' => true,
),
'function' => array_filter(
array(
'empty' => true,
'isset' => true,
'die' => true,
// These are provided by libphutil but not visible in the map.
'phutil_is_windows' => true,
'phutil_load_library' => true,
'phutil_is_hiphop_runtime' => true,
// HPHP/i defines these functions as 'internal', but they are NOT
// builtins and do not exist in vanilla PHP. Make sure we don't mark
// them as builtin since we need to add dependencies for them.
'idx' => false,
'id' => false,
) + array_fill_keys($builtin['functions'], true)),
'interface' => array_fill_keys($builtin['interfaces'], true),
);
}
diff --git a/scripts/update_compat_info.php b/scripts/update_compat_info.php
index 03ab7e00..c85b3459 100755
--- a/scripts/update_compat_info.php
+++ b/scripts/update_compat_info.php
@@ -1,92 +1,76 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 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.
- */
-
require_once dirname(__FILE__).'/__init_script__.php';
$target = 'resources/php_compat_info.json';
echo "Purpose: Updates {$target} used by ArcanistXHPASTLinter.\n";
$ok = include 'PHP/CompatInfo/Autoload.php';
if (!$ok) {
echo "You need PHP_CompatInfo available in 'include_path'.\n";
echo "http://php5.laurent-laville.org/compatinfo/\n";
exit(1);
}
$required = '5.2.3';
$reference = id(new PHP_CompatInfo_Reference_ALL())->getAll();
$output = array();
$output['@'.'generated'] = true;
$output['params'] = array();
foreach (array('functions', 'classes', 'interfaces') as $type) {
$output[$type] = array();
foreach ($reference[$type] as $name => $versions) {
$name = strtolower($name);
$versions = reset($versions);
list($min, $max) = $versions;
if (version_compare($min, $required) > 0) {
$output[$type][$name] = $min;
}
if ($type == 'functions' && isset($versions[2])) {
$params = explode(', ', $versions[2]);
foreach ($params as $i => $version) {
if (version_compare($version, $required) > 0) {
$output['params'][$name][$i] = $version;
}
}
}
}
}
// Grepped from PHP Manual.
$output['functions_windows'] = array(
'apache_child_terminate' => '',
'chroot' => '',
'getrusage' => '',
'imagecreatefromxpm' => '',
'lchgrp' => '',
'lchown' => '',
'nl_langinfo' => '',
'strptime' => '',
'sys_getloadavg' => '',
'checkdnsrr' => '5.3.0',
'dns_get_record' => '5.3.0',
'fnmatch' => '5.3.0',
'getmxrr' => '5.3.0',
'getopt' => '5.3.0',
'imagecolorclosesthwb' => '5.3.0',
'inet_ntop' => '5.3.0',
'inet_pton' => '5.3.0',
'link' => '5.3.0',
'linkinfo' => '5.3.0',
'readlink' => '5.3.0',
'socket_create_pair' => '5.3.0',
'stream_socket_pair' => '5.3.0',
'symlink' => '5.3.0',
'time_nanosleep' => '5.3.0',
'time_sleep_until' => '5.3.0',
);
file_put_contents(
phutil_get_library_root('arcanist').'/../'.$target,
json_encode($output));
echo "Done.\n";
diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php
index c671c82a..c5168d8d 100644
--- a/src/__phutil_library_init__.php
+++ b/src/__phutil_library_init__.php
@@ -1,19 +1,3 @@
<?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.
- */
-
phutil_register_library('arcanist', __FILE__);
diff --git a/src/configuration/ArcanistConfiguration.php b/src/configuration/ArcanistConfiguration.php
index 560b6432..60bc2825 100644
--- a/src/configuration/ArcanistConfiguration.php
+++ b/src/configuration/ArcanistConfiguration.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Runtime workflow configuration. In Arcanist, commands you type like
* "arc diff" or "arc lint" are called "workflows". This class allows you to add
* new workflows (and extend existing workflows) by subclassing it and then
* pointing to your subclass in your project configuration.
*
* When specified as the **arcanist_configuration** class in your project's
* ##.arcconfig##, your subclass will be instantiated (instead of this class)
* and be able to handle all the method calls. In particular, you can:
*
* - create, replace, or disable workflows by overriding buildWorkflow()
* and buildAllWorkflows();
* - add additional steps before or after workflows run by overriding
* willRunWorkflow() or didRunWorkflow() or didAbortWorkflow(); and
* - add new flags to existing workflows by overriding
* getCustomArgumentsForCommand().
*
* @group config
* @concrete-extensible
*/
class ArcanistConfiguration {
public function buildWorkflow($command) {
if ($command == '--help') {
// Special-case "arc --help" to behave like "arc help" instead of telling
// you to type "arc help" without being helpful.
$command = 'help';
}
return idx($this->buildAllWorkflows(), $command);
}
public function buildAllWorkflows() {
$symbols = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass('ArcanistBaseWorkflow')
->selectAndLoadSymbols();
$workflows = array();
foreach ($symbols as $symbol) {
$class = $symbol['name'];
$workflow = newv($class, array());
$name = $workflow->getWorkflowName();
if (isset($workflows[$name])) {
$other = get_class($workflows[$name]);
throw new Exception(
"Workflows {$class} and {$other} both implement workflows named ".
"{$name}.");
}
$workflows[$workflow->getWorkflowName()] = $workflow;
}
return $workflows;
}
final public function isValidWorkflow($workflow) {
return (bool)$this->buildWorkflow($workflow);
}
public function willRunWorkflow($command, ArcanistBaseWorkflow $workflow) {
// This is a hook.
}
public function didRunWorkflow($command, ArcanistBaseWorkflow $workflow,
$err) {
// This is a hook.
}
public function didAbortWorkflow($command, $workflow, Exception $ex) {
// This is a hook.
}
public function getCustomArgumentsForCommand($command) {
return array();
}
}
diff --git a/src/configuration/ArcanistSettings.php b/src/configuration/ArcanistSettings.php
index 9afe206b..e931c7e1 100644
--- a/src/configuration/ArcanistSettings.php
+++ b/src/configuration/ArcanistSettings.php
@@ -1,241 +1,225 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* @group config
*/
final class ArcanistSettings {
private function getOptions() {
return array(
'default' => array(
'type' => 'string',
'help' =>
'The URI of a Phabricator install to connect to by default, if '.
'arc is run in a project without a Phabricator URI or run outside '.
'of a project.',
'example' => '"http://phabricator.example.com/"',
),
'base' => array(
'type' => 'string',
'help' =>
'Base commit ruleset to invoke when determining the start of a '.
'commit range. See "Arcanist User Guide: Commit Ranges" for '.
'details.',
'example' => '"arc:amended, arc:prompt"',
),
'load' => array(
'type' => 'list',
'legacy' => 'phutil_libraries',
'help' =>
'A list of paths to phutil libraries that should be loaded at '.
'startup. This can be used to make classes available, like lint or '.
'unit test engines.',
'example' => '["/var/arc/customlib/src"]',
),
'lint.engine' => array(
'type' => 'string',
'legacy' => 'lint_engine',
'help' =>
'The name of a default lint engine to use, if no lint engine is '.
'specified by the current project.',
'example' => '"ExampleLintEngine"',
),
'unit.engine' => array(
'type' => 'string',
'legacy' => 'unit_engine',
'help' =>
'The name of a default unit test engine to use, if no unit test '.
'engine is specified by the current project.',
'example' => '"ExampleUnitTestEngine"',
),
'arc.land.onto.default' => array(
'type' => 'string',
'help' =>
'The name of the default branch to land changes onto when '.
'`arc land` is run.',
'example' => '"develop"',
),
'history.immutable' => array(
'type' => 'bool',
'legacy' => 'immutable_history',
'help' =>
'If true, arc will never change repository history (e.g., through '.
'amending or rebasing). Defaults to true in Mercurial and false in '.
'Git. This setting has no effect in Subversion.',
'example' => 'false',
),
'editor' => array(
'type' => 'string',
'help' =>
"Command to use to invoke an interactive editor, like 'nano' or ".
"'vim'. This setting overrides the EDITOR environmental variable.",
'example' => '"nano"',
),
'events.listeners' => array(
'type' => 'list',
'help' => 'List of event listener classes to install at startup.',
'example' => '["ExampleEventListener"]',
),
);
}
private function getOption($key) {
return idx($this->getOptions(), $key, array());
}
public function getAllKeys() {
return array_keys($this->getOptions());
}
public function getHelp($key) {
return idx($this->getOption($key), 'help');
}
public function getExample($key) {
return idx($this->getOption($key), 'example');
}
public function getType($key) {
return idx($this->getOption($key), 'type', 'wild');
}
public function getLegacyName($key) {
return idx($this->getOption($key), 'legacy');
}
public function willWriteValue($key, $value) {
$type = $this->getType($key);
switch ($type) {
case 'bool':
if (strtolower($value) === 'false' ||
strtolower($value) === 'no' ||
strtolower($value) === 'off' ||
$value === '' ||
$value === '0' ||
$value === 0 ||
$value === false) {
$value = false;
} else if (strtolower($value) === 'true' ||
strtolower($value) === 'yes' ||
strtolower($value) === 'on' ||
$value === '1' ||
$value === 1 ||
$value === true) {
$value = true;
} else {
throw new ArcanistUsageException(
"Type of setting '{$key}' must be boolean, like 'true' or ".
"'false'.");
}
break;
case 'list':
if (is_array($value)) {
break;
}
if (is_string($value)) {
$list = json_decode($value, true);
if (is_array($list)) {
$value = $list;
break;
}
}
$list_example = '["apple", "banana", "cherry"]';
throw new ArcanistUsageException(
"Type of setting '{$key}' must be list. You can specify a list ".
"in JSON, like: {$list_example}");
case 'string':
if (!is_scalar($value)) {
throw new ArcanistUsageException(
"Type of setting '{$key}' must be string.");
}
$value = (string)$value;
break;
case 'wild':
break;
}
return $value;
}
public function willReadValue($key, $value) {
$type = $this->getType($key);
switch ($type) {
case 'string':
if (!is_string($value)) {
throw new ArcanistUsageException(
"Type of setting '{$key}' must be string.");
}
break;
case 'bool':
if ($value !== true && $value !== false) {
throw new ArcanistUsageException(
"Type of setting '{$key}' must be boolean.");
}
break;
case 'list':
if (!is_array($value)) {
throw new ArcanistUsageException(
"Type of setting '{$key}' must be list.");
}
break;
case 'wild':
break;
}
return $value;
}
public function formatConfigValueForDisplay($key, $value) {
if ($value === false) {
return 'false';
}
if ($value === true) {
return 'true';
}
if ($value === null) {
return 'null';
}
if (is_string($value)) {
return '"'.$value.'"';
}
if (is_array($value)) {
// TODO: Both json_encode() and PhutilJSON do a bad job with one-liners.
// PhutilJSON splits them across a bunch of lines, while json_encode()
// escapes all kinds of stuff like "/". It would be nice if PhutilJSON
// had a mode for pretty one-liners.
$value = json_encode($value);
// json_encode() unnecessarily escapes "/" to prevent "</script>" stuff,
// optimistically unescape it for display to improve readability.
$value = preg_replace('@(?<!\\\\)\\\\/@', '/', $value);
return $value;
}
return $value;
}
}
diff --git a/src/difference/ArcanistDiffUtils.php b/src/difference/ArcanistDiffUtils.php
index dcc7b332..38207ff5 100644
--- a/src/difference/ArcanistDiffUtils.php
+++ b/src/difference/ArcanistDiffUtils.php
@@ -1,348 +1,332 @@
<?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.
- */
-
/**
* Dumping ground for diff- and diff-algorithm-related miscellany.
*
* @group diff
*/
final class ArcanistDiffUtils {
/**
* Make a best-effort attempt to determine if a file is definitely binary.
*
* @return bool If true, the file is almost certainly binary. If false, the
* file might still be binary but is subtle about it.
*/
public static function isHeuristicBinaryFile($data) {
// Detect if a file is binary according to the Git heuristic, which is the
// presence of NULL ("\0") bytes. Git only examines the first "few" bytes of
// each file (8KB or so) as an optimization, but we don't have a reasonable
// equivalent in PHP, so just look at all of it.
return (strpos($data, "\0") !== false);
}
public static function renderDifferences(
$old,
$new,
$context_lines = 3,
$diff_options = "-L 'Old Value' -L 'New Value'") {
if ((string)$old === (string)$new) {
$new .= "\n(Old and new values are identical.)";
}
$file_old = new TempFile();
$file_new = new TempFile();
Filesystem::writeFile($file_old, (string)$old."\n");
Filesystem::writeFile($file_new, (string)$new."\n");
list($err, $stdout) = exec_manual(
"/usr/bin/diff {$diff_options} -U {$context_lines} %s %s",
$file_old,
$file_new);
return $stdout;
}
public static function generateIntralineDiff($o, $n) {
if (!strlen($o) || !strlen($n)) {
return array(
array(array(0, strlen($o))),
array(array(0, strlen($n)))
);
}
// This algorithm is byte-oriented and thus not safe for UTF-8, so just
// mark all the text as changed if either string has multibyte characters
// in it. TODO: Fix this so that this algorithm is UTF-8 aware.
if (preg_match('/[\x80-\xFF]/', $o.$n)) {
return array(
array(array(1, strlen($o))),
array(array(1, strlen($n))),
);
}
$result = self::buildLevenshteinDifferenceString($o, $n);
do {
$orig = $result;
$result = preg_replace(
'/([xdi])(s{3})([xdi])/',
'$1xxx$3',
$result);
$result = preg_replace(
'/([xdi])(s{2})([xdi])/',
'$1xx$3',
$result);
$result = preg_replace(
'/([xdi])(s{1})([xdi])/',
'$1x$3',
$result);
} while ($result != $orig);
$o_bright = array();
$n_bright = array();
$rlen = strlen($result);
$len = -1;
$cur = $result[0];
$result .= '-';
for ($ii = 0; $ii < strlen($result); $ii++) {
$len++;
$now = $result[$ii];
if ($result[$ii] == $cur) {
continue;
}
if ($cur == 's') {
$o_bright[] = array(0, $len);
$n_bright[] = array(0, $len);
} else if ($cur == 'd') {
$o_bright[] = array(1, $len);
} else if ($cur == 'i') {
$n_bright[] = array(1, $len);
} else if ($cur == 'x') {
$o_bright[] = array(1, $len);
$n_bright[] = array(1, $len);
}
$cur = $now;
$len = 0;
}
$o_bright = self::collapseIntralineRuns($o_bright);
$n_bright = self::collapseIntralineRuns($n_bright);
return array($o_bright, $n_bright);
}
public static function applyIntralineDiff($str, $intra_stack) {
$buf = '';
$p = $s = $e = 0; // position, start, end
$highlight = $tag = $ent = false;
$highlight_o = '<span class="bright">';
$highlight_c = '</span>';
$n = strlen($str);
for ($i = 0; $i < $n; $i++) {
if ($p == $e) {
do {
if (empty($intra_stack)) {
$buf .= substr($str, $i);
break 2;
}
$stack = array_shift($intra_stack);
$s = $e;
$e += $stack[1];
} while ($stack[0] == 0);
}
if (!$highlight && !$tag && !$ent && $p == $s) {
$buf .= $highlight_o;
$highlight = true;
}
if ($str[$i] == '<') {
$tag = true;
if ($highlight) {
$buf .= $highlight_c;
}
}
if (!$tag) {
if ($str[$i] == '&') {
$ent = true;
}
if ($ent && $str[$i] == ';') {
$ent = false;
}
if (!$ent) {
$p++;
}
}
$buf .= $str[$i];
if ($tag && $str[$i] == '>') {
$tag = false;
if ($highlight) {
$buf .= $highlight_o;
}
}
if ($highlight && ($p == $e || $i == $n - 1)) {
$buf .= $highlight_c;
$highlight = false;
}
}
return $buf;
}
private static function collapseIntralineRuns($runs) {
$count = count($runs);
for ($ii = 0; $ii < $count - 1; $ii++) {
if ($runs[$ii][0] == $runs[$ii + 1][0]) {
$runs[$ii + 1][1] += $runs[$ii][1];
unset($runs[$ii]);
}
}
return array_values($runs);
}
public static function buildLevenshteinDifferenceString($o, $n) {
$olt = strlen($o);
$nlt = strlen($n);
if (!$olt) {
return str_repeat('i', $nlt);
}
if (!$nlt) {
return str_repeat('d', $olt);
}
$min = min($olt, $nlt);
$t_start = microtime(true);
$pre = 0;
while ($pre < $min && $o[$pre] == $n[$pre]) {
$pre++;
}
$end = 0;
while ($end < $min && $o[($olt - 1) - $end] == $n[($nlt - 1) - $end]) {
$end++;
}
if ($end + $pre >= $min) {
$end = min($end, $min - $pre);
$prefix = str_repeat('s', $pre);
$suffix = str_repeat('s', $end);
$infix = null;
if ($olt > $nlt) {
$infix = str_repeat('d', $olt - ($end + $pre));
} else if ($nlt > $olt) {
$infix = str_repeat('i', $nlt - ($end + $pre));
}
return $prefix.$infix.$suffix;
}
if ($min - ($end + $pre) > 80) {
$max = max($olt, $nlt);
return str_repeat('x', $min) .
str_repeat($olt < $nlt ? 'i' : 'd', $max - $min);
}
$prefix = str_repeat('s', $pre);
$suffix = str_repeat('s', $end);
$o = substr($o, $pre, $olt - $end - $pre);
$n = substr($n, $pre, $nlt - $end - $pre);
$ol = strlen($o);
$nl = strlen($n);
$m = array_fill(0, $ol + 1, array_fill(0, $nl + 1, array()));
$T_D = 'd';
$T_I = 'i';
$T_S = 's';
$T_X = 'x';
$m[0][0] = array(
0,
null);
for ($ii = 1; $ii <= $ol; $ii++) {
$m[$ii][0] = array(
$ii * 1000,
$T_D);
}
for ($jj = 1; $jj <= $nl; $jj++) {
$m[0][$jj] = array(
$jj * 1000,
$T_I);
}
$ii = 1;
do {
$jj = 1;
do {
if ($o[$ii - 1] == $n[$jj - 1]) {
$sub_t_cost = $m[$ii - 1][$jj - 1][0] + 0;
$sub_t = $T_S;
} else {
$sub_t_cost = $m[$ii - 1][$jj - 1][0] + 2000;
$sub_t = $T_X;
}
if ($m[$ii - 1][$jj - 1][1] != $sub_t) {
$sub_t_cost += 1;
}
$del_t_cost = $m[$ii - 1][$jj][0] + 1000;
if ($m[$ii - 1][$jj][1] != $T_D) {
$del_t_cost += 1;
}
$ins_t_cost = $m[$ii][$jj - 1][0] + 1000;
if ($m[$ii][$jj - 1][1] != $T_I) {
$ins_t_cost += 1;
}
if ($sub_t_cost <= $del_t_cost && $sub_t_cost <= $ins_t_cost) {
$m[$ii][$jj] = array(
$sub_t_cost,
$sub_t);
} else if ($ins_t_cost <= $del_t_cost) {
$m[$ii][$jj] = array(
$ins_t_cost,
$T_I);
} else {
$m[$ii][$jj] = array(
$del_t_cost,
$T_D);
}
} while ($jj++ < $nl);
} while ($ii++ < $ol);
$result = '';
$ii = $ol;
$jj = $nl;
do {
$r = $m[$ii][$jj][1];
$result .= $r;
switch ($r) {
case $T_S:
case $T_X:
$ii--;
$jj--;
break;
case $T_I:
$jj--;
break;
case $T_D:
$ii--;
break;
}
} while ($ii || $jj);
return $prefix.strrev($result).$suffix;
}
}
diff --git a/src/difference/__tests__/ArcanistDiffUtilsTestCase.php b/src/difference/__tests__/ArcanistDiffUtilsTestCase.php
index d84eb8fa..4938542f 100644
--- a/src/difference/__tests__/ArcanistDiffUtilsTestCase.php
+++ b/src/difference/__tests__/ArcanistDiffUtilsTestCase.php
@@ -1,115 +1,99 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Test cases for @{class:ArcanistDiffUtils}.
*
* @group testcase
*/
final class ArcanistDiffUtilsTestCase extends ArcanistTestCase {
public function testLevenshtein() {
$tests = array(
array(
'a',
'b',
'x'
),
array(
'kalrmr(array($b))',
'array($b)',
'dddddddssssssssds'
),
array(
'array($b)',
'kalrmr(array($b))',
'iiiiiiissssssssis'
),
array(
'zkalrmr(array($b))z',
'xarray($b)x',
'dddddddxsssssssssdx'
),
array(
'xarray($b)x',
'zkalrmr(array($b))z',
'iiiiiiixsssssssssix'
),
array(
'abcdefghi',
'abcdefghi',
'sssssssss'
),
array(
'abcdefghi',
'abcdefghijkl',
'sssssssssiii'
),
array(
'abcdefghijkl',
'abcdefghi',
'sssssssssddd'
),
array(
'xyzabcdefghi',
'abcdefghi',
'dddsssssssss'
),
array(
'abcdefghi',
'xyzabcdefghi',
'iiisssssssss'
),
array(
'abcdefg',
'abxdxfg',
'ssxsxss'
),
array(
'private function a($a, $b) {',
'public function and($b, $c) {',
'siixsdddxsssssssssssiissxsssxsss'
),
array(
// This is a test that we correctly detect shared prefixes and suffixes
// and don't trigger "give up, too long" mode if there's a small text
// change in an ocean of similar text.
' if ('.
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) {',
' if('.
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) {',
'ssssssssssds'.
'ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'.
'ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'.
'sssssssssssssssssssssssssssssssssssssss',
),
);
foreach ($tests as $test) {
$this->assertEqual(
$test[2],
ArcanistDiffUtils::buildLevenshteinDifferenceString($test[0], $test[1])
);
}
}
}
diff --git a/src/differential/ArcanistDifferentialCommitMessage.php b/src/differential/ArcanistDifferentialCommitMessage.php
index 95733663..9a0d3779 100644
--- a/src/differential/ArcanistDifferentialCommitMessage.php
+++ b/src/differential/ArcanistDifferentialCommitMessage.php
@@ -1,137 +1,121 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Represents a parsed commit message.
*
* @group differential
*/
final class ArcanistDifferentialCommitMessage {
private $rawCorpus;
private $revisionID;
private $fields = array();
private $gitSVNBaseRevision;
private $gitSVNBasePath;
private $gitSVNUUID;
public static function newFromRawCorpus($corpus) {
$obj = new ArcanistDifferentialCommitMessage();
$obj->rawCorpus = $corpus;
// Parse older-style "123" fields, or newer-style full-URI fields.
// TODO: Remove support for older-style fields.
$match = null;
if (preg_match('/^Differential Revision:\s*(.*)/im', $corpus, $match)) {
$revision_id = trim($match[1]);
if (strlen($revision_id)) {
if (preg_match('/^D?\d+$/', $revision_id)) {
$obj->revisionID = (int)trim($revision_id, 'D');
} else {
$uri = new PhutilURI($revision_id);
$path = $uri->getPath();
$path = trim($path, '/');
if (preg_match('/^D\d+$/', $path)) {
$obj->revisionID = (int)trim($path, 'D');
} else {
throw new ArcanistUsageException(
"Invalid 'Differential Revision' field. The field should have a ".
"Phabricator URI like 'http://phabricator.example.com/D123', ".
"but has '{$match[1]}'.");
}
}
}
}
$pattern = '/^git-svn-id:\s*([^@]+)@(\d+)\s+(.*)$/m';
if (preg_match($pattern, $corpus, $match)) {
$obj->gitSVNBaseRevision = $match[1].'@'.$match[2];
$obj->gitSVNBasePath = $match[1];
$obj->gitSVNUUID = $match[3];
}
return $obj;
}
public function getRawCorpus() {
return $this->rawCorpus;
}
public function getRevisionID() {
return $this->revisionID;
}
public function pullDataFromConduit(
ConduitClient $conduit,
$partial = false) {
$result = $conduit->callMethodSynchronous(
'differential.parsecommitmessage',
array(
'corpus' => $this->rawCorpus,
'partial' => $partial,
));
$this->fields = $result['fields'];
if (!empty($result['errors'])) {
throw new ArcanistDifferentialCommitMessageParserException(
$result['errors']);
}
return $this;
}
public function getFieldValue($key) {
if (array_key_exists($key, $this->fields)) {
return $this->fields[$key];
}
return null;
}
public function setFieldValue($key, $value) {
$this->fields[$key] = $value;
return $this;
}
public function getFields() {
return $this->fields;
}
public function getGitSVNBaseRevision() {
return $this->gitSVNBaseRevision;
}
public function getGitSVNBasePath() {
return $this->gitSVNBasePath;
}
public function getGitSVNUUID() {
return $this->gitSVNUUID;
}
public function getChecksum() {
$fields = array_filter($this->fields);
ksort($fields);
$fields = json_encode($fields);
return md5($fields);
}
}
diff --git a/src/differential/ArcanistDifferentialCommitMessageParserException.php b/src/differential/ArcanistDifferentialCommitMessageParserException.php
index dea1b0eb..680811dc 100644
--- a/src/differential/ArcanistDifferentialCommitMessageParserException.php
+++ b/src/differential/ArcanistDifferentialCommitMessageParserException.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown when a commit message isn't parseable.
*
* @group differential
*/
final class ArcanistDifferentialCommitMessageParserException extends Exception {
private $parserErrors;
public function __construct(array $errors) {
$this->parserErrors = $errors;
parent::__construct(head($errors));
}
public function getParserErrors() {
return $this->parserErrors;
}
}
diff --git a/src/differential/constants/ArcanistDifferentialRevisionHash.php b/src/differential/constants/ArcanistDifferentialRevisionHash.php
index cbbc43d0..6df8bc5c 100644
--- a/src/differential/constants/ArcanistDifferentialRevisionHash.php
+++ b/src/differential/constants/ArcanistDifferentialRevisionHash.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistDifferentialRevisionHash {
const TABLE_NAME = 'differential_revisionhash';
const HASH_GIT_COMMIT = 'gtcm';
const HASH_GIT_TREE = 'gttr';
const HASH_MERCURIAL_COMMIT = 'hgcm';
public static function getTypes() {
return array(
ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
);
}
}
diff --git a/src/differential/constants/ArcanistDifferentialRevisionStatus.php b/src/differential/constants/ArcanistDifferentialRevisionStatus.php
index 9c75e1bc..4f641ee0 100644
--- a/src/differential/constants/ArcanistDifferentialRevisionStatus.php
+++ b/src/differential/constants/ArcanistDifferentialRevisionStatus.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistDifferentialRevisionStatus {
const NEEDS_REVIEW = 0;
const NEEDS_REVISION = 1;
const ACCEPTED = 2;
const CLOSED = 3;
const ABANDONED = 4;
public static function getNameForRevisionStatus($status) {
static $map = array(
self::NEEDS_REVIEW => 'Needs Review',
self::NEEDS_REVISION => 'Needs Revision',
self::ACCEPTED => 'Accepted',
self::CLOSED => 'Closed',
self::ABANDONED => 'Abandoned',
);
return idx($map, coalesce($status, '?'), 'Unknown');
}
}
diff --git a/src/events/constant/ArcanistEventType.php b/src/events/constant/ArcanistEventType.php
index 99679e11..409fac72 100644
--- a/src/events/constant/ArcanistEventType.php
+++ b/src/events/constant/ArcanistEventType.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistEventType extends PhutilEventType {
const TYPE_COMMIT_WILLCOMMITSVN = 'commit.willCommitSVN';
const TYPE_DIFF_WILLBUILDMESSAGE = 'diff.willBuildMessage';
const TYPE_DIFF_DIDBUILDMESSAGE = 'diff.didBuildMessage';
const TYPE_DIFF_WASCREATED = 'diff.wasCreated';
const TYPE_REVISION_WILLCREATEREVISION = 'revision.willCreateRevision';
}
diff --git a/src/exception/ArcanistChooseInvalidRevisionException.php b/src/exception/ArcanistChooseInvalidRevisionException.php
index 459f0cc2..9c1b2893 100644
--- a/src/exception/ArcanistChooseInvalidRevisionException.php
+++ b/src/exception/ArcanistChooseInvalidRevisionException.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown when the user chooses an invalid revision when prompted by a workflow.
*
* @group workflow
*/
final class ArcanistChooseInvalidRevisionException extends Exception {
}
diff --git a/src/exception/ArcanistChooseNoRevisionsException.php b/src/exception/ArcanistChooseNoRevisionsException.php
index e0e7d2bf..562d910e 100644
--- a/src/exception/ArcanistChooseNoRevisionsException.php
+++ b/src/exception/ArcanistChooseNoRevisionsException.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown when there are no valid revisions to choose from, in a workflow which
* prompts the user to choose a revision.
*
* @group workflow
*/
final class ArcanistChooseNoRevisionsException extends Exception {
}
diff --git a/src/exception/ArcanistUsageException.php b/src/exception/ArcanistUsageException.php
index 6ec385eb..8a8c31f6 100644
--- a/src/exception/ArcanistUsageException.php
+++ b/src/exception/ArcanistUsageException.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown when there is a problem with how a user is invoking a command, rather
* than a technical problem.
*
* @group workflow
* @concrete-extensible
*/
class ArcanistUsageException extends Exception {
}
diff --git a/src/exception/usage/ArcanistNoEffectException.php b/src/exception/usage/ArcanistNoEffectException.php
index 4cca1ab0..8a2e3201 100644
--- a/src/exception/usage/ArcanistNoEffectException.php
+++ b/src/exception/usage/ArcanistNoEffectException.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown when lint or unit tests have no effect, i.e. no paths are affected
* by any linter or no unit tests provide coverage.
*
* @group workflow
*/
final class ArcanistNoEffectException extends ArcanistUsageException {
}
diff --git a/src/exception/usage/ArcanistNoEngineException.php b/src/exception/usage/ArcanistNoEngineException.php
index ba76e449..b9ddb129 100644
--- a/src/exception/usage/ArcanistNoEngineException.php
+++ b/src/exception/usage/ArcanistNoEngineException.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown when no engine is configured for linting or running unit tests.
*
* @group workflow
*/
final class ArcanistNoEngineException extends ArcanistUsageException {
}
diff --git a/src/exception/usage/ArcanistUncommittedChangesException.php b/src/exception/usage/ArcanistUncommittedChangesException.php
index 433751be..488150b0 100644
--- a/src/exception/usage/ArcanistUncommittedChangesException.php
+++ b/src/exception/usage/ArcanistUncommittedChangesException.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistUncommittedChangesException extends ArcanistUsageException {
}
diff --git a/src/exception/usage/ArcanistUserAbortException.php b/src/exception/usage/ArcanistUserAbortException.php
index c6ed1d5c..02b9daf1 100644
--- a/src/exception/usage/ArcanistUserAbortException.php
+++ b/src/exception/usage/ArcanistUserAbortException.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown when the user chooses not to continue when warned that they're about
* to do something dangerous.
*
* @group workflow
*/
final class ArcanistUserAbortException extends ArcanistUsageException {
public function __construct() {
parent::__construct('User aborted the workflow.');
}
}
diff --git a/src/hgdaemon/ArcanistHgClientChannel.php b/src/hgdaemon/ArcanistHgClientChannel.php
index 3a9bdd43..61b97940 100644
--- a/src/hgdaemon/ArcanistHgClientChannel.php
+++ b/src/hgdaemon/ArcanistHgClientChannel.php
@@ -1,186 +1,170 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Channel to a Mercurial "cmdserver" client. For a detailed description of the
* "cmdserver" protocol, see @{class:ArcanistHgServerChannel}. This channel
* implements the other half of the protocol: it decodes messages from the
* client and encodes messages from the server.
*
* Because the proxy server speaks the exact same protocol that Mercurial
* does and fully decodes both sides of the protocol, we need this half of the
* decode/encode to talk to clients. Without it, we wouldn't be able to
* determine when a client request had completed and was ready for transmission
* to the Mercurial server.
*
* (Technically, we could get away without re-encoding messages from the
* server, but the serialization is not complicated and having a general
* implementation of encoded/decode for both the client and server dialects
* seemed useful.)
*
* @task protocol Protocol Implementation
*/
final class ArcanistHgClientChannel extends PhutilProtocolChannel {
const MODE_COMMAND = 'command';
const MODE_LENGTH = 'length';
const MODE_ARGUMENTS = 'arguments';
private $command;
private $byteLengthOfNextChunk;
private $buf = '';
private $mode = self::MODE_COMMAND;
/* -( Protocol Implementation )-------------------------------------------- */
/**
* Encode a message for transmission to the client. The message should be
* a pair with the channel name and the a block of data, like this:
*
* array('o', '<some data...>');
*
* We encode it like this:
*
* o
* 1234 # Length, as a 4-byte unsigned long.
* <data: 1234 bytes>
*
* For a detailed description of the cmdserver protocol, see
* @{class:ArcanistHgServerChannel}.
*
* @param pair<string,string> The <channel, data> pair to encode.
* @return string Encoded string for transmission to the client.
*
* @task protocol
*/
protected function encodeMessage($argv) {
if (!is_array($argv) || count($argv) !== 2) {
throw new Exception("Message should be <channel, data>.");
}
$channel = head($argv);
$data = last($argv);
$len = strlen($data);
$len = pack('N', $len);
return "{$channel}{$len}{$data}";
}
/**
* Decode a message received from the client. The message looks like this:
*
* runcommand\n
* 8 # Length, as a 4-byte unsigned long.
* log\0
* -l\0
* 5
*
* We decode it into a list in PHP, which looks like this:
*
* array(
* 'runcommand',
* 'log',
* '-l',
* '5',
* );
*
* @param string Bytes from the server.
* @return list<list<string>> Zero or more complete commands.
*
* @task protocol
*/
protected function decodeStream($data) {
$this->buf .= $data;
// The first part is terminated by "\n", so we don't always know how many
// bytes we need to look for. This makes parsing a bit of a pain.
$messages = array();
do {
$continue_parsing = false;
switch ($this->mode) {
case self::MODE_COMMAND:
// We're looking for "\n", which indicates the end of the command
// name, like "runcommand". Next, we'll expect a length.
$pos = strpos($this->buf, "\n");
if ($pos === false) {
break;
}
$this->command = substr($this->buf, 0, $pos);
$this->buf = substr($this->buf, $pos + 1);
$this->mode = self::MODE_LENGTH;
$continue_parsing = true;
break;
case self::MODE_LENGTH:
// We're looking for a byte length, as a 4-byte big-endian unsigned
// integer. Next, we'll expect that many bytes of data.
if (strlen($this->buf) < 4) {
break;
}
$len = substr($this->buf, 0, 4);
$len = unpack('N', $len);
$len = head($len);
$this->buf = substr($this->buf, 4);
$this->mode = self::MODE_ARGUMENTS;
$this->byteLengthOfNextChunk = $len;
$continue_parsing = true;
break;
case self::MODE_ARGUMENTS:
// We're looking for the data itself, which is a block of bytes
// of the given length. These are arguments delimited by "\0". Next
// we'll expect another command.
if (strlen($this->buf) < $this->byteLengthOfNextChunk) {
break;
}
$data = substr($this->buf, 0, $this->byteLengthOfNextChunk);
$this->buf = substr($this->buf, $this->byteLengthOfNextChunk);
$message = array_merge(array($this->command), explode("\0", $data));
$this->mode = self::MODE_COMMAND;
$this->command = null;
$this->byteLengthOfNextChunk = null;
$messages[] = $message;
$continue_parsing = true;
break;
}
} while ($continue_parsing);
return $messages;
}
}
diff --git a/src/hgdaemon/ArcanistHgProxyClient.php b/src/hgdaemon/ArcanistHgProxyClient.php
index 7ab36991..3fb1af78 100644
--- a/src/hgdaemon/ArcanistHgProxyClient.php
+++ b/src/hgdaemon/ArcanistHgProxyClient.php
@@ -1,214 +1,198 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Client for an @{class:ArcanistHgProxyServer}. This client talks to a PHP
* process which serves as a proxy in front of a Mercurial server process.
* The PHP proxy allows multiple clients to use the same Mercurial server.
*
* This class presents an API which is similar to the hg command-line API.
*
* Each client is bound to a specific working copy:
*
* $working_copy = '/path/to/some/hg/working/copy/';
* $client = new ArcanistHgProxyClient($working_copy);
*
* For example, to run `hg log -l 5` via a client:
*
* $command = array('log', '-l', '5');
* list($err, $stdout, $stderr) = $client->executeCommand($command);
*
* The advantage of using this complex mechanism is that commands run in this
* way do not need to pay the startup overhead for hg and the Python runtime,
* which is often on the order of 100ms or more per command.
*
* @task construct Construction
* @task config Configuration
* @task exec Executing Mercurial Commands
* @task internal Internals
*/
final class ArcanistHgProxyClient {
private $workingCopy;
private $server;
private $skipHello;
/* -( Construction )------------------------------------------------------- */
/**
* Build a new client. This client is bound to a working copy. A server
* must already be running on this working copy for the client to work.
*
* @param string Path to a Mercurial working copy.
*
* @task construct
*/
public function __construct($working_copy) {
$this->workingCopy = Filesystem::resolvePath($working_copy);
}
/* -( Configuration )------------------------------------------------------ */
/**
* When connecting, do not expect the "capabilities" message.
*
* @param bool True to skip the "capabilities" message.
* @return this
*
* @task config
*/
public function setSkipHello($skip) {
$this->skipHello = $skip;
return $this;
}
/* -( Executing Merucurial Commands )-------------------------------------- */
/**
* Execute a command (given as a list of arguments) via the command server.
*
* @param list<string> A list of command arguments, like "log", "-l", "5".
* @return tuple<int, string, string> Return code, stdout and stderr.
*
* @task exec
*/
public function executeCommand(array $argv) {
if (!$this->server) {
try {
$server = $this->connectToDaemon();
} catch (Exception $ex) {
$this->launchDaemon();
$server = $this->connectToDaemon();
}
$this->server = $server;
}
$server = $this->server;
// Note that we're adding "runcommand" to make the server run the command.
// Theoretically the server supports other capabilities, but in practice
// we are only concerend with "runcommand".
$server->write(array_merge(array('runcommand'), $argv));
// We'll get back one or more blocks of response data, ending with an 'r'
// block which indicates the return code. Reconstitute these into stdout,
// stderr and a return code.
$stdout = '';
$stderr = '';
$err = 0;
$done = false;
while ($message = $server->waitForMessage()) {
// The $server channel handles decoding of the wire format and gives us
// messages which look like this:
//
// array('o', '<data...>');
list($channel, $data) = $message;
switch ($channel) {
case 'o':
$stdout .= $data;
break;
case 'e':
$stderr .= $data;
break;
case 'd':
// TODO: Do something with this? This is the 'debug' channel.
break;
case 'r':
// NOTE: This little dance is because the value is emitted as a
// big-endian signed 32-bit long. PHP has no flag to unpack() that
// can unpack these, so we unpack a big-endian unsigned long, then
// repack it as a machine-order unsigned long, then unpack it as
// a machine-order signed long. This appears to produce the desired
// result.
$err = head(unpack('N', $data));
$err = pack('L', $err);
$err = head(unpack('l', $err));
$done = true;
break;
}
if ($done) {
break;
}
}
return array($err, $stdout, $stderr);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function connectToDaemon() {
$errno = null;
$errstr = null;
$socket_path = ArcanistHgProxyServer::getPathToSocket($this->workingCopy);
$socket = @stream_socket_client('unix://'.$socket_path, $errno, $errstr);
if ($errno || !$socket) {
throw new Exception(
"Unable to connect socket! Error #{$errno}: {$errstr}");
}
$channel = new PhutilSocketChannel($socket);
$server = new ArcanistHgServerChannel($channel);
if (!$this->skipHello) {
// The protocol includes a "hello" message with capability and encoding
// information. Read and discard it, we use only the "runcommand"
// capability which is guaranteed to be available.
$hello = $server->waitForMessage();
}
return $server;
}
/**
* @task internal
*/
private function launchDaemon() {
$root = dirname(phutil_get_library_root('arcanist'));
$bin = $root.'/scripts/hgdaemon/hgdaemon_server.php';
$proxy = new ExecFuture(
'%s %s --idle-limit 15 --quiet %C',
$bin,
$this->workingCopy,
$this->skipHello ? '--skip-hello' : null);
$proxy->resolvex();
}
}
diff --git a/src/hgdaemon/ArcanistHgProxyServer.php b/src/hgdaemon/ArcanistHgProxyServer.php
index 0dfc8ab8..5e41f441 100644
--- a/src/hgdaemon/ArcanistHgProxyServer.php
+++ b/src/hgdaemon/ArcanistHgProxyServer.php
@@ -1,504 +1,488 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Server which @{class:ArcanistHgProxyClient} clients connect to. This
* server binds to a Mercurial working copy and creates a Mercurial process and
* a unix domain socket in that working copy. It listens for connections on
* the socket, reads commands from them, and forwards their requests to the
* Mercurial process. It then returns responses to the original clients.
*
* Note that this server understands the underlying protocol and completely
* decodes messages from both the client and server before re-encoding them
* and relaying them to their final destinations. It must do this (at least
* in part) to determine where messages begin and end. Additionally, this proxy
* sends and receives the Mercurial cmdserver protocol exactly, without
* any extensions or sneakiness.
*
* The advantage of this mechanism is that it avoids the overhead of starting
* a Mercurial process for each Mercurial command, which can exceed 100ms per
* invocation. This server can also accept connections from multiple clients
* and serve them from a single Mercurial server process.
*
* @task construct Construction
* @task config Configuration
* @task server Serving Requests
* @task client Managing Clients
* @task hg Managing Mercurial
* @task internal Internals
*/
final class ArcanistHgProxyServer {
private $workingCopy;
private $socket;
private $hello;
private $quiet;
private $clientLimit;
private $lifetimeClientCount;
private $idleLimit;
private $idleSince;
private $skipHello;
private $doNotDaemonize;
/* -( Construction )------------------------------------------------------- */
/**
* Build a new server. This server is bound to a working copy. The server
* is inactive until you @{method:start} it.
*
* @param string Path to a Mercurial working copy.
*
* @task construct
*/
public function __construct($working_copy) {
$this->workingCopy = Filesystem::resolvePath($working_copy);
}
/* -( Configuration )------------------------------------------------------ */
/**
* Disable status messages to stdout. Controlled with `--quiet`.
*
* @param bool True to disable status messages.
* @return this
*
* @task config
*/
public function setQuiet($quiet) {
$this->quiet = $quiet;
return $this;
}
/**
* Configure a client limit. After serving this many clients, the server
* will exit. Controlled with `--client-limit`.
*
* You can use `--client-limit 1` with `--xprofile` and `--do-not-daemonize`
* to profile the server.
*
* @param int Client limit, or 0 to disable limit.
* @return this
*
* @task config
*/
public function setClientLimit($limit) {
$this->clientLimit = $limit;
return $this;
}
/**
* Configure an idle time limit. After this many seconds idle, the server
* will exit. Controlled with `--idle-limit`.
*
* @param int Idle limit, or 0 to disable limit.
* @return this
*
* @task config
*/
public function setIdleLimit($limit) {
$this->idleLimit = $limit;
return $this;
}
/**
* When clients connect, do not send the "capabilities" message expected by
* the Mercurial protocol. This deviates from the protocol and will only work
* if the clients are also configured not to expect the message, but slightly
* improves performance. Controlled with --skip-hello.
*
* @param bool True to skip the "capabilities" message.
* @return this
*
* @task config
*/
public function setSkipHello($skip) {
$this->skipHello = $skip;
return $this;
}
/**
* Configure whether the server runs in the foreground or daemonizes.
* Controlled by --do-not-daemonize. Primarily useful for debugging.
*
* @param bool True to run in the foreground.
* @return this
*
* @task config
*/
public function setDoNotDaemonize($do_not_daemonize) {
$this->doNotDaemonize = $do_not_daemonize;
return $this;
}
/* -( Serving Requests )--------------------------------------------------- */
/**
* Start the server. This method returns after the client limit or idle
* limit are exceeded. If neither limit is configured, this method does not
* exit.
*
* @return null
*
* @task server
*/
public function start() {
// Create the unix domain socket in the working copy to listen for clients.
$socket = $this->startWorkingCopySocket();
$this->socket = $socket;
if (!$this->doNotDaemonize) {
$this->daemonize();
}
// Start the Mercurial process which we'll forward client requests to.
$hg = $this->startMercurialProcess();
$clients = array();
$this->log(null, 'Listening');
$this->idleSince = time();
while (true) {
// Wait for activity on any active clients, the Mercurial process, or
// the listening socket where new clients connect.
PhutilChannel::waitForAny(
array_merge($clients, array($hg)),
array(
'read' => $socket ? array($socket) : array(),
'except' => $socket ? array($socket) : array()
));
if (!$hg->update()) {
throw new Exception("Server exited unexpectedly!");
}
// Accept any new clients.
while ($socket && ($client = $this->acceptNewClient($socket))) {
$clients[] = $client;
$key = last_key($clients);
$client->setName($key);
$this->log($client, 'Connected');
$this->idleSince = time();
// Check if we've hit the client limit. If there's a configured
// client limit and we've hit it, stop accepting new connections
// and close the socket.
$this->lifetimeClientCount++;
if ($this->clientLimit) {
if ($this->lifetimeClientCount >= $this->clientLimit) {
$this->closeSocket();
$socket = null;
}
}
}
// Update all the active clients.
foreach ($clients as $key => $client) {
if ($this->updateClient($client, $hg)) {
// In this case, the client is still connected so just move on to
// the next one. Otherwise we continue below and handle the disconect.
continue;
}
$this->log($client, 'Disconnected');
unset($clients[$key]);
// If we have a client limit and we've served that many clients, exit.
if ($this->clientLimit) {
if ($this->lifetimeClientCount >= $this->clientLimit) {
if (!$clients) {
$this->log(null, 'Exiting (Client Limit)');
return;
}
}
}
}
// If we have an idle limit and haven't had any activity in at least
// that long, exit.
if ($this->idleLimit) {
$remaining = $this->idleLimit - (time() - $this->idleSince);
if ($remaining <= 0) {
$this->log(null, 'Exiting (Idle Limit)');
return;
}
if ($remaining <= 5) {
$this->log(null, 'Exiting in '.$remaining.' seconds');
}
}
}
}
/**
* Update one client, processing any commands it has sent us. We fully
* process all commands we've received here before returning to the main
* server loop.
*
* @param ArcanistHgClientChannel The client to update.
* @param ArcanistHgServerChannel The Mercurial server.
*
* @task server
*/
private function updateClient(
ArcanistHgClientChannel $client,
ArcanistHgServerChannel $hg) {
if (!$client->update()) {
// Client has disconnected, don't bother proceeding.
return false;
}
// Read a command from the client if one is available. Note that we stop
// updating other clients or accepting new connections while processing a
// command, since there isn't much we can do with them until the server
// finishes executing this command.
$message = $client->read();
if (!$message) {
return true;
}
$this->log($client, '$ '.$message[0].' '.$message[1]);
$t_start = microtime(true);
// Forward the command to the server.
$hg->write($message);
while (true) {
PhutilChannel::waitForAny(array($client, $hg));
if (!$client->update() || !$hg->update()) {
// If either the client or server has exited, bail.
return false;
}
$response = $hg->read();
if (!$response) {
continue;
}
// Forward the response back to the client.
$client->write($response);
// If the response was on the 'r'esult channel, it indicates the end
// of the command output. We can process the next command (if any
// remain) or go back to accepting new connections and servicing
// other clients.
if ($response[0] == 'r') {
// Update the client immediately to try to get the bytes on the wire
// as quickly as possible. This gives us slightly more throughput.
$client->update();
break;
}
}
// Log the elapsed time.
$t_end = microtime(true);
$t = 1000000 * ($t_end - $t_start);
$this->log($client, '< '.number_format($t, 0).'us');
$this->idleSince = time();
return true;
}
/* -( Managing Clients )--------------------------------------------------- */
/**
* @task client
*/
public static function getPathToSocket($working_copy) {
return $working_copy.'/.hg/hgdaemon-socket';
}
/**
* @task client
*/
private function startWorkingCopySocket() {
$errno = null;
$errstr = null;
$socket_path = self::getPathToSocket($this->workingCopy);
$socket_uri = 'unix://'.$socket_path;
$socket = @stream_socket_server($socket_uri, $errno, $errstr);
if ($errno || !$socket) {
Filesystem::remove($socket_path);
$socket = @stream_socket_server($socket_uri, $errno, $errstr);
}
if ($errno || !$socket) {
throw new Exception(
"Unable to start socket! Error #{$errno}: {$errstr}");
}
$ok = stream_set_blocking($socket, 0);
if ($ok === false) {
throw new Exception("Unable to set socket nonblocking!");
}
return $socket;
}
/**
* @task client
*/
private function acceptNewClient($socket) {
// NOTE: stream_socket_accept() always blocks, even when the socket has
// been set nonblocking.
$new_client = @stream_socket_accept($socket, $timeout = 0);
if (!$new_client) {
return null;
}
$channel = new PhutilSocketChannel($new_client);
$client = new ArcanistHgClientChannel($channel);
if (!$this->skipHello) {
$client->write($this->hello);
}
return $client;
}
/* -( Managing Mercurial )------------------------------------------------- */
/**
* Starts a Mercurial process which can actually handle requests.
*
* @return ArcanistHgServerChannel Channel to the Mercurial server.
* @task hg
*/
private function startMercurialProcess() {
// NOTE: "cmdserver.log=-" makes Mercurial use the 'd'ebug channel for
// log messages.
$command = 'HGPLAIN=1 hg --config cmdserver.log=- serve --cmdserver pipe';
$future = new ExecFuture($command);
$future->setCWD($this->workingCopy);
$channel = new PhutilExecChannel($future);
$hg = new ArcanistHgServerChannel($channel);
// The server sends a "hello" message with capability and encoding
// information. Save it and forward it to clients when they connect.
$this->hello = $hg->waitForMessage();
return $hg;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Close and remove the unix domain socket in the working copy.
*
* @task internal
*/
public function __destruct() {
$this->closeSocket();
}
private function closeSocket() {
if ($this->socket) {
@stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
@fclose($this->socket);
Filesystem::remove(self::getPathToSocket($this->workingCopy));
$this->socket = null;
}
}
private function log($client, $message) {
if ($this->quiet) {
return;
}
if ($client) {
$message = '[Client '.$client->getName().'] '.$message;
} else {
$message = '[Server] '.$message;
}
echo $message."\n";
}
private function daemonize() {
// Keep stdout if it's been redirected somewhere, otherwise shut it down.
$keep_stdout = false;
$keep_stderr = false;
if (function_exists('posix_isatty')) {
if (!posix_isatty(STDOUT)) {
$keep_stdout = true;
}
if (!posix_isatty(STDERR)) {
$keep_stderr = true;
}
}
$pid = pcntl_fork();
if ($pid === -1) {
throw new Exception("Unable to fork!");
} else if ($pid) {
// We're the parent; exit. First, drop our reference to the socket so
// our __destruct() doesn't tear it down; the child will tear it down
// later.
$this->socket = null;
exit(0);
}
// We're the child; continue.
fclose(STDIN);
if (!$keep_stdout) {
fclose(STDOUT);
$this->quiet = true;
}
if (!$keep_stderr) {
fclose(STDERR);
}
}
}
diff --git a/src/hgdaemon/ArcanistHgServerChannel.php b/src/hgdaemon/ArcanistHgServerChannel.php
index e77df37a..da57e81f 100644
--- a/src/hgdaemon/ArcanistHgServerChannel.php
+++ b/src/hgdaemon/ArcanistHgServerChannel.php
@@ -1,193 +1,177 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Channel to a Mercurial "cmdserver" server. Messages sent to the server
* look like this:
*
* runcommand\n
* 8 # Length, as a 4-byte unsigned long.
* log\0
* -l\0
* 5
*
* In PHP, the format of these messages is an array of arguments:
*
* array(
* 'runcommand',
* 'log',
* '-l',
* '5',
* );
*
* The server replies with messages that look like this:
*
* o
* 1234 # Length, as a 4-byte unsigned long.
* <data: 1234 bytes>
*
* The first character in a message from the server is the "channel". Mercurial
* channels have nothing to do with Phutil channels; they are more similar to
* stdout/stderr. Mercurial has four primary channels:
*
* 'o'utput, like stdout
* 'e'rror, like stderr
* 'r'esult, like return codes
* 'd'ebug, like an external log file
*
* In PHP, the format of these messages is a pair, with the channel and then
* the data:
*
* array('o', '<data...>');
*
* In general, we send "runcommand" requests, and the server responds with
* a series of messages on the "output" channel and then a single response
* on the "result" channel to indicate that output is complete.
*
* @task protocol Protocol Implementation
*/
final class ArcanistHgServerChannel extends PhutilProtocolChannel {
const MODE_CHANNEL = 'channel';
const MODE_LENGTH = 'length';
const MODE_BLOCK = 'block';
private $mode = self::MODE_CHANNEL;
private $byteLengthOfNextChunk = 1;
private $buf = '';
/* -( Protocol Implementation )-------------------------------------------- */
/**
* Encode a message for transmission to the server. The message should be
* formatted as an array, like this:
*
* array(
* 'runcommand',
* 'log',
* '-l',
* '5',
* );
*
*
* We will return the cmdserver version of this:
*
* runcommand\n
* 8 # Length, as a 4-byte unsigned long.
* log\0
* -l\0
* 5
*
* @param list<string> List of command arguments.
* @return string Encoded string for transmission to the server.
*
* @task protocol
*/
protected function encodeMessage($argv) {
if (!is_array($argv)) {
throw new Exception("Message to Mercurial server should be an array.");
}
$command = head($argv);
$args = array_slice($argv, 1);
$args = implode("\0", $args);
$len = strlen($args);
$len = pack('N', $len);
return "{$command}\n{$len}{$args}";
}
/**
* Decode a message received from the server. The message looks like this:
*
* o
* 1234 # Length, as a 4-byte unsigned long.
* <data: 1234 bytes>
*
* ...where 'o' is the "channel" the message is being sent over.
*
* We decode into a pair in PHP, which looks like this:
*
* array('o', '<data...>');
*
* @param string Bytes from the server.
* @return list<pair<string,string>> Zero or more complete messages.
*
* @task protocol
*/
protected function decodeStream($data) {
$this->buf .= $data;
// We always know how long the next chunk is, so this parser is fairly
// easy to implement.
$messages = array();
while ($this->byteLengthOfNextChunk <= strlen($this->buf)) {
$chunk = substr($this->buf, 0, $this->byteLengthOfNextChunk);
$this->buf = substr($this->buf, $this->byteLengthOfNextChunk);
switch ($this->mode) {
case self::MODE_CHANNEL:
// We've received the channel name, one of 'o', 'e', 'r' or 'd' for
// 'output', 'error', 'result' or 'debug' respectively. This is a
// single byte long. Next, we'll expect a length.
$this->channel = $chunk;
$this->byteLengthOfNextChunk = 4;
$this->mode = self::MODE_LENGTH;
break;
case self::MODE_LENGTH:
// We've received the length of the data, as a 4-byte big-endian
// unsigned integer. Next, we'll expect the data itself.
$this->byteLengthOfNextChunk = head(unpack('N', $chunk));
$this->mode = self::MODE_BLOCK;
break;
case self::MODE_BLOCK:
// We've received the data itself, which is a block of bytes of the
// given length. We produce a message from the channel and the data
// and return it. Next, we expect another channel name.
$message = array($this->channel, $chunk);
$this->byteLengthOfNextChunk = 1;
$this->mode = self::MODE_CHANNEL;
$this->channel = null;
$messages[] = $message;
break;
}
}
// Return zero or more messages, which might look something like this:
//
// array(
// array('o', '<...>'),
// array('o', '<...>'),
// array('r', '<...>'),
// );
return $messages;
}
}
diff --git a/src/infrastructure/testing/ArcanistTestCase.php b/src/infrastructure/testing/ArcanistTestCase.php
index 9e011b71..41de1242 100644
--- a/src/infrastructure/testing/ArcanistTestCase.php
+++ b/src/infrastructure/testing/ArcanistTestCase.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
abstract class ArcanistTestCase extends ArcanistPhutilTestCase {
protected function getLink($method) {
$arcanist_project = 'PHID-APRJ-703e0b140530f17ede30';
return
'https://secure.phabricator.com/diffusion/symbol/'.$method.
'/?lang=php&projects='.$arcanist_project.
'&jump=true&context='.get_class($this);
}
}
diff --git a/src/lint/ArcanistLintMessage.php b/src/lint/ArcanistLintMessage.php
index 4b997e09..6da5a0d8 100644
--- a/src/lint/ArcanistLintMessage.php
+++ b/src/lint/ArcanistLintMessage.php
@@ -1,190 +1,174 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Message emitted by a linter, like an error or warning.
*
* @group lint
*/
final class ArcanistLintMessage {
protected $path;
protected $line;
protected $char;
protected $code;
protected $severity;
protected $name;
protected $description;
protected $originalText;
protected $replacementText;
protected $appliedToDisk;
protected $dependentMessages = array();
protected $obsolete;
public static function newFromDictionary(array $dict) {
$message = new ArcanistLintMessage();
$message->setPath($dict['path']);
$message->setLine($dict['line']);
$message->setChar($dict['char']);
$message->setCode($dict['code']);
$message->setSeverity($dict['severity']);
$message->setName($dict['name']);
$message->setDescription($dict['description']);
if (isset($dict['original'])) {
$message->setOriginalText($dict['original']);
}
if (isset($dict['replacement'])) {
$message->setReplacementText($dict['replacement']);
}
return $message;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function setLine($line) {
$this->line = $line;
return $this;
}
public function getLine() {
return $this->line;
}
public function setChar($char) {
$this->char = $char;
return $this;
}
public function getChar() {
return $this->char;
}
public function setCode($code) {
$this->code = $code;
return $this;
}
public function getCode() {
return $this->code;
}
public function setSeverity($severity) {
$this->severity = $severity;
return $this;
}
public function getSeverity() {
return $this->severity;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setOriginalText($original) {
$this->originalText = $original;
return $this;
}
public function getOriginalText() {
return $this->originalText;
}
public function setReplacementText($replacement) {
$this->replacementText = $replacement;
return $this;
}
public function getReplacementText() {
return $this->replacementText;
}
public function isError() {
return $this->getSeverity() == ArcanistLintSeverity::SEVERITY_ERROR;
}
public function isWarning() {
return $this->getSeverity() == ArcanistLintSeverity::SEVERITY_WARNING;
}
public function isAutofix() {
return $this->getSeverity() == ArcanistLintSeverity::SEVERITY_AUTOFIX;
}
public function hasFileContext() {
return ($this->getLine() !== null);
}
public function setObsolete($obsolete) {
$this->obsolete = $obsolete;
return $this;
}
public function getObsolete() {
return $this->obsolete;
}
public function isPatchable() {
return ($this->getReplacementText() !== null) &&
($this->getReplacementText() !== $this->getOriginalText());
}
public function didApplyPatch() {
if ($this->appliedToDisk) {
return $this;
}
$this->appliedToDisk = true;
foreach ($this->dependentMessages as $message) {
$message->didApplyPatch();
}
return $this;
}
public function isPatchApplied() {
return $this->appliedToDisk;
}
public function setDependentMessages(array $messages) {
assert_instances_of($messages, 'ArcanistLintMessage');
$this->dependentMessages = $messages;
return $this;
}
}
diff --git a/src/lint/ArcanistLintPatcher.php b/src/lint/ArcanistLintPatcher.php
index de687147..5ec3825d 100644
--- a/src/lint/ArcanistLintPatcher.php
+++ b/src/lint/ArcanistLintPatcher.php
@@ -1,161 +1,145 @@
<?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.
- */
-
/**
* Applies lint patches to the working copy.
*
* @group lint
*/
final class ArcanistLintPatcher {
private $dirtyUntil = 0;
private $characterDelta = 0;
private $modifiedData = null;
private $lineOffsets = null;
private $lintResult = null;
private $applyMessages = array();
public static function newFromArcanistLintResult(ArcanistLintResult $result) {
$obj = new ArcanistLintPatcher();
$obj->lintResult = $result;
return $obj;
}
public function getUnmodifiedFileContent() {
return $this->lintResult->getData();
}
public function getModifiedFileContent() {
if ($this->modifiedData === null) {
$this->buildModifiedFile();
}
return $this->modifiedData;
}
public function writePatchToDisk() {
$path = $this->lintResult->getFilePathOnDisk();
$data = $this->getModifiedFileContent();
$ii = null;
do {
$lint = $path.'.linted'.($ii++);
} while (file_exists($lint));
// Copy existing file to preserve permissions. 'chmod --reference' is not
// supported under OSX.
if (Filesystem::pathExists($path)) {
// This path may not exist if we're generating a new file.
execx('cp -p %s %s', $path, $lint);
}
Filesystem::writeFile($lint, $data);
list($err) = exec_manual("mv -f %s %s", $lint, $path);
if ($err) {
throw new Exception(
"Unable to overwrite path `{$path}', patched version was left ".
"at `{$lint}'.");
}
foreach ($this->applyMessages as $message) {
$message->didApplyPatch();
}
}
private function __construct() {
}
private function buildModifiedFile() {
$data = $this->getUnmodifiedFileContent();
foreach ($this->lintResult->getMessages() as $lint) {
if (!$lint->isPatchable()) {
continue;
}
$orig_offset = $this->getCharacterOffset($lint->getLine() - 1);
$orig_offset += $lint->getChar() - 1;
$dirty = $this->getDirtyCharacterOffset();
if ($dirty > $orig_offset) {
continue;
}
// Adjust the character offset by the delta *after* checking for
// dirtiness. The dirty character cursor is a cursor on the original file,
// and should be compared with the patch position in the original file.
$working_offset = $orig_offset + $this->getCharacterDelta();
$old_str = $lint->getOriginalText();
$old_len = strlen($old_str);
$new_str = $lint->getReplacementText();
$new_len = strlen($new_str);
if ($working_offset == strlen($data)) {
// Temporary hack to work around a destructive hphpi issue, see #451031.
$data .= $new_str;
} else {
$data = substr_replace($data, $new_str, $working_offset, $old_len);
}
$this->changeCharacterDelta($new_len - $old_len);
$this->setDirtyCharacterOffset($orig_offset + $old_len);
$this->applyMessages[] = $lint;
}
$this->modifiedData = $data;
}
private function getCharacterOffset($line_num) {
if ($this->lineOffsets === null) {
$lines = explode("\n", $this->getUnmodifiedFileContent());
$this->lineOffsets = array(0);
$last = 0;
foreach ($lines as $line) {
$this->lineOffsets[] = $last + strlen($line) + 1;
$last += strlen($line) + 1;
}
}
if ($line_num >= count($this->lineOffsets)) {
throw new Exception("Data has fewer than `{$line}' lines.");
}
return idx($this->lineOffsets, $line_num);
}
private function setDirtyCharacterOffset($offset) {
$this->dirtyUntil = $offset;
return $this;
}
private function getDirtyCharacterOffset() {
return $this->dirtyUntil;
}
private function changeCharacterDelta($change) {
$this->characterDelta += $change;
return $this;
}
private function getCharacterDelta() {
return $this->characterDelta;
}
}
diff --git a/src/lint/ArcanistLintResult.php b/src/lint/ArcanistLintResult.php
index 5f40ffa4..ab3a5d1e 100644
--- a/src/lint/ArcanistLintResult.php
+++ b/src/lint/ArcanistLintResult.php
@@ -1,113 +1,97 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* A group of @{class:ArcanistLintMessage}s that apply to a file.
*
* @group lint
*/
final class ArcanistLintResult {
protected $path;
protected $data;
protected $filePathOnDisk;
protected $messages = array();
protected $effectiveMessages = array();
private $needsSort;
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function addMessage(ArcanistLintMessage $message) {
$this->messages[] = $message;
$this->needsSort = true;
return $this;
}
public function getMessages() {
if ($this->needsSort) {
$this->sortAndFilterMessages();
}
return $this->effectiveMessages;
}
public function setData($data) {
$this->data = $data;
return $this;
}
public function getData() {
return $this->data;
}
public function setFilePathOnDisk($file_path_on_disk) {
$this->filePathOnDisk = $file_path_on_disk;
return $this;
}
public function getFilePathOnDisk() {
return $this->filePathOnDisk;
}
public function isPatchable() {
foreach ($this->messages as $message) {
if ($message->isPatchable()) {
return true;
}
}
return false;
}
public function isAllAutofix() {
foreach ($this->messages as $message) {
if (!$message->isAutofix()) {
return false;
}
}
return true;
}
private function sortAndFilterMessages() {
$messages = $this->messages;
foreach ($messages as $key => $message) {
if ($message->getObsolete()) {
unset($messages[$key]);
continue;
}
}
$map = array();
foreach ($messages as $key => $message) {
$map[$key] = ($message->getLine() * (2 << 12)) + $message->getChar();
}
asort($map);
$messages = array_select_keys($messages, array_keys($map));
$this->effectiveMessages = $messages;
$this->needsSort = false;
}
}
diff --git a/src/lint/ArcanistLintSeverity.php b/src/lint/ArcanistLintSeverity.php
index 83ba9c5b..f239e1a2 100644
--- a/src/lint/ArcanistLintSeverity.php
+++ b/src/lint/ArcanistLintSeverity.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Describes the severity of an @{class:ArcanistLintMessage}.
*
* @group lint
*/
final class ArcanistLintSeverity {
const SEVERITY_ADVICE = 'advice';
const SEVERITY_AUTOFIX = 'autofix';
const SEVERITY_WARNING = 'warning';
const SEVERITY_ERROR = 'error';
const SEVERITY_DISABLED = 'disabled';
public static function getStringForSeverity($severity_code) {
static $map = array(
self::SEVERITY_ADVICE => 'Advice',
self::SEVERITY_AUTOFIX => 'Auto-Fix',
self::SEVERITY_WARNING => 'Warning',
self::SEVERITY_ERROR => 'Error',
self::SEVERITY_DISABLED => 'Disabled',
);
if (!array_key_exists($severity_code, $map)) {
throw new Exception("Unknown lint severity '{$severity_code}'!");
}
return $map[$severity_code];
}
public static function isAtLeastAsSevere(
ArcanistLintMessage $message,
$level) {
static $map = array(
self::SEVERITY_DISABLED => 10,
self::SEVERITY_ADVICE => 20,
self::SEVERITY_AUTOFIX => 25,
self::SEVERITY_WARNING => 30,
self::SEVERITY_ERROR => 40,
);
$message_sev = $message->getSeverity();
if (empty($map[$message_sev])) {
return true;
}
return $map[$message_sev] >= idx($map, $level, 0);
}
}
diff --git a/src/lint/engine/ArcanistLintEngine.php b/src/lint/engine/ArcanistLintEngine.php
index 8ea1ef6a..08d818c7 100644
--- a/src/lint/engine/ArcanistLintEngine.php
+++ b/src/lint/engine/ArcanistLintEngine.php
@@ -1,355 +1,339 @@
<?php
-/*
- * Copyright 2012 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
* @stable
*/
abstract class ArcanistLintEngine {
protected $workingCopy;
protected $paths = array();
protected $fileData = array();
protected $charToLine = array();
protected $lineToFirstChar = array();
private $results = array();
private $minimumSeverity = ArcanistLintSeverity::SEVERITY_DISABLED;
private $changedLines = array();
private $commitHookMode = false;
private $hookAPI;
private $enableAsyncLint = false;
private $postponedLinters = array();
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, $changed) {
if ($changed === null) {
$this->changedLines[$path] = null;
} else {
$this->changedLines[$path] = array_fill_keys($changed, true);
}
return $this;
}
public function getPathChangedLines($path) {
return idx($this->changedLines, $path);
}
public function setFileData($data) {
$this->fileData = $data + $this->fileData;
return $this;
}
public function setCommitHookMode($mode) {
$this->commitHookMode = $mode;
return $this;
}
public function setHookAPI(ArcanistHookAPI $hook_api) {
$this->hookAPI = $hook_api;
return $this;
}
public function getHookAPI() {
return $this->hookAPI;
}
public function setEnableAsyncLint($enable_async_lint) {
$this->enableAsyncLint = $enable_async_lint;
return $this;
}
public function getEnableAsyncLint() {
return $this->enableAsyncLint;
}
public function loadData($path) {
if (!isset($this->fileData[$path])) {
if ($this->getCommitHookMode()) {
$this->fileData[$path] = $this->getHookAPI()
->getCurrentFileData($path);
} else {
$disk_path = $this->getFilePathOnDisk($path);
$this->fileData[$path] = Filesystem::readFile($disk_path);
}
}
return $this->fileData[$path];
}
public function pathExists($path) {
if ($this->getCommitHookMode()) {
$file_data = $this->loadData($path);
return ($file_data !== 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.");
}
$exceptions = array();
foreach ($linters as $linter_name => $linter) {
try {
$linter->setEngine($this);
if (!$linter->canRun()) {
continue;
}
$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;
}
if (!$this->isRelevantMessage($message)) {
continue;
}
$result = $this->getResultForPath($message->getPath());
$result->addMessage($message);
}
} catch (Exception $ex) {
if (!is_string($linter_name)) {
$linter_name = get_class($linter);
}
$exceptions[$linter_name] = $ex;
}
}
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.
}
}
}
if ($exceptions) {
throw new PhutilAggregateException('Some linters failed:', $exceptions);
}
return $this->results;
}
public function getResults() {
return $this->results;
}
abstract protected function buildLinters();
private function isRelevantMessage($message) {
// 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). The list of $changed lines may be null, to indicate that the
// path is a directory or a binary file so we should not exclude
// warnings.
$changed = $this->getPathChangedLines($message->getPath());
if ($changed === null || $message->isError() || !$message->getLine()) {
return true;
}
$last_line = $message->getLine();
if ($message->getOriginalText()) {
$last_line += substr_count($message->getOriginalText(), "\n");
}
for ($l = $message->getLine(); $l <= $last_line; $l++) {
if (!empty($changed[$l])) {
return true;
}
}
return false;
}
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);
}
public function getPostponedLinters() {
return $this->postponedLinters;
}
public function setPostponedLinters(array $linters) {
$this->postponedLinters = $linters;
return $this;
}
}
diff --git a/src/lint/engine/ArcanistSingleLintEngine.php b/src/lint/engine/ArcanistSingleLintEngine.php
index d08e830a..18ced269 100644
--- a/src/lint/engine/ArcanistSingleLintEngine.php
+++ b/src/lint/engine/ArcanistSingleLintEngine.php
@@ -1,78 +1,62 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Run a single linter on every path unconditionally. This is a glue engine for
* linters like @{class:ArcanistScriptAndRegexLinter}, if you are averse to
* writing a phutil library. Your linter will receive every path, including
* paths which have been moved or deleted.
*
* Set which linter should be run by configuring `lint.engine.single.linter` in
* `.arcconfig` or user config.
*
* @group linter
*/
final class ArcanistSingleLintEngine extends ArcanistLintEngine {
public function buildLinters() {
$key = 'lint.engine.single.linter';
$linter_name = $this->getWorkingCopy()->getConfigFromAnySource($key);
if (!$linter_name) {
throw new ArcanistUsageException(
"You must configure '{$key}' with the name of a linter in order to ".
"use ArcanistSingleLintEngine.");
}
if (!class_exists($linter_name)) {
throw new ArcanistUsageException(
"Linter '{$linter_name}' configured in '{$key}' does not exist!");
}
if (!is_subclass_of($linter_name, 'ArcanistLinter')) {
throw new ArcanistUsageException(
"Linter '{$linter_name}' configured in '{$key}' MUST be a subclass of ".
"ArcanistLinter.");
}
// Filter the affected paths.
$paths = $this->getPaths();
foreach ($paths as $key => $path) {
if (!$this->pathExists($path)) {
// Don't lint removed files. In more complex linters it is sometimes
// appropriate to lint removed files so you can raise a warning like
// "you deleted X, but forgot to delete Y!", but most linters do not
// operate correctly on removed files.
unset($paths[$key]);
continue;
}
$disk = $this->getFilePathOnDisk($path);
if (is_dir($disk)) {
// Don't lint directories. (In SVN, they can be directly modified by
// changing properties on them, and may appear as modified paths.)
unset($paths[$key]);
continue;
}
}
$linter = newv($linter_name, array());
$linter->setPaths($paths);
return array($linter);
}
}
diff --git a/src/lint/engine/ComprehensiveLintEngine.php b/src/lint/engine/ComprehensiveLintEngine.php
index 56a79663..43d3efb1 100644
--- a/src/lint/engine/ComprehensiveLintEngine.php
+++ b/src/lint/engine/ComprehensiveLintEngine.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Basic lint engine which just applies several linters based on the file types
*
* @group linter
*/
final class ComprehensiveLintEngine extends ArcanistLintEngine {
public function buildLinters() {
$linters = array();
$paths = $this->getPaths();
foreach ($paths as $key => $path) {
if (!$this->pathExists($path)) {
unset($paths[$key]);
}
if (preg_match('@^externals/@', $path)) {
// Third-party stuff lives in /externals/; don't run lint engines
// against it.
unset($paths[$key]);
}
}
$text_paths = preg_grep('/\.(php|css|hpp|cpp|l|y)$/', $paths);
$linters[] = id(new ArcanistGeneratedLinter())->setPaths($text_paths);
$linters[] = id(new ArcanistNoLintLinter())->setPaths($text_paths);
$linters[] = id(new ArcanistTextLinter())->setPaths($text_paths);
$linters[] = id(new ArcanistFilenameLinter())->setPaths($paths);
$linters[] = id(new ArcanistXHPASTLinter())
->setPaths(preg_grep('/\.php$/', $paths));
$linters[] = id(new ArcanistApacheLicenseLinter())
->setPaths(preg_grep('/\.(php|cpp|hpp|l|y)$/', $paths));
$py_paths = preg_grep('/\.py$/', $paths);
$linters[] = id(new ArcanistPyFlakesLinter())->setPaths($py_paths);
$linters[] = id(new ArcanistPEP8Linter())->setPaths($py_paths);
$linters[] = id(new ArcanistRubyLinter())
->setPaths(preg_grep('/\.rb$/', $paths));
$linters[] = id(new ArcanistJSHintLinter())
->setPaths(preg_grep('/\.js$/', $paths));
return $linters;
}
}
diff --git a/src/lint/engine/ExampleLintEngine.php b/src/lint/engine/ExampleLintEngine.php
index 128ddec6..adb7ea0f 100644
--- a/src/lint/engine/ExampleLintEngine.php
+++ b/src/lint/engine/ExampleLintEngine.php
@@ -1,82 +1,66 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* This a simple example lint engine which just applies the
* @{class:ArcanistPyLintLinter} to any Python files. For a more complex
* example, see @{class:PhutilLintEngine}.
*
* @group linter
*/
final class ExampleLintEngine extends ArcanistLintEngine {
public function buildLinters() {
// This is a list of paths which the user wants to lint. Either they
// provided them explicitly, or arc figured them out from a commit or set
// of changes. The engine needs to return a list of ArcanistLinter objects,
// representing the linters which should be run on these files.
$paths = $this->getPaths();
// The ArcanistPyLintLinter runs "PyLint" (an open source python linter) on
// files you give it. There are several linters available by default like
// this one which you can use out of the box, or you can write your own.
// Linters are responsible for actually analyzing the contents of a file
// and raising warnings and errors.
$pylint_linter = new ArcanistPyLintLinter();
// Remove any paths that don't exist before we add paths to linters. We want
// to do this for linters that operate on file contents because the
// generated list of paths will include deleted paths when a file is
// removed.
foreach ($paths as $key => $path) {
if (!$this->pathExists($path)) {
unset($paths[$key]);
}
}
foreach ($paths as $path) {
if (!preg_match('/\.py$/', $path)) {
// This isn't a python file, so don't try to apply the PyLint linter
// to it.
continue;
}
if (preg_match('@^externals/@', $path)) {
// This is just an example of how to exclude a path so it doesn't get
// linted. If you put third-party code in an externals/ directory, you
// can just have your lint engine ignore it.
continue;
}
// Add the path, to tell the linter it should examine the source code
// to try to find problems.
$pylint_linter->addPath($path);
}
// We only built one linter, but you can build more than one (e.g., a
// Javascript linter for JS), and return a list of linters to execute. You
// can also add a path to more than one linter (for example, if you want
// to run a Python linter and a more general text linter on every .py file).
return array(
$pylint_linter,
);
}
}
diff --git a/src/lint/engine/PhutilLintEngine.php b/src/lint/engine/PhutilLintEngine.php
index fd9be1e0..360b6ca2 100644
--- a/src/lint/engine/PhutilLintEngine.php
+++ b/src/lint/engine/PhutilLintEngine.php
@@ -1,88 +1,69 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Lint engine which enforces libphutil rules.
*
* TODO: Deal with PhabricatorLintEngine extending this and then finalize it.
*
* @group linter
*/
class PhutilLintEngine extends ArcanistLintEngine {
public function buildLinters() {
$linters = array();
$paths = $this->getPaths();
$linters[] = id(new ArcanistPhutilLibraryLinter())->setPaths($paths);
// Remaining linters operate on file contents and ignore removed files.
foreach ($paths as $key => $path) {
if (!$this->pathExists($path)) {
unset($paths[$key]);
}
if (preg_match('@^externals/@', $path)) {
// Third-party stuff lives in /externals/; don't run lint engines
// against it.
unset($paths[$key]);
}
}
$linters[] = id(new ArcanistFilenameLinter())->setPaths($paths);
// Skip directories and lint only regular files in remaining linters.
foreach ($paths as $key => $path) {
if ($this->getCommitHookMode()) {
continue;
}
if (!is_file($this->getFilePathOnDisk($path))) {
unset($paths[$key]);
}
}
$text_paths = preg_grep('/\.(php|css|js|hpp|cpp|l|y)$/', $paths);
$linters[] = id(new ArcanistGeneratedLinter())->setPaths($text_paths);
$linters[] = id(new ArcanistNoLintLinter())->setPaths($text_paths);
$linters[] = id(new ArcanistTextLinter())->setPaths($text_paths);
$linters[] = id(new ArcanistSpellingLinter())->setPaths($text_paths);
$linters[] = id(new ArcanistXHPASTLinter())
->setCustomSeverityMap($this->getXHPASTSeverityMap())
->setPaths(preg_grep('/\.php$/', $paths));
- $linters[] = id(new ArcanistApacheLicenseLinter())
- ->setPaths(preg_grep('/\.(php|cpp|hpp|l|y)$/', $paths));
-
return $linters;
}
private function getXHPASTSeverityMap() {
$error = ArcanistLintSeverity::SEVERITY_ERROR;
$warning = ArcanistLintSeverity::SEVERITY_WARNING;
return array(
ArcanistXHPASTLinter::LINT_PHP_53_FEATURES => $error,
ArcanistXHPASTLinter::LINT_PHP_54_FEATURES => $error,
ArcanistXHPASTLinter::LINT_PHT_WITH_DYNAMIC_STRING => $error,
ArcanistXHPASTLinter::LINT_COMMENT_SPACING => $error,
ArcanistXHPASTLinter::LINT_RAGGED_CLASSTREE_EDGE => $warning,
);
}
}
diff --git a/src/lint/engine/UnitTestableArcanistLintEngine.php b/src/lint/engine/UnitTestableArcanistLintEngine.php
index cbb71bec..939111fa 100644
--- a/src/lint/engine/UnitTestableArcanistLintEngine.php
+++ b/src/lint/engine/UnitTestableArcanistLintEngine.php
@@ -1,43 +1,27 @@
<?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.
- */
-
/**
* Lint engine for use in constructing test cases. See
* @{class:ArcanistLinterTestCase}.
*
* @group testcase
*/
final class UnitTestableArcanistLintEngine extends ArcanistLintEngine {
protected $linters = array();
public function addLinter($linter) {
$this->linters[] = $linter;
return $this;
}
public function addFileData($path, $data) {
$this->fileData[$path] = $data;
return $this;
}
protected function buildLinters() {
return $this->linters;
}
}
diff --git a/src/lint/linter/ArcanistApacheLicenseLinter.php b/src/lint/linter/ArcanistApacheLicenseLinter.php
index ff0336da..f87ffe0a 100644
--- a/src/lint/linter/ArcanistApacheLicenseLinter.php
+++ b/src/lint/linter/ArcanistApacheLicenseLinter.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Adds the Apache license to source files.
*
* @group linter
*/
final class ArcanistApacheLicenseLinter extends ArcanistLicenseLinter {
public function getLinterName() {
return 'APACHELICENSE';
}
protected function getLicenseText($copyright_holder) {
$year = date('Y');
return <<<EOLICENSE
/*
* Copyright {$year} {$copyright_holder}
*
* 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.
*/
EOLICENSE;
}
protected function getLicensePatterns() {
$maybe_php_or_script = '(#![^\n]+?[\n])?(<[?]php\s+?)?';
return array(
"@^{$maybe_php_or_script}//[^\n]*Copyright[^\n]*[\n]\s*@i",
// We need to be careful about matching after "/*", since otherwise we'll
// end up in trouble on code like this, and consume the entire thing:
//
// /* a */
// copyright();
// /* b */
"@^{$maybe_php_or_script}/[*](?:[^*]|[*][^/])*?Copyright.*?[*]/\s*@is",
"@^{$maybe_php_or_script}\s*@",
);
}
}
diff --git a/src/lint/linter/ArcanistConduitLinter.php b/src/lint/linter/ArcanistConduitLinter.php
index 68c03132..c9e50cf9 100644
--- a/src/lint/linter/ArcanistConduitLinter.php
+++ b/src/lint/linter/ArcanistConduitLinter.php
@@ -1,110 +1,94 @@
<?php
-/*
- * Copyright 2012 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 linting via Conduit RPC call.
* Slow by definition, but allows sophisticated linting that relies on
* stuff like big indexes of a codebase.
* Recommended usage is to gate these to the advice lint level.
*
* The conduit endpoint should implement a method named the same as
* the value of ArcanistConduitLinter::CONDUIT_METHOD.
*
* It takes an array with a key 'file_contents' which is an array mapping
* file paths to their complete contents.
*
* It should return an array mapping those same paths to arrays describing the
* lint for each path.
*
* The lint for a path is described as a list of structured dictionaries.
*
* The dictionary structure is effectively defined by
* ArcanistLintMessage::newFromDictionary.
*
* Effective keys are:
* 'path' => must match passed in path.
* 'line'
* 'char'
* 'code'
* 'severity' => Must match a constant in ArcanistLintSeverity.
* 'name'
* 'description'
* 'original' & 'replacement' => optional patch information
*
* This class is intended for customization via instantiation, not via
* subclassing.
*/
final class ArcanistConduitLinter extends ArcanistLinter {
const CONDUIT_METHOD = 'lint.getalllint';
private $conduitURI;
private $linterName;
private $lintByPath; // array(/pa/th/ => <lint>), valid after willLintPaths().
public function __construct($conduit_uri, $linter_name) {
$this->conduitURI = $conduit_uri;
$this->linterName = $linter_name;
}
public function willLintPaths(array $paths) {
// Load all file path data into $this->data.
array_map(array($this, 'getData'), $paths);
$conduit = new ConduitClient($this->conduitURI);
$this->lintByPath = $conduit->callMethodSynchronous(
self::CONDUIT_METHOD,
array(
'file_contents' => $this->data,
)
);
}
public function lintPath($path) {
$lint_for_path = idx($this->lintByPath, $path);
if (!$lint_for_path) {
return;
}
foreach ($lint_for_path as $lint) {
$this->addLintMessage(ArcanistLintMessage::newFromDictionary($lint));
}
}
public function getLinterName() {
return $this->linterName;
}
public function getLintSeverityMap() {
// The rationale here is that this class will only be used for custom
// linting in installations. No two server endpoints will be the same across
// different instantiations. Therefore, the server can handle all severity
// customization directly.
throw new ArcanistUsageException(
'ArcanistConduitLinter does not support client-side severity '.
'customization.'
);
}
public function getLintNameMap() {
// See getLintSeverityMap for rationale.
throw new ArcanistUsageException(
'ArcanistConduitLinter does not support a name map.'
);
}
}
diff --git a/src/lint/linter/ArcanistFilenameLinter.php b/src/lint/linter/ArcanistFilenameLinter.php
index e1c2d2bb..b8a81d76 100644
--- a/src/lint/linter/ArcanistFilenameLinter.php
+++ b/src/lint/linter/ArcanistFilenameLinter.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Stifles creativity in choosing imaginative file names.
*
* @group linter
*/
final class ArcanistFilenameLinter extends ArcanistLinter {
const LINT_BAD_FILENAME = 1;
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'NAM';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
self::LINT_BAD_FILENAME => 'Bad Filename',
);
}
public function lintPath($path) {
if (!preg_match('@^[a-z0-9./\\\\_-]+$@i', $path)) {
$this->raiseLintAtPath(
self::LINT_BAD_FILENAME,
'Name files using only letters, numbers, period, hyphen and '.
'underscore.');
}
}
}
diff --git a/src/lint/linter/ArcanistGeneratedLinter.php b/src/lint/linter/ArcanistGeneratedLinter.php
index 84ee58be..25281d70 100644
--- a/src/lint/linter/ArcanistGeneratedLinter.php
+++ b/src/lint/linter/ArcanistGeneratedLinter.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Stops other linters from running on generated code.
*
* @group linter
*/
final class ArcanistGeneratedLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'GEN';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
);
}
public function lintPath($path) {
$data = $this->getData($path);
if (preg_match('/@'.'generated/', $data)) {
$this->stopAllLinters();
}
}
}
diff --git a/src/lint/linter/ArcanistJSHintLinter.php b/src/lint/linter/ArcanistJSHintLinter.php
index 8dfbc52f..e01e43cd 100644
--- a/src/lint/linter/ArcanistJSHintLinter.php
+++ b/src/lint/linter/ArcanistJSHintLinter.php
@@ -1,171 +1,155 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Uses "JSHint" to detect errors and potential problems in JavaScript code.
* To use this linter, you must install jshint through NPM (Node Package
* Manager). You can configure different JSHint options on a per-file basis.
*
* If you have NodeJS installed you should be able to install jshint with
* ##npm install jshint -g## (don't forget the -g flag or NPM will install
* the package locally). If your system is unusual, you can manually specify
* the location of jshint and its dependencies by configuring these keys in
* your .arcconfig:
*
* lint.jshint.prefix
* lint.jshint.bin
*
* If you want to configure custom options for your project, create a JSON
* file with these options and add the path to the file to your .arcconfig
* by configuring this key:
*
* lint.jshint.config
*
* Example JSON file (config.json):
*
* {
* "predef": [ // Custom globals
* "myGlobalVariable",
* "anotherGlobalVariable"
* ],
*
* "es5": true, // Allow ES5 syntax
* "strict": true // Require strict mode
* }
*
* For more options see http://www.jshint.com/options/.
*
* @group linter
*/
final class ArcanistJSHintLinter extends ArcanistLinter {
const JSHINT_ERROR = 1;
public function getLinterName() {
return 'JSHint';
}
public function getLintSeverityMap() {
return array(
self::JSHINT_ERROR => ArcanistLintSeverity::SEVERITY_ERROR
);
}
public function getLintNameMap() {
return array(
self::JSHINT_ERROR => "JSHint Error"
);
}
public function getJSHintOptions() {
$working_copy = $this->getEngine()->getWorkingCopy();
$options = '--reporter '.dirname(realpath(__FILE__)).'/reporter.js';
$config = $working_copy->getConfig('lint.jshint.config');
if ($config !== null) {
$config = Filesystem::resolvePath($config, $working_copy->getProjectRoot());
if (!Filesystem::pathExists($config)) {
throw new ArcanistUsageException(
"Unable to find custom options file defined by 'lint.jshint.config'. ".
"Make sure that the path is correct.");
}
$options .= ' --config '.$config;
}
return $options;
}
private function getJSHintPath() {
$working_copy = $this->getEngine()->getWorkingCopy();
$prefix = $working_copy->getConfig('lint.jshint.prefix');
$bin = $working_copy->getConfig('lint.jshint.bin');
if ($bin === null) {
$bin = "jshint";
}
if ($prefix !== null) {
$bin = $prefix."/".$bin;
if (!Filesystem::pathExists($bin)) {
throw new ArcanistUsageException(
"Unable to find JSHint binary in a specified directory. Make sure ".
"that 'lint.jshint.prefix' and 'lint.jshint.bin' keys are set ".
"correctly. If you'd rather use a copy of JSHint installed globally, ".
"you can just remove these keys from your .arcconfig");
}
return $bin;
}
// Look for globally installed JSHint
$cmd = (phutil_is_windows()) ? 'where %s' : 'which %s';
list($err) = exec_manual($cmd, $bin);
if ($err) {
throw new ArcanistUsageException(
"JSHint does not appear to be installed on this system. Install it ".
"(e.g., with 'npm install jshint -g') or configure ".
"'lint.jshint.prefix' in your .arcconfig to point to the directory ".
"where it resides.");
}
return $bin;
}
public function willLintPaths(array $paths) {
$jshint_bin = $this->getJSHintPath();
$jshint_options = $this->getJSHintOptions();
$futures = array();
foreach ($paths as $path) {
$filepath = $this->getEngine()->getFilePathOnDisk($path);
$futures[$path] = new ExecFuture("{$jshint_bin} {$filepath} ${jshint_options}");
}
foreach (Futures($futures)->limit(8) as $path => $future) {
$this->results[$path] = $future->resolve();
}
}
public function lintPath($path) {
list($rc, $stdout, $stderr) = $this->results[$path];
if ($rc === 0) {
return;
}
$errors = json_decode($stdout);
if (!is_array($errors)) {
// Something went wrong and we can't decode the output. Exit abnormally.
throw new ArcanistUsageException(
"JSHint returned unparseable output.\n".
"stdout:\n\n{$stdout}".
"stderr:\n\n{$stderr}");
}
foreach ($errors as $err) {
$this->raiseLintAtLine(
$err->line,
$err->col,
self::JSHINT_ERROR,
$err->reason);
}
}
}
diff --git a/src/lint/linter/ArcanistLicenseLinter.php b/src/lint/linter/ArcanistLicenseLinter.php
index 819cb731..024c88fa 100644
--- a/src/lint/linter/ArcanistLicenseLinter.php
+++ b/src/lint/linter/ArcanistLicenseLinter.php
@@ -1,84 +1,68 @@
<?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.
- */
-
/**
* Adds a license or copyright header to source files.
*
* @group linter
*/
abstract class ArcanistLicenseLinter extends ArcanistLinter {
const LINT_NO_LICENSE_HEADER = 1;
public function willLintPaths(array $paths) {
return;
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
self::LINT_NO_LICENSE_HEADER => 'No License Header',
);
}
/**
* Given the name of the copyright holder, return appropriate license header
* text.
*/
abstract protected function getLicenseText($copyright_holder);
/**
* Return an array of regular expressions that, if matched, indicate
* that a copyright header is required. The appropriate match will be
* stripped from the input when comparing against the expected license.
*/
abstract protected function getLicensePatterns();
public function lintPath($path) {
$working_copy = $this->getEngine()->getWorkingCopy();
$copyright_holder = $working_copy->getConfig('copyright_holder');
if (!$copyright_holder) {
return;
}
$patterns = $this->getLicensePatterns();
$license = $this->getLicenseText($copyright_holder);
$data = $this->getData($path);
$matches = 0;
foreach ($patterns as $pattern) {
if (preg_match($pattern, $data, $matches)) {
$expect = rtrim(implode('', array_slice($matches, 1)))."\n".$license;
if (trim($matches[0]) != trim($expect)) {
$this->raiseLintAtOffset(
0,
self::LINT_NO_LICENSE_HEADER,
'This file has a missing or out of date license header.',
$matches[0],
ltrim($expect));
}
break;
}
}
}
}
diff --git a/src/lint/linter/ArcanistLinter.php b/src/lint/linter/ArcanistLinter.php
index 662c897c..cf3da352 100644
--- a/src/lint/linter/ArcanistLinter.php
+++ b/src/lint/linter/ArcanistLinter.php
@@ -1,216 +1,200 @@
<?php
-/*
- * Copyright 2012 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 lint rules, like syntax checks for a specific language.
*
* @group linter
* @stable
*/
abstract class ArcanistLinter {
protected $paths = array();
protected $data = array();
protected $engine;
protected $activePath;
protected $messages = array();
protected $stopAllLinters = false;
private $customSeverityMap = array();
public function setCustomSeverityMap(array $map) {
$this->customSeverityMap = $map;
return $this;
}
public function getActivePath() {
return $this->activePath;
}
public function stopAllLinters() {
$this->stopAllLinters = true;
return $this;
}
public function didStopAllLinters() {
return $this->stopAllLinters;
}
public function addPath($path) {
$this->paths[$path] = $path;
return $this;
}
public function setPaths(array $paths) {
$this->paths = $paths;
return $this;
}
public function getPaths() {
return array_values($this->paths);
}
public function addData($path, $data) {
$this->data[$path] = $data;
return $this;
}
protected function getData($path) {
if (!array_key_exists($path, $this->data)) {
$this->data[$path] = $this->getEngine()->loadData($path);
}
return $this->data[$path];
}
public function setEngine(ArcanistLintEngine $engine) {
$this->engine = $engine;
return $this;
}
protected function getEngine() {
return $this->engine;
}
public function getLintMessageFullCode($short_code) {
return $this->getLinterName().$short_code;
}
public function getLintMessageSeverity($code) {
$map = $this->customSeverityMap;
if (isset($map[$code])) {
return $map[$code];
}
$map = $this->getLintSeverityMap();
if (isset($map[$code])) {
return $map[$code];
}
return ArcanistLintSeverity::SEVERITY_ERROR;
}
public function isMessageEnabled($code) {
return ($this->getLintMessageSeverity($code) !==
ArcanistLintSeverity::SEVERITY_DISABLED);
}
public function getLintMessageName($code) {
$map = $this->getLintNameMap();
if (isset($map[$code])) {
return $map[$code];
}
return "Unknown lint message!";
}
protected function addLintMessage(ArcanistLintMessage $message) {
if (!$this->getEngine()->getCommitHookMode()) {
$root = $this->getEngine()->getWorkingCopy()->getProjectRoot();
$path = Filesystem::resolvePath($message->getPath(), $root);
$message->setPath(Filesystem::readablePath($path, $root));
}
$this->messages[] = $message;
return $message;
}
public function getLintMessages() {
return $this->messages;
}
protected function raiseLintAtLine(
$line,
$char,
$code,
$desc,
$original = null,
$replacement = null) {
$dict = array(
'path' => $this->getActivePath(),
'line' => $line,
'char' => $char,
'code' => $this->getLintMessageFullCode($code),
'severity' => $this->getLintMessageSeverity($code),
'name' => $this->getLintMessageName($code),
'description' => $desc,
);
if ($original !== null) {
$dict['original'] = $original;
}
if ($replacement !== null) {
$dict['replacement'] = $replacement;
}
return $this->addLintMessage(ArcanistLintMessage::newFromDictionary($dict));
}
protected function raiseLintAtPath(
$code,
$desc) {
return $this->raiseLintAtLine(null, null, $code, $desc, null, null);
}
protected function raiseLintAtOffset(
$offset,
$code,
$desc,
$original = null,
$replacement = null) {
$path = $this->getActivePath();
$engine = $this->getEngine();
if ($offset === null) {
$line = null;
$char = null;
} else {
list($line, $char) = $engine->getLineAndCharFromOffset($path, $offset);
}
return $this->raiseLintAtLine(
$line + 1,
$char + 1,
$code,
$desc,
$original,
$replacement);
}
public function willLintPath($path) {
$this->stopAllLinters = false;
$this->activePath = $path;
}
public function canRun() {
return true;
}
abstract public function willLintPaths(array $paths);
abstract public function lintPath($path);
abstract public function getLinterName();
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array();
}
}
diff --git a/src/lint/linter/ArcanistNoLintLinter.php b/src/lint/linter/ArcanistNoLintLinter.php
index a6c4b6e4..c71a30e4 100644
--- a/src/lint/linter/ArcanistNoLintLinter.php
+++ b/src/lint/linter/ArcanistNoLintLinter.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Stops other linters from running on code marked with
* a nolint annotation.
*
* @group linter
*/
final class ArcanistNoLintLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'NOLINT';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
);
}
public function lintPath($path) {
$data = $this->getData($path);
if (preg_match('/@'.'nolint/', $data)) {
$this->stopAllLinters();
}
}
}
diff --git a/src/lint/linter/ArcanistPEP8Linter.php b/src/lint/linter/ArcanistPEP8Linter.php
index b9778de4..c2426802 100644
--- a/src/lint/linter/ArcanistPEP8Linter.php
+++ b/src/lint/linter/ArcanistPEP8Linter.php
@@ -1,137 +1,121 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Uses "pep8.py" to enforce PEP8 rules for Python.
*
* @group linter
*/
final class ArcanistPEP8Linter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'PEP8';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array();
}
public function getPEP8Options() {
$working_copy = $this->getEngine()->getWorkingCopy();
$options = $working_copy->getConfig('lint.pep8.options');
if ($options === null) {
// W293 (blank line contains whitespace) is redundant when used
// alongside TXT6, causing pain to python programmers.
return '--ignore=W293';
}
return $options;
}
public function getPEP8Path() {
$working_copy = $this->getEngine()->getWorkingCopy();
$prefix = $working_copy->getConfig('lint.pep8.prefix');
$bin = $working_copy->getConfig('lint.pep8.bin');
if ($bin === null && $prefix === null) {
$bin = csprintf('/usr/bin/env python2.6 %s',
phutil_get_library_root('arcanist').
'/../externals/pep8/pep8.py');
}
else {
if ($bin === null) {
$bin = 'pep8';
}
if ($prefix !== null) {
if (!Filesystem::pathExists($prefix.'/'.$bin)) {
throw new ArcanistUsageException(
"Unable to find PEP8 binary in a specified directory. Make sure ".
"that 'lint.pep8.prefix' and 'lint.pep8.bin' keys are set ".
"correctly. If you'd rather use a copy of PEP8 installed ".
"globally, you can just remove these keys from your .arcconfig");
}
$bin = csprintf("%s/%s", $prefix, $bin);
return $bin;
}
// Look for globally installed PEP8
list($err) = exec_manual('which %s', $bin);
if ($err) {
throw new ArcanistUsageException(
"PEP8 does not appear to be installed on this system. Install it ".
"(e.g., with 'easy_install pep8') or configure ".
"'lint.pep8.prefix' in your .arcconfig to point to the directory ".
"where it resides.");
}
}
return $bin;
}
public function lintPath($path) {
$pep8_bin = $this->getPEP8Path();
$options = $this->getPEP8Options();
list($rc, $stdout) = exec_manual(
"%C %C %s",
$pep8_bin,
$options,
$this->getEngine()->getFilePathOnDisk($path));
$lines = explode("\n", $stdout);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match('/^(.*?):(\d+):(\d+): (\S+) (.*)$/', $line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
$message->setChar($matches[3]);
$message->setCode($matches[4]);
$message->setName('PEP8 '.$matches[4]);
$message->setDescription($matches[5]);
if (!$this->isMessageEnabled($matches[4])) {
continue;
}
if ($matches[4][0] == 'E') {
$message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR);
} else {
$message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING);
}
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistPhpcsLinter.php b/src/lint/linter/ArcanistPhpcsLinter.php
index ebc3ea72..0213b565 100644
--- a/src/lint/linter/ArcanistPhpcsLinter.php
+++ b/src/lint/linter/ArcanistPhpcsLinter.php
@@ -1,138 +1,122 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Uses "PHP_CodeSniffer" to detect checkstyle errors in php code.
* To use this linter, you must install PHP_CodeSniffer.
* http://pear.php.net/package/PHP_CodeSniffer.
*
* Optional configurations in .arcconfig:
*
* lint.phpcs.standard
* lint.phpcs.options
* lint.phpcs.bin
*
* @group linter
*/
final class ArcanistPhpcsLinter extends ArcanistLinter {
private $reports;
public function getLinterName() {
return 'PHPCS';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array();
}
public function getPhpcsOptions() {
$working_copy = $this->getEngine()->getWorkingCopy();
$options = $working_copy->getConfig('lint.phpcs.options');
$standard = $working_copy->getConfig('lint.phpcs.standard');
$options .= !empty($standard) ? ' --standard=' . $standard : '';
return $options;
}
private function getPhpcsPath() {
$working_copy = $this->getEngine()->getWorkingCopy();
$bin = $working_copy->getConfig('lint.phpcs.bin');
if ($bin === null) {
$bin = 'phpcs';
}
return $bin;
}
public function willLintPaths(array $paths) {
$phpcs_bin = $this->getPhpcsPath();
$phpcs_options = $this->getPhpcsOptions();
$futures = array();
foreach ($paths as $path) {
$filepath = $this->getEngine()->getFilePathOnDisk($path);
$this->reports[$path] = new TempFile();
$futures[$path] = new ExecFuture('%C %C --report=xml --report-file=%s %s',
$phpcs_bin,
$phpcs_options,
$this->reports[$path],
$filepath);
}
foreach (Futures($futures)->limit(8) as $path => $future) {
$this->results[$path] = $future->resolve();
}
libxml_use_internal_errors(true);
}
public function lintPath($path) {
list($rc, $stdout) = $this->results[$path];
$report = Filesystem::readFile($this->reports[$path]);
if ($report) {
$report_dom = new DOMDocument();
libxml_clear_errors();
$report_dom->loadXML($report);
}
if (!$report || libxml_get_errors()) {
throw new ArcanistUsageException('PHPCS Linter failed to load ' .
'reporting file. Something happened when running phpcs. ' .
"Output:\n$stdout" .
"\nTry running lint with --trace flag to get more details.");
}
$files = $report_dom->getElementsByTagName('file');
foreach ($files as $file) {
foreach ($file->childNodes as $child) {
if (!($child instanceof DOMElement)) {
continue;
}
$data = $this->getData($path);
$lines = explode("\n", $data);
$line = $lines[$child->getAttribute('line') - 1];
$text = substr($line, $child->getAttribute('column') - 1);
$name = $this->getLinterName() . ' - ' . $child->getAttribute('source');
$severity = $child->tagName == 'error' ?
ArcanistLintSeverity::SEVERITY_ERROR
: ArcanistLintSeverity::SEVERITY_WARNING;
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($child->getAttribute('line'));
$message->setChar($child->getAttribute('column'));
$message->setCode($child->getAttribute('severity'));
$message->setName($name);
$message->setDescription($child->nodeValue);
$message->setSeverity($severity);
$message->setOriginalText($text);
$this->addLintMessage($message);
}
}
}
}
diff --git a/src/lint/linter/ArcanistPhutilLibraryLinter.php b/src/lint/linter/ArcanistPhutilLibraryLinter.php
index 028eedb4..c7a31e52 100644
--- a/src/lint/linter/ArcanistPhutilLibraryLinter.php
+++ b/src/lint/linter/ArcanistPhutilLibraryLinter.php
@@ -1,190 +1,174 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Applies lint rules for Phutil libraries. We enforce three rules:
*
* # If you use a symbol, it must be defined somewhere.
* # If you define a symbol, it must not duplicate another definition.
* # If you define a class or interface in a file, it MUST be the only symbol
* defined in that file.
*
* @group linter
*/
final class ArcanistPhutilLibraryLinter extends ArcanistLinter {
const LINT_UNKNOWN_SYMBOL = 1;
const LINT_DUPLICATE_SYMBOL = 2;
const LINT_ONE_CLASS_PER_FILE = 3;
public function getLintNameMap() {
return array(
self::LINT_UNKNOWN_SYMBOL => 'Unknown Symbol',
self::LINT_DUPLICATE_SYMBOL => 'Duplicate Symbol',
self::LINT_ONE_CLASS_PER_FILE => 'One Class Per File',
);
}
public function getLinterName() {
return 'PHL';
}
public function getLintSeverityMap() {
return array();
}
public function willLintPaths(array $paths) {
if (!xhpast_is_available()) {
throw new Exception(xhpast_get_build_instructions());
}
// NOTE: For now, we completely ignore paths and just lint every library in
// its entirety. This is simpler and relatively fast because we don't do any
// detailed checks and all the data we need for this comes out of module
// caches.
$bootloader = PhutilBootloader::getInstance();
$libs = $bootloader->getAllLibraries();
// Load the up-to-date map for each library, without loading the library
// itself. This means lint results will accurately reflect the state of
// the working copy.
$arc_root = dirname(phutil_get_library_root('arcanist'));
$bin = "{$arc_root}/scripts/phutil_rebuild_map.php";
$symbols = array();
foreach ($libs as $lib) {
// Do these one at a time since they individually fanout to saturate
// available system resources.
$future = new ExecFuture(
'%s --show --quiet --ugly -- %s',
$bin,
phutil_get_library_root($lib));
$symbols[$lib] = $future->resolveJSON();
}
$all_symbols = array();
foreach ($symbols as $library => $map) {
// Check for files which declare more than one class/interface in the same
// file, or mix function definitions with class/interface definitions. We
// must isolate autoloadable symbols to one per file so the autoloader
// can't end up in an unresolvable cycle.
foreach ($map as $file => $spec) {
$have = idx($spec, 'have', array());
$have_classes =
idx($have, 'class', array()) +
idx($have, 'interface', array());
$have_functions = idx($have, 'function');
if ($have_functions && $have_classes) {
$function_list = implode(', ', array_keys($have_functions));
$class_list = implode(', ', array_keys($have_classes));
$this->raiseLintInLibrary(
$library,
$file,
end($have_functions),
self::LINT_ONE_CLASS_PER_FILE,
"File '{$file}' mixes function ({$function_list}) and ".
"class/interface ({$class_list}) definitions in the same file. ".
"A file which declares a class or an interface MUST ".
"declare nothing else.");
} else if (count($have_classes) > 1) {
$class_list = implode(', ', array_keys($have_classes));
$this->raiseLintInLibrary(
$library,
$file,
end($have_classes),
self::LINT_ONE_CLASS_PER_FILE,
"File '{$file}' declares more than one class or interface ".
"({$class_list}). A file which declares a class or interface MUST ".
"declare nothing else.");
}
}
// Check for duplicate symbols: two files providing the same class or
// function.
foreach ($map as $file => $spec) {
$have = idx($spec, 'have', array());
foreach (array('class', 'function', 'interface') as $type) {
$libtype = ($type == 'interface') ? 'class' : $type;
foreach (idx($have, $type, array()) as $symbol => $offset) {
if (empty($all_symbols[$libtype][$symbol])) {
$all_symbols[$libtype][$symbol] = array(
'library' => $library,
'file' => $file,
'offset' => $offset,
);
continue;
}
$osrc = $all_symbols[$libtype][$symbol]['file'];
$olib = $all_symbols[$libtype][$symbol]['library'];
$this->raiseLintInLibrary(
$library,
$file,
$offset,
self::LINT_DUPLICATE_SYMBOL,
"Definition of {$type} '{$symbol}' in '{$file}' in library ".
"'{$library}' duplicates prior definition in '{$osrc}' in ".
"library '{$olib}'.");
}
}
}
}
foreach ($symbols as $library => $map) {
// Check for unknown symbols: uses of classes, functions or interfaces
// which are not defined anywhere. We reference the list of all symbols
// we built up earlier.
foreach ($map as $file => $spec) {
$need = idx($spec, 'need', array());
foreach (array('class', 'function', 'interface') as $type) {
$libtype = ($type == 'interface') ? 'class' : $type;
foreach (idx($need, $type, array()) as $symbol => $offset) {
if (!empty($all_symbols[$libtype][$symbol])) {
// Symbol is defined somewhere.
continue;
}
$this->raiseLintInLibrary(
$library,
$file,
$offset,
self::LINT_UNKNOWN_SYMBOL,
"Use of unknown {$type} '{$symbol}'. This symbol is not defined ".
"in any loaded phutil library.");
}
}
}
}
}
private function raiseLintInLibrary($library, $path, $offset, $code, $desc) {
$root = phutil_get_library_root($library);
$this->activePath = $root.'/'.$path;
$this->raiseLintAtOffset($offset, $code, $desc);
}
public function lintPath($path) {
return;
}
}
diff --git a/src/lint/linter/ArcanistPyFlakesLinter.php b/src/lint/linter/ArcanistPyFlakesLinter.php
index 1cb79886..87054330 100644
--- a/src/lint/linter/ArcanistPyFlakesLinter.php
+++ b/src/lint/linter/ArcanistPyFlakesLinter.php
@@ -1,120 +1,104 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Uses "PyFlakes" to detect various errors in Python code.
*
* @group linter
*/
final class ArcanistPyFlakesLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'PyFlakes';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
);
}
public function getPyFlakesOptions() {
return null;
}
public function lintPath($path) {
$working_copy = $this->getEngine()->getWorkingCopy();
$pyflakes_path = $working_copy->getConfig('lint.pyflakes.path');
$pyflakes_prefix = $working_copy->getConfig('lint.pyflakes.prefix');
// Default to just finding pyflakes in the users path
$pyflakes_bin = 'pyflakes';
$python_path = '';
// If a pyflakes path was specified, then just use that as the
// pyflakes binary and assume that the libraries will be imported
// correctly.
//
// If no pyflakes path was specified and a pyflakes prefix was
// specified, then use the binary from this prefix and add it to
// the PYTHONPATH environment variable so that the libs are imported
// correctly. This is useful when pyflakes is installed into a
// non-default location.
if ($pyflakes_path !== null) {
$pyflakes_bin = $pyflakes_path;
} else if ($pyflakes_prefix !== null) {
$pyflakes_bin = $pyflakes_prefix.'/bin/pyflakes';
$python_path = $pyflakes_prefix.'/lib/python2.6/site-packages:';
}
$options = $this->getPyFlakesOptions();
$f = new ExecFuture(
"/usr/bin/env PYTHONPATH=%s\$PYTHONPATH ".
"{$pyflakes_bin} {$options}", $python_path);
$f->write($this->getData($path));
try {
list($stdout, $_) = $f->resolvex();
} catch (CommandException $e) {
// PyFlakes will return an exit code of 1 if warnings/errors
// are found but print nothing to stderr in this case. Therefore,
// if we see any output on stderr or a return code other than 1 or 0,
// pyflakes failed.
if ($e->getError() !== 1 || $e->getStderr() !== '') {
throw $e;
} else {
$stdout = $e->getStdout();
}
}
$lines = explode("\n", $stdout);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match('/^(.*?):(\d+): (.*)$/', $line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$severity = ArcanistLintSeverity::SEVERITY_WARNING;
$description = $matches[3];
if (preg_match('/(^undefined|^duplicate|before assignment$)/', $description)) {
$severity = ArcanistLintSeverity::SEVERITY_ERROR;
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
$message->setCode($this->getLinterName());
$message->setDescription($description);
$message->setSeverity($severity);
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistPyLintLinter.php b/src/lint/linter/ArcanistPyLintLinter.php
index 0a01c64e..618014ad 100644
--- a/src/lint/linter/ArcanistPyLintLinter.php
+++ b/src/lint/linter/ArcanistPyLintLinter.php
@@ -1,261 +1,245 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Uses "PyLint" to detect various errors in Python code. To use this linter,
* you must install pylint and configure which codes you want to be reported as
* errors, warnings and advice.
*
* You should be able to install pylint with ##sudo easy_install pylint##. If
* your system is unusual, you can manually specify the location of pylint and
* its dependencies by configuring these keys in your .arcconfig:
*
* lint.pylint.prefix
* lint.pylint.logilab_astng.prefix
* lint.pylint.logilab_common.prefix
*
* You can specify additional command-line options to pass to PyLint by
* setting ##lint.pylint.options##. You may also specify a list of additional
* entries for PYTHONPATH with ##lint.pylint.pythonpath##. Those can be
* absolute or relative to the project root.
*
* If you have a PyLint rcfile, specify its path with
* ##lint.pylint.rcfile##. It can be absolute or relative to the project
* root. Be sure not to define ##output-format##, or if you do, set it to
* ##text##.
*
* Specify which PyLint messages map to which Arcanist messages by defining
* the following regular expressions:
*
* lint.pylint.codes.error
* lint.pylint.codes.warning
* lint.pylint.codes.advice
*
* The regexps are run in that order; the first to match determines which
* Arcanist severity applies, if any. For example, to capture all PyLint
* "E...." errors as Arcanist errors, set ##lint.pylint.codes.error## to:
*
* ^E.*
*
* You can also match more granularly:
*
* ^E(0001|0002)$
*
* According to ##man pylint##, there are 5 kind of messages:
*
* (C) convention, for programming standard violation
* (R) refactor, for bad code smell
* (W) warning, for python specific problems
* (E) error, for probable bugs in the code
* (F) fatal, if an error occurred which prevented pylint from
* doing further processing.
*
* @group linter
*/
final class ArcanistPyLintLinter extends ArcanistLinter {
private function getMessageCodeSeverity($code) {
$working_copy = $this->getEngine()->getWorkingCopy();
$error_regexp = $working_copy->getConfig('lint.pylint.codes.error');
$warning_regexp = $working_copy->getConfig('lint.pylint.codes.warning');
$advice_regexp = $working_copy->getConfig('lint.pylint.codes.advice');
if (!$error_regexp && !$warning_regexp && !$advice_regexp) {
throw new ArcanistUsageException(
"You are invoking the PyLint linter but have not configured any of ".
"'lint.pylint.codes.error', 'lint.pylint.codes.warning', or ".
"'lint.pylint.codes.advice'. Consult the documentation for ".
"ArcanistPyLintLinter.");
}
$code_map = array(
ArcanistLintSeverity::SEVERITY_ERROR => $error_regexp,
ArcanistLintSeverity::SEVERITY_WARNING => $warning_regexp,
ArcanistLintSeverity::SEVERITY_ADVICE => $advice_regexp,
);
foreach ($code_map as $sev => $codes) {
if ($codes === null) {
continue;
}
if (!is_array($codes)) {
$codes = array($codes);
}
foreach ($codes as $code_re) {
if (preg_match("/{$code_re}/", $code)) {
return $sev;
}
}
}
// If the message code doesn't match any of the provided regex's,
// then just disable it.
return ArcanistLintSeverity::SEVERITY_DISABLED;
}
private function getPyLintPath() {
$pylint_bin = "pylint";
// Use the PyLint prefix specified in the config file
$working_copy = $this->getEngine()->getWorkingCopy();
$prefix = $working_copy->getConfig('lint.pylint.prefix');
if ($prefix !== null) {
$pylint_bin = $prefix."/bin/".$pylint_bin;
}
if (!Filesystem::pathExists($pylint_bin)) {
list($err) = exec_manual('which %s', $pylint_bin);
if ($err) {
throw new ArcanistUsageException(
"PyLint does not appear to be installed on this system. Install it ".
"(e.g., with 'sudo easy_install pylint') or configure ".
"'lint.pylint.prefix' in your .arcconfig to point to the directory ".
"where it resides.");
}
}
return $pylint_bin;
}
private function getPyLintPythonPath() {
// Get non-default install locations for pylint and its dependencies
// libraries.
$working_copy = $this->getEngine()->getWorkingCopy();
$prefixes = array(
$working_copy->getConfig('lint.pylint.prefix'),
$working_copy->getConfig('lint.pylint.logilab_astng.prefix'),
$working_copy->getConfig('lint.pylint.logilab_common.prefix'),
);
// Add the libraries to the python search path
$python_path = array();
foreach ($prefixes as $prefix) {
if ($prefix !== null) {
$python_path[] = $prefix.'/lib/python2.6/site-packages';
}
}
$config_paths = $working_copy->getConfig('lint.pylint.pythonpath');
if ($config_paths !== null) {
foreach ($config_paths as $config_path) {
if ($config_path !== null) {
$python_path[] =
Filesystem::resolvePath($config_path,
$working_copy->getProjectRoot());
}
}
}
$python_path[] = '';
return implode(":", $python_path);
}
private function getPyLintOptions() {
// '-rn': don't print lint report/summary at end
// '-iy': show message codes for lint warnings/errors
$options = array('-rn', '-iy');
$working_copy = $this->getEngine()->getWorkingCopy();
// Specify an --rcfile, either absolute or relative to the project root.
// Stupidly, the command line args above are overridden by rcfile, so be
// careful.
$rcfile = $working_copy->getConfig('lint.pylint.rcfile');
if ($rcfile !== null) {
$rcfile = Filesystem::resolvePath(
$rcfile,
$working_copy->getProjectRoot());
$options[] = csprintf('--rcfile=%s', $rcfile);
}
// Add any options defined in the config file for PyLint
$config_options = $working_copy->getConfig('lint.pylint.options');
if ($config_options !== null) {
$options = array_merge($options, $config_options);
}
return implode(" ", $options);
}
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'PyLint';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array();
}
public function lintPath($path) {
$pylint_bin = $this->getPyLintPath();
$python_path = $this->getPyLintPythonPath();
$options = $this->getPyLintOptions();
$path_on_disk = $this->getEngine()->getFilePathOnDisk($path);
try {
list($stdout, $_) = execx(
"/usr/bin/env PYTHONPATH=%s\$PYTHONPATH ".
"{$pylint_bin} {$options} {$path_on_disk}",
$python_path);
} catch (CommandException $e) {
if ($e->getError() == 32) {
// According to ##man pylint## the exit status of 32 means there was a
// usage error. That's bad, so actually exit abnormally.
throw $e;
} else {
// The other non-zero exit codes mean there were messages issued,
// which is expected, so don't exit.
$stdout = $e->getStdout();
}
}
$lines = explode("\n", $stdout);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match(
'/([A-Z]\d+): *(\d+)(?:|,\d*): *(.*)$/',
$line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
$message->setCode($matches[1]);
$message->setName($this->getLinterName()." ".$matches[1]);
$message->setDescription($matches[3]);
$message->setSeverity($this->getMessageCodeSeverity($matches[1]));
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistRubyLinter.php b/src/lint/linter/ArcanistRubyLinter.php
index df5ca138..e6e1e7d5 100644
--- a/src/lint/linter/ArcanistRubyLinter.php
+++ b/src/lint/linter/ArcanistRubyLinter.php
@@ -1,102 +1,86 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Uses "Ruby" to detect various errors in Ruby code.
*
* @group linter
*/
final class ArcanistRubyLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'Ruby';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array();
}
private function getRubyPath() {
$ruby_bin = "ruby";
// Use the Ruby prefix specified in the config file
$working_copy = $this->getEngine()->getWorkingCopy();
$prefix = $working_copy->getConfig('lint.ruby.prefix');
if ($prefix !== null) {
$ruby_bin = $prefix . $ruby_bin;
}
if (!Filesystem::pathExists($ruby_bin)) {
list($err) = exec_manual('which %s', $ruby_bin);
if ($err) {
throw new ArcanistUsageException(
"Ruby does not appear to be installed on this system. Install it or ".
"add 'lint.ruby.prefix' in your .arcconfig to point to ".
"the directory where it resides.");
}
}
return $ruby_bin;
}
private function getMessageCodeSeverity($code) {
return ArcanistLintSeverity::SEVERITY_ERROR;
}
public function lintPath($path) {
$rubyp = $this->getRubyPath();
$f = new ExecFuture("%s -wc", $rubyp);
$f->write($this->getData($path));
list($err, $stdout, $stderr) = $f->resolve();
if ($err === 0 ) {
return;
}
$lines = explode("\n", $stderr);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match("/(.*?):(\d+): (.*?)$/", $line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$code = head(explode(',', $matches[3]));
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
$message->setName($this->getLinterName() . " " . $code);
$message->setDescription($matches[3]);
$message->setSeverity($this->getMessageCodeSeverity($code));
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistScriptAndRegexLinter.php b/src/lint/linter/ArcanistScriptAndRegexLinter.php
index da4891fd..662697e8 100644
--- a/src/lint/linter/ArcanistScriptAndRegexLinter.php
+++ b/src/lint/linter/ArcanistScriptAndRegexLinter.php
@@ -1,416 +1,400 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Simple glue linter which runs some script on each path, and then uses a
* regex to parse lint messages from the script's output. (This linter uses a
* script and a regex to interpret the results of some real linter, it does
* not itself lint both scripts and regexes).
*
* Configure this linter by setting these keys in your configuration:
*
* - `linter.scriptandregex.script` Script command to run. This can be
* the path to a linter script, but may also include flags or use shell
* features (see below for examples).
* - `linter.scriptandregex.regex` The regex to process output with. This
* regex uses named capturing groups (detailed below) to interpret output.
*
* The script will be invoked from the project root, so you can specify a
* relative path like `scripts/lint.sh` or an absolute path like
* `/opt/lint/lint.sh`.
*
* This linter is necessarily more limited in its capabilities than a normal
* linter which can perform custom processing, but may be somewhat simpler to
* configure.
*
* == Script... ==
*
* The script will be invoked once for each file that is to be linted, with
* the file passed as the first argument. The file may begin with a "-"; ensure
* your script will not interpret such files as flags (perhaps by ending your
* script configuration with "--", if its argument parser supports that).
*
* Note that when run via `arc diff`, the list of files to be linted includes
* deleted files and files that were moved away by the change. The linter should
* not assume the path it is given exists, and it is not an error for the
* linter to be invoked with paths which are no longer there. (Every affected
* path is subject to lint because some linters may raise errors in other files
* when a file is removed, or raise an error about its removal.)
*
* The script should emit lint messages to stdout, which will be parsed with
* the provided regex.
*
* For example, you might use a configuration like this:
*
* /opt/lint/lint.sh --flag value --other-flag --
*
* stderr is ignored. If you have a script which writes messages to stderr,
* you can redirect stderr to stdout by using a configuration like this:
*
* sh -c '/opt/lint/lint.sh "$0" 2>&1'
*
* The return code of the script must be 0, or an exception will be raised
* reporting that the linter failed. If you have a script which exits nonzero
* under normal circumstances, you can force it to always exit 0 by using a
* configuration like this:
*
* sh -c '/opt/lint/lint.sh "$0" || true'
*
* Multiple instances of the script will be run in parallel if there are
* multiple files to be linted, so they should not use any unique resources.
* For instance, this configuration would not work properly, because several
* processes may attempt to write to the file at the same time:
*
* COUNTEREXAMPLE
* sh -c '/opt/lint/lint.sh --output /tmp/lint.out "$0" && cat /tmp/lint.out'
*
* There are necessary limits to how gracefully this linter can deal with
* edge cases, because it is just a script and a regex. If you need to do
* things that this linter can't handle, you can write a phutil linter and move
* the logic to handle those cases into PHP. PHP is a better general-purpose
* programming language than regular expressions are, if only by a small margin.
*
* == ...and Regex ==
*
* The regex must be a valid PHP PCRE regex, including delimiters and flags.
*
* The regex will be matched against the entire output of the script, so it
* should generally be in this form if messages are one-per-line:
*
* /^...$/m
*
* The regex should capture these named patterns with `(?P<name>...)`:
*
* - `message` (required) Text describing the lint message. For example,
* "This is a syntax error.".
* - `name` (optional) Text summarizing the lint message. For example,
* "Syntax Error".
* - `severity` (optional) The word "error", "warning", "autofix", "advice",
* or "disabled", in any combination of upper and lower case. Instead, you
* may match groups called `error`, `warning`, `advice`, `autofix`, or
* `disabled`. These allow you to match output formats like "E123" and
* "W123" to indicate errors and warnings, even though the word "error" is
* not present in the output. If no severity capturing group is present,
* messages are raised with "error" severity. If multiple severity capturing
* groups are present, messages are raised with the highest captured
* serverity. Capturing groups like `error` supersede the `severity`
* capturing group.
* - `error` (optional) Match some nonempty substring to indicate that this
* message has "error" severity.
* - `warning` (optional) Match some nonempty substring to indicate that this
* message has "warning" severity.
* - `advice` (optional) Match some nonempty substring to indicate that this
* message has "advice" severity.
* - `autofix` (optional) Match some nonempty substring to indicate that this
* message has "autofix" severity.
* - `disabled` (optional) Match some nonempty substring to indicate that this
* message has "disabled" severity.
* - `file` (optional) The name of the file to raise the lint message in. If
* not specified, defaults to the linted file. It is generally not necessary
* to capture this unless the linter can raise messages in files other than
* the one it is linting.
* - `line` (optional) The line number of the message.
* - `char` (optional) The character offset of the message.
* - `offset` (optional) The byte offset of the message. If captured, this
* supersedes `line` and `char`.
* - `original` (optional) The text the message affects.
* - `replacement` (optional) The text that the range captured by `original`
* should be automatically replaced by to resolve the message.
* - `code` (optional) A short error type identifier which can be used
* elsewhere to configure handling of specific types of messages. For
* example, "EXAMPLE1", "EXAMPLE2", etc., where each code identifies a
* class of message like "syntax error", "missing whitespace", etc. This
* allows configuration to later change the severity of all whitespace
* messages, for example.
* - `ignore` (optional) Match some nonempty substring to ignore the match.
* You can use this if your linter sometimes emits text like "No lint
* errors".
* - `stop` (optional) Match some nonempty substring to stop processing input.
* Remaining matches for this file will be discarded, but linting will
* continue with other linters and other files.
* - `halt` (optional) Match some nonempty substring to halt all linting of
* this file by any linter. Linting will continue with other files.
* - `throw` (optional) Match some nonempty substring to throw an error, which
* will stop `arc` completely. You can use this to fail abruptly if you
* encounter unexpected output. All processing will abort.
*
* Numbered capturing groups are ignored.
*
* For example, if your lint script's output looks like this:
*
* error:13 Too many goats!
* warning:22 Not enough boats.
*
* ...you could use this regex to parse it:
*
* /^(?P<severity>warning|error):(?P<line>\d+) (?P<message>.*)$/m
*
* The simplest valid regex for line-oriented output is something like this:
*
* /^(?P<message>.*)$/m
*
* @task lint Linting
* @task linterinfo Linter Information
* @task parse Parsing Output
* @task config Validating Configuration
*
* @group linter
*/
final class ArcanistScriptAndRegexLinter extends ArcanistLinter {
private $output = array();
/* -( Linting )------------------------------------------------------------ */
/**
* Run the script on each file to be linted.
*
* @task lint
*/
public function willLintPaths(array $paths) {
$script = $this->getConfiguredScript();
$root = $this->getEngine()->getWorkingCopy()->getProjectRoot();
$futures = array();
foreach ($paths as $path) {
$future = new ExecFuture('%C %s', $script, $path);
$future->setCWD($root);
$futures[$path] = $future;
}
foreach (Futures($futures)->limit(4) as $path => $future) {
list($stdout) = $future->resolvex();
$this->output[$path] = $stdout;
}
}
/**
* Run the regex on the output of the script.
*
* @task lint
*/
public function lintPath($path) {
$regex = $this->getConfiguredRegex();
$output = idx($this->output, $path);
if (!strlen($output)) {
// No output, but it exited 0, so just move on.
return;
}
$matches = null;
if (!preg_match_all($regex, $output, $matches, PREG_SET_ORDER)) {
// Output with no matches. This might be a configuration error, but more
// likely it's something like "No lint errors." and the user just hasn't
// written a sufficiently powerful/ridiculous regexp to capture it into an
// 'ignore' group. Don't make them figure this out; advanced users can
// capture 'throw' to handle this case.
return;
}
foreach ($matches as $match) {
if (!empty($match['throw'])) {
$throw = $match['throw'];
throw new ArcanistUsageException(
"ArcanistScriptAndRegexLinter: ".
"configuration captured a 'throw' named capturing group, ".
"'{$throw}'. Script output:\n".
$output);
}
if (!empty($match['halt'])) {
$this->stopAllLinters();
break;
}
if (!empty($match['stop'])) {
break;
}
if (!empty($match['ignore'])) {
continue;
}
list($line, $char) = $this->getMatchLineAndChar($match, $path);
$dict = array(
'path' => idx($match, 'file', $path),
'line' => $line,
'char' => $char,
'code' => idx($match, 'code', $this->getLinterName()),
'severity' => $this->getMatchSeverity($match),
'name' => idx($match, 'name', 'Lint'),
'description' => idx($match, 'message', 'Undefined Lint Message'),
);
$original = idx($match, 'original');
if ($original !== null) {
$dict['original'] = $original;
}
$replacement = idx($match, 'replacement');
if ($replacement !== null) {
$dict['replacement'] = $replacement;
}
$lint = ArcanistLintMessage::newFromDictionary($dict);
$this->addLintMessage($lint);
}
}
/* -( Linter Information )------------------------------------------------- */
/**
* Return the short name of the linter.
*
* @return string Short linter identifier.
*
* @task linterinfo
*/
public function getLinterName() {
return 'S&RX';
}
/* -( Parsing Output )----------------------------------------------------- */
/**
* Get the line and character of the message from the regex match.
*
* @param dict Captured groups from regex.
* @return pair<int,int> Line and character of the message.
*
* @task parse
*/
private function getMatchLineAndChar(array $match, $path) {
if (!empty($match['offset'])) {
list($line, $char) = $this->getEngine()->getLineAndCharFromOffset(
idx($match, 'file', $path),
$match['offset']);
return array($line + 1, $char + 1);
}
$line = idx($match, 'line', 1);
$char = idx($match, 'char');
return array($line, $char);
}
/**
* Map the regex matching groups to a message severity. We look for either
* a nonempty severity name group like 'error', or a group called 'severity'
* with a valid name.
*
* @param dict Captured groups from regex.
* @return const @{class:ArcanistLintSeverity} constant.
*
* @task parse
*/
private function getMatchSeverity(array $match) {
$map = array(
'error' => ArcanistLintSeverity::SEVERITY_ERROR,
'warning' => ArcanistLintSeverity::SEVERITY_WARNING,
'autofix' => ArcanistLintSeverity::SEVERITY_AUTOFIX,
'advice' => ArcanistLintSeverity::SEVERITY_ADVICE,
'disabled' => ArcanistLintSeverity::SEVERITY_DISABLED,
);
$severity_name = strtolower(idx($match, 'severity'));
foreach ($map as $name => $severity) {
if (!empty($match[$name])) {
return $severity;
}
if ($severity_name == $name) {
return $severity;
}
}
return ArcanistLintSeverity::SEVERITY_ERROR;
}
/* -( Validating Configuration )------------------------------------------- */
/**
* Load, validate, and return the "script" configuration.
*
* @return string The shell command fragment to use to run the linter.
*
* @task config
*/
private function getConfiguredScript() {
$working_copy = $this->getEngine()->getWorkingCopy();
$key = 'linter.scriptandregex.script';
$config = $working_copy->getConfigFromAnySource($key);
if (!$config) {
throw new ArcanistUsageException(
"ArcanistScriptAndRegexLinter: ".
"You must configure '{$key}' to point to a script to execute.");
}
// NOTE: No additional validation since the "script" can be some random
// shell command and/or include flags, so it does not need to point to some
// file on disk.
return $config;
}
/**
* Load, validate, and return the "regex" configuration.
*
* @return string A valid PHP PCRE regular expression.
*
* @task config
*/
private function getConfiguredRegex() {
$working_copy = $this->getEngine()->getWorkingCopy();
$key = 'linter.scriptandregex.regex';
$config = $working_copy->getConfigFromAnySource($key);
if (!$config) {
throw new ArcanistUsageException(
"ArcanistScriptAndRegexLinter: ".
"You must configure '{$key}' with a valid PHP PCRE regex.");
}
// NOTE: preg_match() returns 0 for no matches and false for compile error;
// this won't match, but will validate the syntax of the regex.
$ok = preg_match($config, 'syntax-check');
if ($ok === false) {
throw new ArcanistUsageException(
"ArcanistScriptAndRegexLinter: ".
"Regex '{$config}' does not compile. You must configure '{$key}' with ".
"a valid PHP PCRE regex, including delimiters.");
}
return $config;
}
}
diff --git a/src/lint/linter/ArcanistSpellingLinter.php b/src/lint/linter/ArcanistSpellingLinter.php
index bfd81f6a..032e51e1 100644
--- a/src/lint/linter/ArcanistSpellingLinter.php
+++ b/src/lint/linter/ArcanistSpellingLinter.php
@@ -1,173 +1,157 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Enforces basic spelling. Spelling inside code is actually pretty hard to
* get right without false positives. I take a conservative approach and
* just use a blacklisted set of words that are commonly spelled
* incorrectly.
*
* @group linter
*/
final class ArcanistSpellingLinter extends ArcanistLinter {
const LINT_SPELLING_PICKY = 0;
const LINT_SPELLING_IMPORTANT = 1;
private $partialWordRules;
private $wholeWordRules;
private $severity;
public function __construct($severity = self::LINT_SPELLING_PICKY) {
$this->severity = $severity;
$this->wholeWordRules = ArcanistSpellingDefaultData::getFullWordRules();
$this->partialWordRules =
ArcanistSpellingDefaultData::getPartialWordRules();
}
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'SPELL';
}
public function removeLintRule($word) {
foreach ($this->partialWordRules as $severity=>&$wordlist) {
unset($wordlist[$word]);
}
foreach ($this->wholeWordRules as $severity=>&$wordlist) {
unset($wordlist[$word]);
}
}
public function addPartialWordRule(
$incorrect_word,
$correct_word,
$severity=self::LINT_SPELLING_IMPORTANT) {
$this->partialWordRules[$severity][$incorrect_word] = $correct_word;
}
public function addWholeWordRule(
$incorrect_word,
$correct_word,
$severity=self::LINT_SPELLING_IMPORTANT) {
$this->wholeWordRules[$severity][$incorrect_word] = $correct_word;
}
public function getLintSeverityMap() {
return array(
self::LINT_SPELLING_PICKY => ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_SPELLING_IMPORTANT => ArcanistLintSeverity::SEVERITY_ERROR,
);
}
public function getLintNameMap() {
return array(
self::LINT_SPELLING_PICKY => 'Possible spelling mistake',
self::LINT_SPELLING_IMPORTANT => 'Possible spelling mistake',
);
}
public function lintPath($path) {
foreach ($this->partialWordRules as $severity => $wordlist) {
if ($severity >= $this->severity) {
foreach ($wordlist as $misspell => $correct) {
$this->checkPartialWord($path, $misspell, $correct, $severity);
}
}
}
foreach ($this->wholeWordRules as $severity => $wordlist) {
if ($severity >= $this->severity) {
foreach ($wordlist as $misspell => $correct) {
$this->checkWholeWord($path, $misspell, $correct, $severity);
}
}
}
}
protected function checkPartialWord($path, $word, $correct_word, $severity) {
$text = $this->getData($path);
$pos = 0;
while ($pos < strlen($text)) {
$next = stripos($text, $word, $pos);
if ($next === false) {
return;
}
$original = substr($text, $next, strlen($word));
$replacement = self::fixLetterCase($correct_word, $original);
$this->raiseLintAtOffset(
$next,
$severity,
sprintf(
"Possible spelling error. You wrote '%s', but did you mean '%s'?",
$word,
$correct_word
),
$original,
$replacement
);
$pos = $next + 1;
}
}
protected function checkWholeWord($path, $word, $correct_word, $severity) {
$text = $this->getData($path);
$matches = array();
$num_matches = preg_match_all(
'#\b' . preg_quote($word, '#') . '\b#i',
$text,
$matches,
PREG_OFFSET_CAPTURE
);
if (!$num_matches) {
return;
}
foreach ($matches[0] as $match) {
$original = $match[0];
$replacement = self::fixLetterCase($correct_word, $original);
$this->raiseLintAtOffset(
$match[1],
$severity,
sprintf(
"Possible spelling error. You wrote '%s', but did you mean '%s'?",
$word,
$correct_word
),
$original,
$replacement
);
}
}
public static function fixLetterCase($string, $case) {
if ($case == strtolower($case)) {
return strtolower($string);
}
if ($case == strtoupper($case)) {
return strtoupper($string);
}
if ($case == ucwords(strtolower($case))) {
return ucwords(strtolower($string));
}
return null;
}
}
diff --git a/src/lint/linter/ArcanistTextLinter.php b/src/lint/linter/ArcanistTextLinter.php
index b26191ce..7bd63e11 100644
--- a/src/lint/linter/ArcanistTextLinter.php
+++ b/src/lint/linter/ArcanistTextLinter.php
@@ -1,225 +1,209 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Enforces basic text file rules.
*
* @group linter
*/
final class ArcanistTextLinter extends ArcanistLinter {
const LINT_DOS_NEWLINE = 1;
const LINT_TAB_LITERAL = 2;
const LINT_LINE_WRAP = 3;
const LINT_EOF_NEWLINE = 4;
const LINT_BAD_CHARSET = 5;
const LINT_TRAILING_WHITESPACE = 6;
const LINT_NO_COMMIT = 7;
private $maxLineLength = 80;
public function setMaxLineLength($new_length) {
$this->maxLineLength = $new_length;
return $this;
}
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'TXT';
}
public function getLintSeverityMap() {
return array(
self::LINT_LINE_WRAP => ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_TRAILING_WHITESPACE => ArcanistLintSeverity::SEVERITY_AUTOFIX,
);
}
public function getLintNameMap() {
return array(
self::LINT_DOS_NEWLINE => 'DOS Newlines',
self::LINT_TAB_LITERAL => 'Tab Literal',
self::LINT_LINE_WRAP => 'Line Too Long',
self::LINT_EOF_NEWLINE => 'File Does Not End in Newline',
self::LINT_BAD_CHARSET => 'Bad Charset',
self::LINT_TRAILING_WHITESPACE => 'Trailing Whitespace',
self::LINT_NO_COMMIT => 'Explicit @no'.'commit',
);
}
public function lintPath($path) {
if (!strlen($this->getData($path))) {
// If the file is empty, don't bother; particularly, don't require
// the user to add a newline.
return;
}
$this->lintNewlines($path);
$this->lintTabs($path);
if ($this->didStopAllLinters()) {
return;
}
$this->lintCharset($path);
if ($this->didStopAllLinters()) {
return;
}
$this->lintLineLength($path);
$this->lintEOFNewline($path);
$this->lintTrailingWhitespace($path);
if ($this->getEngine()->getCommitHookMode()) {
$this->lintNoCommit($path);
}
}
protected function lintNewlines($path) {
$pos = strpos($this->getData($path), "\r");
if ($pos !== false) {
$this->raiseLintAtOffset(
$pos,
self::LINT_DOS_NEWLINE,
'You must use ONLY Unix linebreaks ("\n") in source code.',
"\r");
if ($this->isMessageEnabled(self::LINT_DOS_NEWLINE)) {
$this->stopAllLinters();
}
}
}
protected function lintTabs($path) {
$pos = strpos($this->getData($path), "\t");
if ($pos !== false) {
$this->raiseLintAtOffset(
$pos,
self::LINT_TAB_LITERAL,
'Configure your editor to use spaces for indentation.',
"\t");
}
}
protected function lintLineLength($path) {
$lines = explode("\n", $this->getData($path));
$width = $this->maxLineLength;
foreach ($lines as $line_idx => $line) {
if (strlen($line) > $width) {
$this->raiseLintAtLine(
$line_idx + 1,
1,
self::LINT_LINE_WRAP,
'This line is '.number_format(strlen($line)).' characters long, '.
'but the convention is '.$width.' characters.',
$line);
}
}
}
protected function lintEOFNewline($path) {
$data = $this->getData($path);
if (!strlen($data) || $data[strlen($data) - 1] != "\n") {
$this->raiseLintAtOffset(
strlen($data),
self::LINT_EOF_NEWLINE,
"Files must end in a newline.",
'',
"\n");
}
}
protected function lintCharset($path) {
$data = $this->getData($path);
$matches = null;
$preg = preg_match_all(
'/[^\x09\x0A\x20-\x7E]+/',
$data,
$matches,
PREG_OFFSET_CAPTURE);
if (!$preg) {
return;
}
foreach ($matches[0] as $match) {
list($string, $offset) = $match;
$this->raiseLintAtOffset(
$offset,
self::LINT_BAD_CHARSET,
'Source code should contain only ASCII bytes with ordinal decimal '.
'values between 32 and 126 inclusive, plus linefeed. Do not use UTF-8 '.
'or other multibyte charsets.',
$string);
}
if ($this->isMessageEnabled(self::LINT_BAD_CHARSET)) {
$this->stopAllLinters();
}
}
protected function lintTrailingWhitespace($path) {
$data = $this->getData($path);
$matches = null;
$preg = preg_match_all(
'/ +$/m',
$data,
$matches,
PREG_OFFSET_CAPTURE);
if (!$preg) {
return;
}
foreach ($matches[0] as $match) {
list($string, $offset) = $match;
$this->raiseLintAtOffset(
$offset,
self::LINT_TRAILING_WHITESPACE,
'This line contains trailing whitespace. Consider setting up your '.
'editor to automatically remove trailing whitespace, you will save '.
'time.',
$string,
'');
}
}
private function lintNoCommit($path) {
$data = $this->getData($path);
$deadly = '@no'.'commit';
$offset = strpos($data, $deadly);
if ($offset !== false) {
$this->raiseLintAtOffset(
$offset,
self::LINT_NO_COMMIT,
'This file is explicitly marked as "'.$deadly.'", which blocks '.
'commits.',
$deadly);
}
}
}
diff --git a/src/lint/linter/ArcanistXHPASTLinter.php b/src/lint/linter/ArcanistXHPASTLinter.php
index 28010688..1daa5d44 100644
--- a/src/lint/linter/ArcanistXHPASTLinter.php
+++ b/src/lint/linter/ArcanistXHPASTLinter.php
@@ -1,2049 +1,2033 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Uses XHPAST to apply lint rules to PHP.
*
* @group linter
*/
final class ArcanistXHPASTLinter extends ArcanistLinter {
protected $trees = array();
const LINT_PHP_SYNTAX_ERROR = 1;
const LINT_UNABLE_TO_PARSE = 2;
const LINT_VARIABLE_VARIABLE = 3;
const LINT_EXTRACT_USE = 4;
const LINT_UNDECLARED_VARIABLE = 5;
const LINT_PHP_SHORT_TAG = 6;
const LINT_PHP_ECHO_TAG = 7;
const LINT_PHP_CLOSE_TAG = 8;
const LINT_NAMING_CONVENTIONS = 9;
const LINT_IMPLICIT_CONSTRUCTOR = 10;
const LINT_DYNAMIC_DEFINE = 12;
const LINT_STATIC_THIS = 13;
const LINT_PREG_QUOTE_MISUSE = 14;
const LINT_PHP_OPEN_TAG = 15;
const LINT_TODO_COMMENT = 16;
const LINT_EXIT_EXPRESSION = 17;
const LINT_COMMENT_STYLE = 18;
const LINT_CLASS_FILENAME_MISMATCH = 19;
const LINT_TAUTOLOGICAL_EXPRESSION = 20;
const LINT_PLUS_OPERATOR_ON_STRINGS = 21;
const LINT_DUPLICATE_KEYS_IN_ARRAY = 22;
const LINT_REUSED_ITERATORS = 23;
const LINT_BRACE_FORMATTING = 24;
const LINT_PARENTHESES_SPACING = 25;
const LINT_CONTROL_STATEMENT_SPACING = 26;
const LINT_BINARY_EXPRESSION_SPACING = 27;
const LINT_ARRAY_INDEX_SPACING = 28;
const LINT_RAGGED_CLASSTREE_EDGE = 29;
const LINT_IMPLICIT_FALLTHROUGH = 30;
const LINT_PHP_53_FEATURES = 31;
const LINT_REUSED_AS_ITERATOR = 32;
const LINT_PHT_WITH_DYNAMIC_STRING = 33;
const LINT_COMMENT_SPACING = 34;
const LINT_PHP_54_FEATURES = 35;
const LINT_SLOWNESS = 36;
public function getLintNameMap() {
return array(
self::LINT_PHP_SYNTAX_ERROR => 'PHP Syntax Error!',
self::LINT_UNABLE_TO_PARSE => 'Unable to Parse',
self::LINT_VARIABLE_VARIABLE => 'Use of Variable Variable',
self::LINT_EXTRACT_USE => 'Use of extract()',
self::LINT_UNDECLARED_VARIABLE => 'Use of Undeclared Variable',
self::LINT_PHP_SHORT_TAG => 'Use of Short Tag "<?"',
self::LINT_PHP_ECHO_TAG => 'Use of Echo Tag "<?="',
self::LINT_PHP_CLOSE_TAG => 'Use of Close Tag "?>"',
self::LINT_NAMING_CONVENTIONS => 'Naming Conventions',
self::LINT_IMPLICIT_CONSTRUCTOR => 'Implicit Constructor',
self::LINT_DYNAMIC_DEFINE => 'Dynamic define()',
self::LINT_STATIC_THIS => 'Use of $this in Static Context',
self::LINT_PREG_QUOTE_MISUSE => 'Misuse of preg_quote()',
self::LINT_PHP_OPEN_TAG => 'Expected Open Tag',
self::LINT_TODO_COMMENT => 'TODO Comment',
self::LINT_EXIT_EXPRESSION => 'Exit Used as Expression',
self::LINT_COMMENT_STYLE => 'Comment Style',
self::LINT_CLASS_FILENAME_MISMATCH => 'Class-Filename Mismatch',
self::LINT_TAUTOLOGICAL_EXPRESSION => 'Tautological Expression',
self::LINT_PLUS_OPERATOR_ON_STRINGS => 'Not String Concatenation',
self::LINT_DUPLICATE_KEYS_IN_ARRAY => 'Duplicate Keys in Array',
self::LINT_REUSED_ITERATORS => 'Reuse of Iterator Variable',
self::LINT_BRACE_FORMATTING => 'Brace placement',
self::LINT_PARENTHESES_SPACING => 'Spaces Inside Parentheses',
self::LINT_CONTROL_STATEMENT_SPACING => 'Space After Control Statement',
self::LINT_BINARY_EXPRESSION_SPACING => 'Space Around Binary Operator',
self::LINT_ARRAY_INDEX_SPACING => 'Spacing Before Array Index',
self::LINT_RAGGED_CLASSTREE_EDGE => 'Class Not abstract Or final',
self::LINT_IMPLICIT_FALLTHROUGH => 'Implicit Fallthrough',
self::LINT_PHP_53_FEATURES => 'Use Of PHP 5.3 Features',
self::LINT_PHP_54_FEATURES => 'Use Of PHP 5.4 Features',
self::LINT_REUSED_AS_ITERATOR => 'Variable Reused As Iterator',
self::LINT_PHT_WITH_DYNAMIC_STRING => 'Use of pht() on Dynamic String',
self::LINT_COMMENT_SPACING => 'Comment Spaces',
self::LINT_SLOWNESS => 'Slow Construct',
);
}
public function getLinterName() {
return 'XHP';
}
public function getLintSeverityMap() {
return array(
self::LINT_TODO_COMMENT => ArcanistLintSeverity::SEVERITY_DISABLED,
self::LINT_UNABLE_TO_PARSE
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_NAMING_CONVENTIONS
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_PREG_QUOTE_MISUSE
=> ArcanistLintSeverity::SEVERITY_ADVICE,
self::LINT_BRACE_FORMATTING
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_PARENTHESES_SPACING
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_CONTROL_STATEMENT_SPACING
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_BINARY_EXPRESSION_SPACING
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_ARRAY_INDEX_SPACING
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_IMPLICIT_FALLTHROUGH
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_PHT_WITH_DYNAMIC_STRING
=> ArcanistLintSeverity::SEVERITY_DISABLED,
self::LINT_SLOWNESS
=> ArcanistLintSeverity::SEVERITY_WARNING,
self::LINT_COMMENT_SPACING
=> ArcanistLintSeverity::SEVERITY_ADVICE,
// This is disabled by default because it implies a very strict policy
// which isn't necessary in the general case.
self::LINT_RAGGED_CLASSTREE_EDGE
=> ArcanistLintSeverity::SEVERITY_DISABLED,
// This is disabled by default because projects don't necessarily target
// a specific minimum version.
self::LINT_PHP_53_FEATURES
=> ArcanistLintSeverity::SEVERITY_DISABLED,
self::LINT_PHP_54_FEATURES
=> ArcanistLintSeverity::SEVERITY_DISABLED,
);
}
public function willLintPaths(array $paths) {
$futures = array();
foreach ($paths as $path) {
$futures[$path] = xhpast_get_parser_future($this->getData($path));
}
foreach (Futures($futures)->limit(8) as $path => $future) {
$this->willLintPath($path);
try {
$this->trees[$path] = XHPASTTree::newFromDataAndResolvedExecFuture(
$this->getData($path),
$future->resolve());
} catch (XHPASTSyntaxErrorException $ex) {
$this->raiseLintAtLine(
$ex->getErrorLine(),
1,
self::LINT_PHP_SYNTAX_ERROR,
'This file contains a syntax error: '.$ex->getMessage());
$this->stopAllLinters();
return;
} catch (Exception $ex) {
$this->raiseLintAtPath(
self::LINT_UNABLE_TO_PARSE,
'XHPAST could not parse this file, probably because the AST is too '.
'deep. Some lint issues may not have been detected. You may safely '.
'ignore this warning.');
return;
}
}
}
public function getXHPASTTreeForPath($path) {
return idx($this->trees, $path);
}
public function lintPath($path) {
if (empty($this->trees[$path])) {
return;
}
$root = $this->trees[$path]->getRootNode();
$root->buildSelectCache();
$root->buildTokenCache();
$this->lintUseOfThisInStaticMethods($root);
$this->lintDynamicDefines($root);
$this->lintSurpriseConstructors($root);
$this->lintPHPTagUse($root);
$this->lintVariableVariables($root);
$this->lintTODOComments($root);
$this->lintExitExpressions($root);
$this->lintSpaceAroundBinaryOperators($root);
$this->lintSpaceAfterControlStatementKeywords($root);
$this->lintParenthesesShouldHugExpressions($root);
$this->lintNamingConventions($root);
$this->lintPregQuote($root);
$this->lintUndeclaredVariables($root);
$this->lintArrayIndexWhitespace($root);
$this->lintCommentSpaces($root);
$this->lintHashComments($root);
$this->lintPrimaryDeclarationFilenameMatch($root);
$this->lintTautologicalExpressions($root);
$this->lintPlusOperatorOnStrings($root);
$this->lintDuplicateKeysInArray($root);
$this->lintReusedIterators($root);
$this->lintBraceFormatting($root);
$this->lintRaggedClasstreeEdges($root);
$this->lintImplicitFallthrough($root);
$this->lintPHP53Features($root);
$this->lintPHP54Features($root);
$this->lintPHT($root);
$this->lintStrposUsedForStart($root);
$this->lintStrstrUsedForCheck($root);
}
public function lintStrstrUsedForCheck($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildOfType(1, 'n_OPERATOR');
$operator = $operator->getConcreteString();
if ($operator != '===' && $operator != '!==') {
continue;
}
$false = $expression->getChildByIndex(0);
if ($false->getTypeName() == 'n_SYMBOL_NAME' &&
$false->getConcreteString() == 'false') {
$strstr = $expression->getChildByIndex(2);
} else {
$strstr = $false;
$false = $expression->getChildByIndex(2);
if ($false->getTypeName() != 'n_SYMBOL_NAME' ||
$false->getConcreteString() != 'false') {
continue;
}
}
if ($strstr->getTypeName() != 'n_FUNCTION_CALL') {
continue;
}
$name = strtolower($strstr->getChildByIndex(0)->getConcreteString());
if ($name == 'strstr' || $name == 'strchr') {
$this->raiseLintAtNode(
$strstr,
self::LINT_SLOWNESS,
"Use strpos() for checking if the string contains something.");
} else if ($name == 'stristr') {
$this->raiseLintAtNode(
$strstr,
self::LINT_SLOWNESS,
"Use stripos() for checking if the string contains something.");
}
}
}
public function lintStrposUsedForStart($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildOfType(1, 'n_OPERATOR');
$operator = $operator->getConcreteString();
if ($operator != '===' && $operator != '!==') {
continue;
}
$zero = $expression->getChildByIndex(0);
if ($zero->getTypeName() == 'n_NUMERIC_SCALAR' &&
$zero->getConcreteString() == '0') {
$strpos = $expression->getChildByIndex(2);
} else {
$strpos = $zero;
$zero = $expression->getChildByIndex(2);
if ($zero->getTypeName() != 'n_NUMERIC_SCALAR' ||
$zero->getConcreteString() != '0') {
continue;
}
}
if ($strpos->getTypeName() != 'n_FUNCTION_CALL') {
continue;
}
$name = strtolower($strpos->getChildByIndex(0)->getConcreteString());
if ($name == 'strpos') {
$this->raiseLintAtNode(
$strpos,
self::LINT_SLOWNESS,
"Use strncmp() for checking if the string starts with something.");
} else if ($name == 'stripos') {
$this->raiseLintAtNode(
$strpos,
self::LINT_SLOWNESS,
"Use strncasecmp() for checking if the string starts with ".
"something.");
}
}
}
public function lintPHT($root) {
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = strtolower($call->getChildByIndex(0)->getConcreteString());
if ($name != 'pht') {
continue;
}
$parameters = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
if (!$parameters->getChildren()) {
continue;
}
$identifier = $parameters->getChildByIndex(0);
if ($identifier->getTypeName() == 'n_STRING_SCALAR') {
continue;
}
if ($identifier->getTypeName() == 'n_CONCATENATION_LIST') {
foreach ($identifier->getChildren() as $child) {
if ($child->getTypeName() == 'n_STRING_SCALAR' ||
$child->getTypeName() == 'n_OPERATOR') {
continue 2;
}
}
}
$this->raiseLintAtNode(
$call,
self::LINT_PHT_WITH_DYNAMIC_STRING,
"The first parameter of pht() can be only a scalar string, ".
"otherwise it can't be extracted.");
}
}
public function lintPHP53Features($root) {
$functions = $root->selectTokensOfType('T_FUNCTION');
foreach ($functions as $function) {
$next = $function->getNextToken();
while ($next) {
if ($next->isSemantic()) {
break;
}
$next = $next->getNextToken();
}
if ($next) {
if ($next->getTypeName() == '(') {
$this->raiseLintAtToken(
$function,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but anonymous functions were '.
'not introduced until PHP 5.3.');
}
}
}
$namespaces = $root->selectTokensOfType('T_NAMESPACE');
foreach ($namespaces as $namespace) {
$this->raiseLintAtToken(
$namespace,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but namespaces were not introduced '.
'until PHP 5.3.');
}
// NOTE: This is only "use x;", in anonymous functions the node type is
// n_LEXICAL_VARIABLE_LIST even though both tokens are T_USE.
// TODO: We parse n_USE in a slightly crazy way right now; that would be
// a better selector once it's fixed.
$uses = $root->selectDescendantsOfType('n_USE_LIST');
foreach ($uses as $use) {
$this->raiseLintAtNode(
$use,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but namespaces were not introduced '.
'until PHP 5.3.');
}
$statics = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($statics as $static) {
$name = $static->getChildOfType(0, 'n_CLASS_NAME');
if ($name->getConcreteString() == 'static') {
$this->raiseLintAtNode(
$name,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but `static::` was not introduced '.
'until PHP 5.3.');
}
}
$ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION');
foreach ($ternaries as $ternary) {
$yes = $ternary->getChildByIndex(1);
if ($yes->getTypeName() == 'n_EMPTY') {
$this->raiseLintAtNode(
$ternary,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but short ternary was not '.
'introduced until PHP 5.3.');
}
}
$this->lintPHP53Functions($root);
}
private function lintPHP53Functions($root) {
$target = phutil_get_library_root('arcanist').
'/../resources/php_compat_info.json';
$compat_info = json_decode(file_get_contents($target), true);
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$node = $call->getChildByIndex(0);
$name = strtolower($node->getConcreteString());
$version = idx($compat_info['functions'], $name);
$windows_version = idx($compat_info['functions_windows'], $name);
if ($version) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but `{$name}()` was not ".
"introduced until PHP {$version}.");
} else if (array_key_exists($name, $compat_info['params'])) {
$params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
foreach (array_values($params->getChildren()) as $i => $param) {
$version = idx($compat_info['params'][$name], $i);
if ($version) {
$this->raiseLintAtNode(
$param,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but parameter ".($i + 1)." ".
"of `{$name}()` was not introduced until PHP {$version}.");
}
}
} else if ($windows_version !== null) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but `{$name}()` is not available ".
"on Windows".
($windows_version ? " until PHP {$windows_version}" : "").".");
}
}
$classes = $root->selectDescendantsOfType('n_CLASS_NAME');
foreach ($classes as $node) {
$name = strtolower($node->getConcreteString());
$version = idx($compat_info['interfaces'], $name);
$version = idx($compat_info['classes'], $name, $version);
if ($version) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but `{$name}` was not ".
"introduced until PHP {$version}.");
}
}
}
public function lintPHP54Features($root) {
$indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS');
foreach ($indexes as $index) {
$left = $index->getChildByIndex(0);
switch ($left->getTypeName()) {
case 'n_FUNCTION_CALL':
$this->raiseLintAtNode(
$index->getChildByIndex(1),
self::LINT_PHP_54_FEATURES,
"The f()[...] syntax was not introduced until PHP 5.4, but this ".
"codebase targets an earlier version of PHP. You can rewrite ".
"this expression using idx().");
break;
}
}
}
private function lintImplicitFallthrough($root) {
$switches = $root->selectDescendantsOfType('n_SWITCH');
foreach ($switches as $switch) {
$blocks = array();
$cases = $switch->selectDescendantsOfType('n_CASE');
foreach ($cases as $case) {
$blocks[] = $case;
}
$defaults = $switch->selectDescendantsOfType('n_DEFAULT');
foreach ($defaults as $default) {
$blocks[] = $default;
}
foreach ($blocks as $key => $block) {
// Collect all the tokens in this block which aren't at top level.
// We want to ignore "break", and "continue" in these blocks.
$lower_level = $block->selectDescendantsOfType('n_WHILE');
$lower_level->add($block->selectDescendantsOfType('n_DO_WHILE'));
$lower_level->add($block->selectDescendantsOfType('n_FOR'));
$lower_level->add($block->selectDescendantsOfType('n_FOREACH'));
$lower_level->add($block->selectDescendantsOfType('n_SWITCH'));
$lower_level_tokens = array();
foreach ($lower_level as $lower_level_block) {
$lower_level_tokens += $lower_level_block->getTokens();
}
// Collect all the tokens in this block which aren't in this scope
// (because they're inside class, function or interface declarations).
// We want to ignore all of these tokens.
$decls = $block->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$decls->add($block->selectDescendantsOfType('n_CLASS_DECLARATION'));
// For completeness; these can't actually have anything.
$decls->add($block->selectDescendantsOfType('n_INTERFACE_DECLARATION'));
$different_scope_tokens = array();
foreach ($decls as $decl) {
$different_scope_tokens += $decl->getTokens();
}
$lower_level_tokens += $different_scope_tokens;
// Get all the trailing nonsemantic tokens, since we need to look for
// "fallthrough" comments past the end of the semantic block.
$tokens = $block->getTokens();
$last = end($tokens);
while ($last && $last = $last->getNextToken()) {
if (!$last->isSemantic()) {
$tokens[$last->getTokenID()] = $last;
}
}
$blocks[$key] = array(
$tokens,
$lower_level_tokens,
$different_scope_tokens,
);
}
foreach ($blocks as $token_lists) {
list(
$tokens,
$lower_level_tokens,
$different_scope_tokens) = $token_lists;
// Test each block (case or default statement) to see if it's OK. It's
// OK if:
//
// - it is empty; or
// - it ends in break, return, throw, continue or exit at top level; or
// - it has a comment with "fallthrough" in its text.
// Empty blocks are OK, so we start this at `true` and only set it to
// false if we find a statement.
$block_ok = true;
// Keeps track of whether the current statement is one that validates
// the block (break, return, throw, continue) or something else.
$statement_ok = false;
foreach ($tokens as $token_id => $token) {
if (!$token->isSemantic()) {
// Liberally match "fall" in the comment text so that comments like
// "fallthru", "fall through", "fallthrough", etc., are accepted.
if (preg_match('/fall/i', $token->getValue())) {
$block_ok = true;
break;
}
continue;
}
$tok_type = $token->getTypeName();
if ($tok_type == 'T_FUNCTION' ||
$tok_type == 'T_CLASS' ||
$tok_type == 'T_INTERFACE') {
// These aren't statements, but mark the block as nonempty anyway.
$block_ok = false;
continue;
}
if ($tok_type == ';') {
if ($statement_ok) {
$statment_ok = false;
} else {
$block_ok = false;
}
continue;
}
if ($tok_type == 'T_BREAK' ||
$tok_type == 'T_CONTINUE') {
if (empty($lower_level_tokens[$token_id])) {
$statement_ok = true;
$block_ok = true;
}
continue;
}
if ($tok_type == 'T_RETURN' ||
$tok_type == 'T_THROW' ||
$tok_type == 'T_EXIT') {
if (empty($different_scope_tokens[$token_id])) {
$statement_ok = true;
$block_ok = true;
}
continue;
}
}
if (!$block_ok) {
$this->raiseLintAtToken(
head($tokens),
self::LINT_IMPLICIT_FALLTHROUGH,
"This 'case' or 'default' has a nonempty block which does not ".
"end with 'break', 'continue', 'return', 'throw' or 'exit'. Did ".
"you forget to add one of those? If you intend to fall through, ".
"add a '// fallthrough' comment to silence this warning.");
}
}
}
}
private function lintBraceFormatting($root) {
foreach ($root->selectDescendantsOfType('n_STATEMENT_LIST') as $list) {
$tokens = $list->getTokens();
if (!$tokens || head($tokens)->getValue() != '{') {
continue;
}
list($before, $after) = $list->getSurroundingNonsemanticTokens();
if (!$before) {
$first = head($tokens);
// Only insert the space if we're after a closing parenthesis. If
// we're in a construct like "else{}", other rules will insert space
// after the 'else' correctly.
$prev = $first->getPrevToken();
if (!$prev || $prev->getValue() != ')') {
continue;
}
$this->raiseLintAtToken(
$first,
self::LINT_BRACE_FORMATTING,
'Put opening braces on the same line as control statements and '.
'declarations, with a single space before them.',
' '.$first->getValue());
} else if (count($before) == 1) {
$before = reset($before);
if ($before->getValue() != ' ') {
$this->raiseLintAtToken(
$before,
self::LINT_BRACE_FORMATTING,
'Put opening braces on the same line as control statements and '.
'declarations, with a single space before them.',
' ');
}
}
}
}
private function lintTautologicalExpressions($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
static $operators = array(
'-' => true,
'/' => true,
'-=' => true,
'/=' => true,
'<=' => true,
'<' => true,
'==' => true,
'===' => true,
'!=' => true,
'!==' => true,
'>=' => true,
'>' => true,
);
static $logical = array(
'||' => true,
'&&' => true,
);
foreach ($expressions as $expr) {
$operator = $expr->getChildByIndex(1)->getConcreteString();
if (!empty($operators[$operator])) {
$left = $expr->getChildByIndex(0)->getSemanticString();
$right = $expr->getChildByIndex(2)->getSemanticString();
if ($left == $right) {
$this->raiseLintAtNode(
$expr,
self::LINT_TAUTOLOGICAL_EXPRESSION,
'Both sides of this expression are identical, so it always '.
'evaluates to a constant.');
}
}
if (!empty($logical[$operator])) {
$left = $expr->getChildByIndex(0)->getSemanticString();
$right = $expr->getChildByIndex(2)->getSemanticString();
// NOTE: These will be null to indicate "could not evaluate".
$left = $this->evaluateStaticBoolean($left);
$right = $this->evaluateStaticBoolean($right);
if (($operator == '||' && ($left === true || $right === true)) ||
($operator == '&&' && ($left === false || $right === false))) {
$this->raiseLintAtNode(
$expr,
self::LINT_TAUTOLOGICAL_EXPRESSION,
'The logical value of this expression is static. Did you forget '.
'to remove some debugging code?');
}
}
}
}
/**
* Statically evaluate a boolean value from an XHP tree.
*
* TODO: Improve this and move it to XHPAST proper?
*
* @param string The "semantic string" of a single value.
* @return mixed ##true## or ##false## if the value could be evaluated
* statically; ##null## if static evaluation was not possible.
*/
private function evaluateStaticBoolean($string) {
switch (strtolower($string)) {
case '0':
case 'null':
case 'false':
return false;
case '1':
case 'true':
return true;
}
return null;
}
protected function lintCommentSpaces($root) {
foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
$value = $comment->getValue();
if ($value[0] != '#') {
$match = null;
if (preg_match('@^(/[/*]+)[^/*\s]@', $value, $match)) {
$this->raiseLintAtOffset(
$comment->getOffset(),
self::LINT_COMMENT_SPACING,
'Put space after comment start.',
$match[1],
$match[1].' ');
}
}
}
}
protected function lintHashComments($root) {
foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
$value = $comment->getValue();
if ($value[0] != '#') {
continue;
}
$this->raiseLintAtOffset(
$comment->getOffset(),
self::LINT_COMMENT_STYLE,
'Use "//" single-line comments, not "#".',
'#',
(preg_match('/^#\S/', $value) ? '// ' : '//'));
}
}
/**
* Find cases where loops get nested inside each other but use the same
* iterator variable. For example:
*
* COUNTEREXAMPLE
* foreach ($list as $thing) {
* foreach ($stuff as $thing) { // <-- Raises an error for reuse of $thing
* // ...
* }
* }
*
*/
private function lintReusedIterators($root) {
$used_vars = array();
$for_loops = $root->selectDescendantsOfType('n_FOR');
foreach ($for_loops as $for_loop) {
$var_map = array();
// Find all the variables that are assigned to in the for() expression.
$for_expr = $for_loop->getChildOfType(0, 'n_FOR_EXPRESSION');
$bin_exprs = $for_expr->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($bin_exprs as $bin_expr) {
if ($bin_expr->getChildByIndex(1)->getConcreteString() == '=') {
$var_map[$bin_expr->getChildByIndex(0)->getConcreteString()] = true;
}
}
$used_vars[$for_loop->getID()] = $var_map;
}
$foreach_loops = $root->selectDescendantsOfType('n_FOREACH');
foreach ($foreach_loops as $foreach_loop) {
$var_map = array();
$foreach_expr = $foreach_loop->getChildOftype(0, 'n_FOREACH_EXPRESSION');
// We might use one or two vars, i.e. "foreach ($x as $y => $z)" or
// "foreach ($x as $y)".
$possible_used_vars = array(
$foreach_expr->getChildByIndex(1),
$foreach_expr->getChildByIndex(2),
);
foreach ($possible_used_vars as $var) {
if ($var->getTypeName() == 'n_EMPTY') {
continue;
}
$name = $var->getConcreteString();
$name = trim($name, '&'); // Get rid of ref silliness.
$var_map[$name] = true;
}
$used_vars[$foreach_loop->getID()] = $var_map;
}
$all_loops = $for_loops->add($foreach_loops);
foreach ($all_loops as $loop) {
$child_for_loops = $loop->selectDescendantsOfType('n_FOR');
$child_foreach_loops = $loop->selectDescendantsOfType('n_FOREACH');
$child_loops = $child_for_loops->add($child_foreach_loops);
$outer_vars = $used_vars[$loop->getID()];
foreach ($child_loops as $inner_loop) {
$inner_vars = $used_vars[$inner_loop->getID()];
$shared = array_intersect_key($outer_vars, $inner_vars);
if ($shared) {
$shared_desc = implode(', ', array_keys($shared));
$this->raiseLintAtNode(
$inner_loop->getChildByIndex(0),
self::LINT_REUSED_ITERATORS,
"This loop reuses iterator variables ({$shared_desc}) from an ".
"outer loop. You might be clobbering the outer iterator. Change ".
"the inner loop to use a different iterator name.");
}
}
}
}
protected function lintVariableVariables($root) {
$vvars = $root->selectDescendantsOfType('n_VARIABLE_VARIABLE');
foreach ($vvars as $vvar) {
$this->raiseLintAtNode(
$vvar,
self::LINT_VARIABLE_VARIABLE,
'Rewrite this code to use an array. Variable variables are unclear '.
'and hinder static analysis.');
}
}
protected function lintUndeclaredVariables($root) {
// These things declare variables in a function:
// Explicit parameters
// Assignment
// Assignment via list()
// Static
// Global
// Lexical vars
// Builtins ($this)
// foreach()
// catch
//
// These things make lexical scope unknowable:
// Use of extract()
// Assignment to variable variables ($$x)
// Global with variable variables
//
// These things don't count as "using" a variable:
// isset()
// empty()
// Static class variables
//
// The general approach here is to find each function/method declaration,
// then:
//
// 1. Identify all the variable declarations, and where they first occur
// in the function/method declaration.
// 2. Identify all the uses that don't really count (as above).
// 3. Everything else must be a use of a variable.
// 4. For each variable, check if any uses occur before the declaration
// and warn about them.
//
// We also keep track of where lexical scope becomes unknowable (e.g.,
// because the function calls extract() or uses dynamic variables,
// preventing us from keeping track of which variables are defined) so we
// can stop issuing warnings after that.
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
$defs = $fdefs->add($mdefs);
foreach ($defs as $def) {
// We keep track of the first offset where scope becomes unknowable, and
// silence any warnings after that. Default it to INT_MAX so we can min()
// it later to keep track of the first problem we encounter.
$scope_destroyed_at = PHP_INT_MAX;
$declarations = array(
'$this' => 0,
) + array_fill_keys($this->getSuperGlobalNames(), 0);
$declaration_tokens = array();
$exclude_tokens = array();
$vars = array();
// First up, find all the different kinds of declarations, as explained
// above. Put the tokens into the $vars array.
$param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
$param_vars = $param_list->selectDescendantsOfType('n_VARIABLE');
foreach ($param_vars as $var) {
$vars[] = $var;
}
// This is PHP5.3 closure syntax: function () use ($x) {};
$lexical_vars = $def
->getChildByIndex(4)
->selectDescendantsOfType('n_VARIABLE');
foreach ($lexical_vars as $var) {
$vars[] = $var;
}
$body = $def->getChildByIndex(5);
if ($body->getTypeName() == 'n_EMPTY') {
// Abstract method declaration.
continue;
}
$static_vars = $body
->selectDescendantsOfType('n_STATIC_DECLARATION')
->selectDescendantsOfType('n_VARIABLE');
foreach ($static_vars as $var) {
$vars[] = $var;
}
$global_vars = $body
->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
foreach ($global_vars as $var_list) {
foreach ($var_list->getChildren() as $var) {
if ($var->getTypeName() == 'n_VARIABLE') {
$vars[] = $var;
} else {
// Dynamic global variable, i.e. "global $$x;".
$scope_destroyed_at = min($scope_destroyed_at, $var->getOffset());
// An error is raised elsewhere, no need to raise here.
}
}
}
$catches = $body
->selectDescendantsOfType('n_CATCH')
->selectDescendantsOfType('n_VARIABLE');
foreach ($catches as $var) {
$vars[] = $var;
}
$binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($binary as $expr) {
if ($expr->getChildByIndex(1)->getConcreteString() != '=') {
continue;
}
$lval = $expr->getChildByIndex(0);
if ($lval->getTypeName() == 'n_VARIABLE') {
$vars[] = $lval;
} else if ($lval->getTypeName() == 'n_LIST') {
// Recursivey grab everything out of list(), since the grammar
// permits list() to be nested. Also note that list() is ONLY valid
// as an lval assignments, so we could safely lift this out of the
// n_BINARY_EXPRESSION branch.
$assign_vars = $lval->selectDescendantsOfType('n_VARIABLE');
foreach ($assign_vars as $var) {
$vars[] = $var;
}
}
if ($lval->getTypeName() == 'n_VARIABLE_VARIABLE') {
$scope_destroyed_at = min($scope_destroyed_at, $lval->getOffset());
// No need to raise here since we raise an error elsewhere.
}
}
$calls = $body->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = strtolower($call->getChildByIndex(0)->getConcreteString());
if ($name == 'empty' || $name == 'isset') {
$params = $call
->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
->selectDescendantsOfType('n_VARIABLE');
foreach ($params as $var) {
$exclude_tokens[$var->getID()] = true;
}
continue;
}
if ($name != 'extract') {
continue;
}
$scope_destroyed_at = min($scope_destroyed_at, $call->getOffset());
$this->raiseLintAtNode(
$call,
self::LINT_EXTRACT_USE,
'Avoid extract(). It is confusing and hinders static analysis.');
}
// Now we have every declaration except foreach(), handled below. Build
// two maps, one which just keeps track of which tokens are part of
// declarations ($declaration_tokens) and one which has the first offset
// where a variable is declared ($declarations).
foreach ($vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$declarations[$concrete] = min(
idx($declarations, $concrete, PHP_INT_MAX),
$var->getOffset());
$declaration_tokens[$var->getID()] = true;
}
// Excluded tokens are ones we don't "count" as being uses, described
// above. Put them into $exclude_tokens.
$class_statics = $body
->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
$class_static_vars = $class_statics
->selectDescendantsOfType('n_VARIABLE');
foreach ($class_static_vars as $var) {
$exclude_tokens[$var->getID()] = true;
}
// Find all the variables in scope, and figure out where they are used.
// We want to find foreach() iterators which are both declared before and
// used after the foreach() loop.
$uses = array();
$all_vars = $body->selectDescendantsOfType('n_VARIABLE');
$all = array();
// NOTE: $all_vars is not a real array so we can't unset() it.
foreach ($all_vars as $var) {
// Be strict since it's easier; we don't let you reuse an iterator you
// declared before a loop after the loop, even if you're just assigning
// to it.
$concrete = $var->getConcreteString();
$uses[$concrete][$var->getID()] = $var->getOffset();
if (isset($declaration_tokens[$var->getID()])) {
// We know this is part of a declaration, so it's fine.
continue;
}
if (isset($exclude_tokens[$var->getID()])) {
// We know this is part of isset() or similar, so it's fine.
continue;
}
$all[$var->getID()] = $var;
}
// Do foreach() last, we want to handle implicit redeclaration of a
// variable already in scope since this probably means we're ovewriting a
// local.
// NOTE: Processing foreach expressions in order allows programs which
// reuse iterator variables in other foreach() loops -- this is fine. We
// have a separate warning to prevent nested loops from reusing the same
// iterators.
$foreaches = $body->selectDescendantsOfType('n_FOREACH');
$all_foreach_vars = array();
foreach ($foreaches as $foreach) {
$foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION');
$foreach_vars = array();
// Determine the end of the foreach() loop.
$foreach_tokens = $foreach->getTokens();
$last_token = end($foreach_tokens);
$foreach_end = $last_token->getOffset();
$key_var = $foreach_expr->getChildByIndex(1);
if ($key_var->getTypeName() == 'n_VARIABLE') {
$foreach_vars[] = $key_var;
}
$value_var = $foreach_expr->getChildByIndex(2);
if ($value_var->getTypeName() == 'n_VARIABLE') {
$foreach_vars[] = $value_var;
} else {
// The root-level token may be a reference, as in:
// foreach ($a as $b => &$c) { ... }
// Reach into the n_VARIABLE_REFERENCE node to grab the n_VARIABLE
// node.
$foreach_vars[] = $value_var->getChildOfType(0, 'n_VARIABLE');
}
// Remove all uses of the iterators inside of the foreach() loop from
// the $uses map.
foreach ($foreach_vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$offset = $var->getOffset();
foreach ($uses[$concrete] as $id => $use_offset) {
if (($use_offset >= $offset) && ($use_offset < $foreach_end)) {
unset($uses[$concrete][$id]);
}
}
$all_foreach_vars[] = $var;
}
}
foreach ($all_foreach_vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$offset = $var->getOffset();
// If a variable was declared before a foreach() and is used after
// it, raise a message.
if (isset($declarations[$concrete])) {
if ($declarations[$concrete] < $offset) {
if (!empty($uses[$concrete]) &&
max($uses[$concrete]) > $offset) {
$this->raiseLintAtNode(
$var,
self::LINT_REUSED_AS_ITERATOR,
'This iterator variable is a previously declared local '.
'variable. To avoid overwriting locals, do not reuse them '.
'as iterator variables.');
}
}
}
// This is a declaration, exclude it from the "declare variables prior
// to use" check below.
unset($all[$var->getID()]);
$vars[] = $var;
}
// Now rebuild declarations to include foreach().
foreach ($vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$declarations[$concrete] = min(
idx($declarations, $concrete, PHP_INT_MAX),
$var->getOffset());
$declaration_tokens[$var->getID()] = true;
}
// Issue a warning for every variable token, unless it appears in a
// declaration, we know about a prior declaration, we have explicitly
// exlcuded it, or scope has been made unknowable before it appears.
$issued_warnings = array();
foreach ($all as $id => $var) {
if ($var->getOffset() >= $scope_destroyed_at) {
// This appears after an extract() or $$var so we have no idea
// whether it's legitimate or not. We raised a harshly-worded warning
// when scope was made unknowable, so just ignore anything we can't
// figure out.
continue;
}
$concrete = $this->getConcreteVariableString($var);
if ($var->getOffset() >= idx($declarations, $concrete, PHP_INT_MAX)) {
// The use appears after the variable is declared, so it's fine.
continue;
}
if (!empty($issued_warnings[$concrete])) {
// We've already issued a warning for this variable so we don't need
// to issue another one.
continue;
}
$this->raiseLintAtNode(
$var,
self::LINT_UNDECLARED_VARIABLE,
'Declare variables prior to use (even if you are passing them '.
'as reference parameters). You may have misspelled this '.
'variable name.');
$issued_warnings[$concrete] = true;
}
}
}
private function getConcreteVariableString($var) {
$concrete = $var->getConcreteString();
// Strip off curly braces as in $obj->{$property}.
$concrete = trim($concrete, '{}');
return $concrete;
}
protected function lintPHPTagUse($root) {
$tokens = $root->getTokens();
foreach ($tokens as $token) {
if ($token->getTypeName() == 'T_OPEN_TAG') {
if (trim($token->getValue()) == '<?') {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_SHORT_TAG,
'Use the full form of the PHP open tag, "<?php".',
"<?php\n");
}
break;
} else if ($token->getTypeName() == 'T_OPEN_TAG_WITH_ECHO') {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_ECHO_TAG,
'Avoid the PHP echo short form, "<?=".');
break;
} else {
if (!preg_match('/^#!/', $token->getValue())) {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_OPEN_TAG,
'PHP files should start with "<?php", which may be preceded by '.
'a "#!" line for scripts.');
}
break;
}
}
foreach ($root->selectTokensOfType('T_CLOSE_TAG') as $token) {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_CLOSE_TAG,
'Do not use the PHP closing tag, "?>".');
}
}
protected function lintNamingConventions($root) {
// We're going to build up a list of <type, name, token, error> tuples
// and then try to instantiate a hook class which has the opportunity to
// override us.
$names = array();
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$name_token = $class->getChildByIndex(1);
$name_string = $name_token->getConcreteString();
$names[] = array(
'class',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
? null
: 'Follow naming conventions: classes should be named using '.
'UpperCamelCase.',
);
}
$ifaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
foreach ($ifaces as $iface) {
$name_token = $iface->getChildByIndex(1);
$name_string = $name_token->getConcreteString();
$names[] = array(
'interface',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
? null
: 'Follow naming conventions: interfaces should be named using '.
'UpperCamelCase.',
);
}
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($functions as $function) {
$name_token = $function->getChildByIndex(2);
if ($name_token->getTypeName() == 'n_EMPTY') {
// Unnamed closure.
continue;
}
$name_string = $name_token->getConcreteString();
$names[] = array(
'function',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
? null
: 'Follow naming conventions: functions should be named using '.
'lowercase_with_underscores.',
);
}
$methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$name_token = $method->getChildByIndex(2);
$name_string = $name_token->getConcreteString();
$names[] = array(
'method',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowerCamelCase(
ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
? null
: 'Follow naming conventions: methods should be named using '.
'lowerCamelCase.',
);
}
$param_tokens = array();
$params = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST');
foreach ($params as $param_list) {
foreach ($param_list->getChildren() as $param) {
$name_token = $param->getChildByIndex(1);
if ($name_token->getTypeName() == 'n_VARIABLE_REFERENCE') {
$name_token = $name_token->getChildOfType(0, 'n_VARIABLE');
}
$param_tokens[$name_token->getID()] = true;
$name_string = $name_token->getConcreteString();
$names[] = array(
'parameter',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
? null
: 'Follow naming conventions: parameters should be named using '.
'lowercase_with_underscores.',
);
}
}
$constants = $root->selectDescendantsOfType(
'n_CLASS_CONSTANT_DECLARATION_LIST');
foreach ($constants as $constant_list) {
foreach ($constant_list->getChildren() as $constant) {
$name_token = $constant->getChildByIndex(0);
$name_string = $name_token->getConcreteString();
$names[] = array(
'constant',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($name_string)
? null
: 'Follow naming conventions: class constants should be named '.
'using UPPERCASE_WITH_UNDERSCORES.',
);
}
}
$member_tokens = array();
$props = $root->selectDescendantsOfType('n_CLASS_MEMBER_DECLARATION_LIST');
foreach ($props as $prop_list) {
foreach ($prop_list->getChildren() as $token_id => $prop) {
if ($prop->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') {
continue;
}
$name_token = $prop->getChildByIndex(0);
$member_tokens[$name_token->getID()] = true;
$name_string = $name_token->getConcreteString();
$names[] = array(
'member',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowerCamelCase(
ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
? null
: 'Follow naming conventions: class properties should be named '.
'using lowerCamelCase.',
);
}
}
$superglobal_map = array_fill_keys(
$this->getSuperGlobalNames(),
true);
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
$defs = $fdefs->add($mdefs);
foreach ($defs as $def) {
$globals = $def->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
$globals = $globals->selectDescendantsOfType('n_VARIABLE');
$globals_map = array();
foreach ($globals as $global) {
$global_string = $global->getConcreteString();
$globals_map[$global_string] = true;
$names[] = array(
'global',
$global_string,
$global,
// No advice for globals, but hooks have an option to provide some.
null);
}
// Exclude access of static properties, since lint will be raised at
// their declaration if they're invalid and they may not conform to
// variable rules. This is slightly overbroad (includes the entire
// rhs of a "Class::..." token) to cover cases like "Class:$x[0]". These
// varaibles are simply made exempt from naming conventions.
$exclude_tokens = array();
$statics = $def->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($statics as $static) {
$rhs = $static->getChildByIndex(1);
$rhs_vars = $def->selectDescendantsOfType('n_VARIABLE');
foreach ($rhs_vars as $var) {
$exclude_tokens[$var->getID()] = true;
}
}
$vars = $def->selectDescendantsOfType('n_VARIABLE');
foreach ($vars as $token_id => $var) {
if (isset($member_tokens[$token_id])) {
continue;
}
if (isset($param_tokens[$token_id])) {
continue;
}
if (isset($exclude_tokens[$token_id])) {
continue;
}
$var_string = $var->getConcreteString();
// Awkward artifact of "$o->{$x}".
$var_string = trim($var_string, '{}');
if (isset($superglobal_map[$var_string])) {
continue;
}
if (isset($globals_map[$var_string])) {
continue;
}
$names[] = array(
'variable',
$var_string,
$var,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPVariable($var_string))
? null
: 'Follow naming conventions: variables should be named using '.
'lowercase_with_underscores.',
);
}
}
$engine = $this->getEngine();
$working_copy = $engine->getWorkingCopy();
if ($working_copy) {
// If a naming hook is configured, give it a chance to override the
// default results for all the symbol names.
$hook_class = $working_copy->getConfig('lint.xhpast.naminghook');
if ($hook_class) {
$hook_obj = newv($hook_class, array());
foreach ($names as $k => $name_attrs) {
list($type, $name, $token, $default) = $name_attrs;
$result = $hook_obj->lintSymbolName($type, $name, $default);
$names[$k][3] = $result;
}
}
}
// Raise anything we're left with.
foreach ($names as $k => $name_attrs) {
list($type, $name, $token, $result) = $name_attrs;
if ($result) {
$this->raiseLintAtNode(
$token,
self::LINT_NAMING_CONVENTIONS,
$result);
}
}
}
protected function lintSurpriseConstructors($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1)->getConcreteString();
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$method_name_token = $method->getChildByIndex(2);
$method_name = $method_name_token->getConcreteString();
if (strtolower($class_name) == strtolower($method_name)) {
$this->raiseLintAtNode(
$method_name_token,
self::LINT_IMPLICIT_CONSTRUCTOR,
'Name constructors __construct() explicitly. This method is a '.
'constructor because it has the same name as the class it is '.
'defined in.');
}
}
}
}
protected function lintParenthesesShouldHugExpressions($root) {
$calls = $root->selectDescendantsOfType('n_CALL_PARAMETER_LIST');
$controls = $root->selectDescendantsOfType('n_CONTROL_CONDITION');
$fors = $root->selectDescendantsOfType('n_FOR_EXPRESSION');
$foreach = $root->selectDescendantsOfType('n_FOREACH_EXPRESSION');
$decl = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST');
$all_paren_groups = $calls
->add($controls)
->add($fors)
->add($foreach)
->add($decl);
foreach ($all_paren_groups as $group) {
$tokens = $group->getTokens();
$token_o = array_shift($tokens);
$token_c = array_pop($tokens);
if ($token_o->getTypeName() != '(') {
throw new Exception('Expected open paren!');
}
if ($token_c->getTypeName() != ')') {
throw new Exception('Expected close paren!');
}
$nonsem_o = $token_o->getNonsemanticTokensAfter();
$nonsem_c = $token_c->getNonsemanticTokensBefore();
if (!$nonsem_o) {
continue;
}
$raise = array();
$string_o = implode('', mpull($nonsem_o, 'getValue'));
if (preg_match('/^[ ]+$/', $string_o)) {
$raise[] = array($nonsem_o, $string_o);
}
if ($nonsem_o !== $nonsem_c) {
$string_c = implode('', mpull($nonsem_c, 'getValue'));
if (preg_match('/^[ ]+$/', $string_c)) {
$raise[] = array($nonsem_c, $string_c);
}
}
foreach ($raise as $warning) {
list($tokens, $string) = $warning;
$this->raiseLintAtOffset(
reset($tokens)->getOffset(),
self::LINT_PARENTHESES_SPACING,
'Parentheses should hug their contents.',
$string,
'');
}
}
}
protected function lintSpaceAfterControlStatementKeywords($root) {
foreach ($root->getTokens() as $id => $token) {
switch ($token->getTypeName()) {
case 'T_IF':
case 'T_ELSE':
case 'T_FOR':
case 'T_FOREACH':
case 'T_WHILE':
case 'T_DO':
case 'T_SWITCH':
$after = $token->getNonsemanticTokensAfter();
if (empty($after)) {
$this->raiseLintAtToken(
$token,
self::LINT_CONTROL_STATEMENT_SPACING,
'Convention: put a space after control statements.',
$token->getValue().' ');
} else if (count($after) == 1) {
$space = head($after);
// If we have an else clause with braces, $space may not be
// a single white space. e.g.,
//
// if ($x)
// echo 'foo'
// else // <- $space is not " " but "\n ".
// echo 'bar'
//
// We just require it starts with either a whitespace or a newline.
if ($token->getTypeName() == 'T_ELSE' ||
$token->getTypeName() == 'T_DO') {
break;
}
if ($space->isAnyWhitespace() && $space->getValue() != ' ') {
$this->raiseLintAtToken(
$space,
self::LINT_CONTROL_STATEMENT_SPACING,
'Convention: put a single space after control statements.',
' ');
}
}
break;
}
}
}
protected function lintSpaceAroundBinaryOperators($root) {
// NOTE: '.' is parsed as n_CONCATENATION_LIST, not n_BINARY_EXPRESSION,
// so we don't select it here.
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildByIndex(1);
$operator_value = $operator->getConcreteString();
list($before, $after) = $operator->getSurroundingNonsemanticTokens();
$replace = null;
if (empty($before) && empty($after)) {
$replace = " {$operator_value} ";
} else if (empty($before)) {
$replace = " {$operator_value}";
} else if (empty($after)) {
$replace = "{$operator_value} ";
}
if ($replace !== null) {
$this->raiseLintAtNode(
$operator,
self::LINT_BINARY_EXPRESSION_SPACING,
'Convention: logical and arithmetic operators should be '.
'surrounded by whitespace.',
$replace);
}
}
$tokens = $root->selectTokensOfType(',');
foreach ($tokens as $token) {
$next = $token->getNextToken();
switch ($next->getTypeName()) {
case ')':
case 'T_WHITESPACE':
break;
break;
default:
$this->raiseLintAtToken(
$token,
self::LINT_BINARY_EXPRESSION_SPACING,
'Convention: comma should be followed by space.',
', ');
break;
}
}
// TODO: Spacing around ".".
// TODO: Spacing around default parameter assignment in function/method
// declarations (which is not n_BINARY_EXPRESSION).
}
protected function lintDynamicDefines($root) {
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = $call->getChildByIndex(0)->getConcreteString();
if (strtolower($name) == 'define') {
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
$defined = $parameter_list->getChildByIndex(0);
if (!$defined->isStaticScalar()) {
$this->raiseLintAtNode(
$defined,
self::LINT_DYNAMIC_DEFINE,
'First argument to define() must be a string literal.');
}
}
}
}
protected function lintUseOfThisInStaticMethods($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$attributes = $method
->getChildByIndex(0, 'n_METHOD_MODIFIER_LIST')
->selectDescendantsOfType('n_STRING');
$method_is_static = false;
$method_is_abstract = false;
foreach ($attributes as $attribute) {
if (strtolower($attribute->getConcreteString()) == 'static') {
$method_is_static = true;
}
if (strtolower($attribute->getConcreteString()) == 'abstract') {
$method_is_abstract = true;
}
}
if ($method_is_abstract) {
continue;
}
if (!$method_is_static) {
continue;
}
$body = $method->getChildOfType(5, 'n_STATEMENT_LIST');
$variables = $body->selectDescendantsOfType('n_VARIABLE');
foreach ($variables as $variable) {
if ($method_is_static &&
strtolower($variable->getConcreteString()) == '$this') {
$this->raiseLintAtNode(
$variable,
self::LINT_STATIC_THIS,
'You can not reference "$this" inside a static method.');
}
}
}
}
}
/**
* preg_quote() takes two arguments, but the second one is optional because
* it is possible to use (), [] or {} as regular expression delimiters. If
* you don't pass a second argument, you're probably going to get something
* wrong.
*/
protected function lintPregQuote($root) {
$function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($function_calls as $call) {
$name = $call->getChildByIndex(0)->getConcreteString();
if (strtolower($name) === 'preg_quote') {
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
if (count($parameter_list->getChildren()) !== 2) {
$this->raiseLintAtNode(
$call,
self::LINT_PREG_QUOTE_MISUSE,
'If you use pattern delimiters that require escaping (such as //, '.
'but not ()) then you should pass two arguments to preg_quote(), '.
'so that preg_quote() knows which delimiter to escape.');
}
}
}
}
/**
* Exit is parsed as an expression, but using it as such is almost always
* wrong. That is, this is valid:
*
* strtoupper(33 * exit - 6);
*
* When exit is used as an expression, it causes the program to terminate with
* exit code 0. This is likely not what is intended; these statements have
* different effects:
*
* exit(-1);
* exit -1;
*
* The former exits with a failure code, the latter with a success code!
*/
protected function lintExitExpressions($root) {
$unaries = $root->selectDescendantsOfType('n_UNARY_PREFIX_EXPRESSION');
foreach ($unaries as $unary) {
$operator = $unary->getChildByIndex(0)->getConcreteString();
if (strtolower($operator) == 'exit') {
if ($unary->getParentNode()->getTypeName() != 'n_STATEMENT') {
$this->raiseLintAtNode(
$unary,
self::LINT_EXIT_EXPRESSION,
"Use exit as a statement, not an expression.");
}
}
}
}
private function lintArrayIndexWhitespace($root) {
$indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS');
foreach ($indexes as $index) {
$tokens = $index->getChildByIndex(0)->getTokens();
$last = array_pop($tokens);
$trailing = $last->getNonsemanticTokensAfter();
$trailing_text = implode('', mpull($trailing, 'getValue'));
if (preg_match('/^ +$/', $trailing_text)) {
$this->raiseLintAtOffset(
$last->getOffset() + strlen($last->getValue()),
self::LINT_ARRAY_INDEX_SPACING,
'Convention: no spaces before index access.',
$trailing_text,
'');
}
}
}
protected function lintTODOComments($root) {
$comments = $root->selectTokensOfType('T_COMMENT') +
$root->selectTokensOfType('T_DOC_COMMENT');
foreach ($comments as $token) {
$value = $token->getValue();
$matches = null;
$preg = preg_match_all(
'/TODO/',
$value,
$matches,
PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $match) {
list($string, $offset) = $match;
$this->raiseLintAtOffset(
$token->getOffset() + $offset,
self::LINT_TODO_COMMENT,
'This comment has a TODO.',
$string);
}
}
}
/**
* Lint that if the file declares exactly one interface or class,
* the name of the file matches the name of the class,
* unless the classname is funky like an XHP element.
*/
private function lintPrimaryDeclarationFilenameMatch($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
if (count($classes) + count($interfaces) != 1) {
return;
}
$declarations = count($classes) ? $classes : $interfaces;
$declarations->rewind();
$declaration = $declarations->current();
$decl_name = $declaration->getChildByIndex(1);
$decl_string = $decl_name->getConcreteString();
// Exclude strangely named classes, e.g. XHP tags.
if (!preg_match('/^\w+$/', $decl_string)) {
return;
}
$rename = $decl_string.'.php';
$path = $this->getActivePath();
$filename = basename($path);
if ($rename == $filename) {
return;
}
$this->raiseLintAtNode(
$decl_name,
self::LINT_CLASS_FILENAME_MISMATCH,
"The name of this file differs from the name of the class or interface ".
"it declares. Rename the file to '{$rename}'."
);
}
private function lintPlusOperatorOnStrings($root) {
$binops = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($binops as $binop) {
$op = $binop->getChildByIndex(1);
if ($op->getConcreteString() != '+') {
continue;
}
$left = $binop->getChildByIndex(0);
$right = $binop->getChildByIndex(2);
if (($left->getTypeName() == 'n_STRING_SCALAR') ||
($right->getTypeName() == 'n_STRING_SCALAR')) {
$this->raiseLintAtNode(
$binop,
self::LINT_PLUS_OPERATOR_ON_STRINGS,
"In PHP, '.' is the string concatenation operator, not '+'. This ".
"expression uses '+' with a string literal as an operand.");
}
}
}
/**
* Finds duplicate keys in array initializers, as in
* array(1 => 'anything', 1 => 'foo'). Since the first entry is ignored,
* this is almost certainly an error.
*/
private function lintDuplicateKeysInArray($root) {
$array_literals = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
foreach ($array_literals as $array_literal) {
$nodes_by_key = array();
$keys_warn = array();
$list_node = $array_literal->getChildByIndex(0);
foreach ($list_node->getChildren() as $array_entry) {
$key_node = $array_entry->getChildByIndex(0);
switch ($key_node->getTypeName()) {
case 'n_STRING_SCALAR':
case 'n_NUMERIC_SCALAR':
// Scalars: array(1 => 'v1', '1' => 'v2');
$key = 'scalar:'.(string)$key_node->evalStatic();
break;
case 'n_SYMBOL_NAME':
case 'n_VARIABLE':
case 'n_CLASS_STATIC_ACCESS':
// Constants: array(CONST => 'v1', CONST => 'v2');
// Variables: array($a => 'v1', $a => 'v2');
// Class constants and vars: array(C::A => 'v1', C::A => 'v2');
$key = $key_node->getTypeName().':'.$key_node->getConcreteString();
break;
default:
$key = null;
break;
}
if ($key !== null) {
if (isset($nodes_by_key[$key])) {
$keys_warn[$key] = true;
}
$nodes_by_key[$key][] = $key_node;
}
}
foreach ($keys_warn as $key => $_) {
foreach ($nodes_by_key[$key] as $node) {
$this->raiseLintAtNode(
$node,
self::LINT_DUPLICATE_KEYS_IN_ARRAY,
"Duplicate key in array initializer. PHP will ignore all ".
"but the last entry.");
}
}
}
}
private function lintRaggedClasstreeEdges($root) {
$parser = new PhutilDocblockParser();
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$is_final = false;
$is_abstract = false;
$is_concrete_extensible = false;
$attributes = $class->getChildOfType(0, 'n_CLASS_ATTRIBUTES');
foreach ($attributes->getChildren() as $child) {
if ($child->getConcreteString() == 'final') {
$is_final = true;
}
if ($child->getConcreteString() == 'abstract') {
$is_abstract = true;
}
}
$docblock = $class->getDocblockToken();
if ($docblock) {
list($text, $specials) = $parser->parse($docblock->getValue());
$is_concrete_extensible = idx($specials, 'concrete-extensible');
}
if (!$is_final && !$is_abstract && !$is_concrete_extensible) {
$this->raiseLintAtNode(
$class->getChildOfType(1, 'n_CLASS_NAME'),
self::LINT_RAGGED_CLASSTREE_EDGE,
"This class is neither 'final' nor 'abstract', and does not have ".
"a docblock marking it '@concrete-extensible'.");
}
}
}
protected function raiseLintAtToken(
XHPASTToken $token,
$code,
$desc,
$replace = null) {
return $this->raiseLintAtOffset(
$token->getOffset(),
$code,
$desc,
$token->getValue(),
$replace);
}
protected function raiseLintAtNode(
XHPASTNode $node,
$code,
$desc,
$replace = null) {
return $this->raiseLintAtOffset(
$node->getOffset(),
$code,
$desc,
$node->getConcreteString(),
$replace);
}
public function getSuperGlobalNames() {
return array(
'$GLOBALS',
'$_SERVER',
'$_GET',
'$_POST',
'$_FILES',
'$_COOKIE',
'$_SESSION',
'$_REQUEST',
'$_ENV',
);
}
}
diff --git a/src/lint/linter/__tests__/ArcanistApacheLicenseLinterTestCase.php b/src/lint/linter/__tests__/ArcanistApacheLicenseLinterTestCase.php
index ab026261..72b79911 100644
--- a/src/lint/linter/__tests__/ArcanistApacheLicenseLinterTestCase.php
+++ b/src/lint/linter/__tests__/ArcanistApacheLicenseLinterTestCase.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Test cases for @{class:ArcanistApacheLicenseLinter}.
*
* @group testcase
*/
final class ArcanistApacheLicenseLinterTestCase extends ArcanistLinterTestCase {
public function testApacheLicenseLint() {
$linter = new ArcanistApacheLicenseLinter();
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(__FILE__);
return $this->executeTestsInDirectory(
dirname(__FILE__).'/apachelicense/',
$linter,
$working_copy);
}
protected function compareTransform($expected, $actual) {
$expected = str_replace('YYYY', date('Y'), $expected);
return parent::compareTransform($expected, $actual);
}
}
diff --git a/src/lint/linter/__tests__/ArcanistLinterTestCase.php b/src/lint/linter/__tests__/ArcanistLinterTestCase.php
index a4974561..364e65f8 100644
--- a/src/lint/linter/__tests__/ArcanistLinterTestCase.php
+++ b/src/lint/linter/__tests__/ArcanistLinterTestCase.php
@@ -1,191 +1,175 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Facilitiates implementation of test cases for @{class:ArcanistLinter}s.
*
* @group testcase
*/
abstract class ArcanistLinterTestCase extends ArcanistTestCase {
public function executeTestsInDirectory($root, $linter, $working_copy) {
foreach (Filesystem::listDirectory($root, $hidden = false) as $file) {
$this->lintFile($root.$file, $linter, $working_copy);
}
}
private function lintFile($file, $linter, $working_copy) {
$linter = clone $linter;
$contents = Filesystem::readFile($file);
$contents = explode("~~~~~~~~~~\n", $contents);
if (count($contents) < 2) {
throw new Exception(
"Expected '~~~~~~~~~~' separating test case and results.");
}
list ($data, $expect, $xform, $config) = array_merge(
$contents,
array(null, null));
$basename = basename($file);
if ($config) {
$config = json_decode($config, true);
if (!is_array($config)) {
throw new Exception(
"Invalid configuration in test '{$basename}', not valid JSON.");
}
} else {
$config = array();
}
/* TODO: ?
validate_parameter_list(
$config,
array(
),
array(
'project' => true,
'path' => true,
'hook' => true,
));
*/
$exception = null;
$after_lint = null;
$messages = null;
$exception_message = false;
$caught_exception = false;
try {
$path = idx($config, 'path', 'lint/'.$basename.'.php');
$engine = new UnitTestableArcanistLintEngine();
$engine->setWorkingCopy($working_copy);
$engine->setPaths(array($path));
$engine->setCommitHookMode(idx($config, 'hook', false));
$linter->addPath($path);
$linter->addData($path, $data);
$engine->addLinter($linter);
$engine->addFileData($path, $data);
$results = $engine->run();
$this->assertEqual(
1,
count($results),
'Expect one result returned by linter.');
$result = reset($results);
$patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
$after_lint = $patcher->getModifiedFileContent();
} catch (ArcanistPhutilTestTerminatedException $ex) {
throw $ex;
} catch (Exception $exception) {
$caught_exception = true;
if ($exception instanceof PhutilAggregateException) {
$caught_exception = false;
foreach ($exception->getExceptions() as $ex) {
if ($ex instanceof ArcanistUsageException) {
$this->assertSkipped($ex->getMessage());
} else {
$caught_exception = true;
}
}
}
$exception_message = $exception->getMessage()."\n\n".
$exception->getTraceAsString();
}
switch ($basename) {
default:
$this->assertEqual(false, $caught_exception, $exception_message);
$this->compareLint($basename, $expect, $result);
$this->compareTransform($xform, $after_lint);
break;
}
}
private function compareLint($file, $expect, $result) {
$seen = array();
$raised = array();
$message_map = array();
foreach ($result->getMessages() as $message) {
$sev = $message->getSeverity();
$line = $message->getLine();
$char = $message->getChar();
$code = $message->getCode();
$name = $message->getName();
$message_key = $sev.":".$line.":".$char;
$message_map[$message_key] = $message;
$seen[] = $message_key;
$raised[] = " {$sev} at line {$line}, char {$char}: {$code} {$name}";
}
$expect = trim($expect);
if ($expect) {
$expect = explode("\n", $expect);
} else {
$expect = array();
}
foreach ($expect as $key => $expected) {
$expect[$key] = head(explode(' ', $expected));
}
$expect = array_fill_keys($expect, true);
$seen = array_fill_keys($seen, true);
if (!$raised) {
$raised = array("No messages.");
}
$raised = "Actually raised:\n".implode("\n", $raised);
foreach (array_diff_key($expect, $seen) as $missing => $ignored) {
list($sev, $line, $char) = explode(':', $missing);
$this->assertFailure(
"In '{$file}', ".
"expected lint to raise {$sev} on line {$line} at char {$char}, ".
"but no {$sev} was raised. {$raised}");
}
foreach (array_diff_key($seen, $expect) as $surprising => $ignored) {
$message = $message_map[$surprising];
$message_info = $message->getDescription();
list($sev, $line, $char) = explode(':', $surprising);
$this->assertFailure(
"In '{$file}', ".
"lint raised {$sev} on line {$line} at char {$char}, ".
"but nothing was expected:\n\n{$message_info}\n\n{$raised}");
}
}
protected function compareTransform($expected, $actual) {
if (!strlen($expected)) {
return;
}
$this->assertEqual(
$expected,
$actual,
"File as patched by lint did not match the expected patched file.");
}
}
diff --git a/src/lint/linter/__tests__/ArcanistNoLintTestCase.php b/src/lint/linter/__tests__/ArcanistNoLintTestCase.php
index 8be51dbd..e88322f8 100644
--- a/src/lint/linter/__tests__/ArcanistNoLintTestCase.php
+++ b/src/lint/linter/__tests__/ArcanistNoLintTestCase.php
@@ -1,29 +1,13 @@
<?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.
- */
-
/**
* Test for @{class:ArcanistNoLintLinter}.
*
* Not a real test... meant to fail lint
* if @nolint is not respected.
*
* @group testcase
*/
class ArcanistNoLintTestCaseMisnamed extends ArcanistLinterTestCase {
}
diff --git a/src/lint/linter/__tests__/ArcanistRubyLinterTestCase.php b/src/lint/linter/__tests__/ArcanistRubyLinterTestCase.php
index b126d32e..23b47398 100644
--- a/src/lint/linter/__tests__/ArcanistRubyLinterTestCase.php
+++ b/src/lint/linter/__tests__/ArcanistRubyLinterTestCase.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Test cases for @{class:ArcanistRubyLinter}.
*
* @group testcase
*/
final class ArcanistRubyLinterTestCase extends ArcanistLinterTestCase {
public function testRubyLint() {
$linter = new ArcanistRubyLinter();
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(__FILE__);
return $this->executeTestsInDirectory(
dirname(__FILE__).'/ruby/',
$linter,
$working_copy);
}
}
diff --git a/src/lint/linter/__tests__/ArcanistSpellingLinterTestCase.php b/src/lint/linter/__tests__/ArcanistSpellingLinterTestCase.php
index 0cd4c3e4..c66ad91f 100644
--- a/src/lint/linter/__tests__/ArcanistSpellingLinterTestCase.php
+++ b/src/lint/linter/__tests__/ArcanistSpellingLinterTestCase.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Test cases for @{class:ArcanistSpellingLinter}.
*
* @group testcase
*/
final class ArcanistSpellingLinterTestCase extends ArcanistLinterTestCase {
public function testSpellingLint() {
$linter = new ArcanistSpellingLinter();
$linter->removeLintRule('acc'.'out');
$linter->addPartialWordRule('supermn', 'superman');
$linter->addWholeWordRule('batmn', 'batman');
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(__FILE__);
return $this->executeTestsInDirectory(
dirname(__FILE__).'/spelling/',
$linter,
$working_copy);
}
public function testFixLetterCase() {
$tests = array(
'tst' => 'test',
'Tst' => 'Test',
'TST' => 'TEST',
'tSt' => null,
);
foreach ($tests as $case => $expect) {
foreach (array('test', 'TEST') as $string) {
$result = ArcanistSpellingLinter::fixLetterCase($string, $case);
$this->assertEqual($expect, $result, $case);
}
}
}
}
diff --git a/src/lint/linter/__tests__/ArcanistTextLinterTestCase.php b/src/lint/linter/__tests__/ArcanistTextLinterTestCase.php
index 28b2bcb5..22bcf1de 100644
--- a/src/lint/linter/__tests__/ArcanistTextLinterTestCase.php
+++ b/src/lint/linter/__tests__/ArcanistTextLinterTestCase.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Test cases for @{class:ArcanistTextLinter}.
*
* @group testcase
*/
final class ArcanistTextLinterTestCase extends ArcanistLinterTestCase {
public function testTextLint() {
$linter = new ArcanistTextLinter();
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(__FILE__);
return $this->executeTestsInDirectory(
dirname(__FILE__).'/text/',
$linter,
$working_copy);
}
}
diff --git a/src/lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php b/src/lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php
index 4630339d..11a3eba9 100644
--- a/src/lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php
+++ b/src/lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Tests for @{class:ArcanistXHPASTLinter}.
*
* @group testcase
*/
final class ArcanistXHPASTLinterTestCase extends ArcanistLinterTestCase {
public function testXHPASTLint() {
$linter = new ArcanistXHPASTLinter();
$linter->setCustomSeverityMap(
array(
ArcanistXHPASTLinter::LINT_RAGGED_CLASSTREE_EDGE
=> ArcanistLintSeverity::SEVERITY_WARNING,
));
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(__FILE__);
return $this->executeTestsInDirectory(
dirname(__FILE__).'/xhpast/',
$linter,
$working_copy);
}
}
diff --git a/src/lint/linter/spelling/ArcanistSpellingDefaultData.php b/src/lint/linter/spelling/ArcanistSpellingDefaultData.php
index b5fa86f5..0f0a5331 100644
--- a/src/lint/linter/spelling/ArcanistSpellingDefaultData.php
+++ b/src/lint/linter/spelling/ArcanistSpellingDefaultData.php
@@ -1,549 +1,533 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Contains default spelling correction rules for ArcanistSpellingLinter.
* Inside its own file to keep logic of ArcanistSpellingLinter clean
*
* @nolint
* @group linter
*/
class ArcanistSpellingDefaultData {
// From http://cpansearch.perl.org/src/APOCAL/Pod-Spell-CommonMistakes-1.000/lib/Pod/Spell/CommonMistakes/WordList.pm
// Pruned by hand. If you modify this list, remember to remove case only
// spelling suggestions and any with special characters like - or '
public static function getFullWordRules() {
return array(
array(
// Variable common
"abandonning" => "abandoning",
"abigious" => "ambiguous",
"abitrate" => "arbitrate",
"absense" => "absence",
"absolut" => "absolute",
"absoulte" => "absolute",
"acceleratoin" => "acceleration",
"accelleration" => "acceleration",
"accesing" => "accessing",
"accesnt" => "accent",
"accessable" => "accessible",
"accidentaly" => "accidentally",
"accidentually" => "accidentally",
"accomodate" => "accommodate",
"accomodates" => "accommodates",
"accout" => "account",
"acessable" => "accessible",
"ackowledge" => "acknowledge",
"ackowledged" => "acknowledged",
"acknowldegement" => "acknowldegement",
"acording" => "according",
"activete" => "activate",
"acumulating" => "accumulating",
"addional" => "additional",
"additionaly" => "additionally",
"addreses" => "addresses",
"aditional" => "additional",
"aditionally" => "additionally",
"aditionaly" => "additionally",
"adress" => "address",
"adresses" => "addresses",
"adviced" => "advised",
"afecting" => "affecting",
"albumns" => "albums",
"alegorical" => "allegorical",
"algorith" => "algorithm",
"algorithmical" => "algorithmically",
"algoritm" => "algorithm",
"algoritms" => "algorithms",
"algorrithm" => "algorithm",
"algorritm" => "algorithm",
"allpication" => "application",
"altough" => "although",
"ambigious" => "ambiguous",
"amoung" => "among",
"amout" => "amount",
"analysator" => "analyzer",
"anniversery" => "anniversary",
"annoucement" => "announcement",
"anomolies" => "anomalies",
"anomoly" => "anomaly",
"aplication" => "application",
"appearence" => "appearance",
"appropiate" => "appropriate",
"appropriatly" => "appropriately",
"aquired" => "acquired",
"arbitary" => "arbitrary",
"architechture" => "architecture",
"arguement" => "argument",
"arguements" => "arguments",
"aritmetic" => "arithmetic",
"arraival" => "arrival",
"artifical" => "artificial",
"artillary" => "artillery",
"assigment" => "assignment",
"assigments" => "assignments",
"assistent" => "assistant",
"asuming" => "assuming",
"atomatically" => "automatically",
"attemps" => "attempts",
"attruibutes" => "attributes",
"authentification" => "authentication",
"automaticaly" => "automatically",
"automaticly" => "automatically",
"automatize" => "automate",
"automatized" => "automated",
"automatizes" => "automates",
"autonymous" => "autonomous",
"auxilliary" => "auxiliary",
"avaiable" => "available",
"availabled" => "available",
"availablity" => "availability",
"availale" => "available",
"availavility" => "availability",
"availble" => "available",
"availiable" => "available",
"avaliable" => "available",
"backgroud" => "background",
"bahavior" => "behavior",
"baloon" => "balloon",
"baloons" => "balloons",
"batery" => "battery",
"becomming" => "becoming",
"becuase" => "because",
"begining" => "beginning",
"calender" => "calendar",
"cancelation" => "cancellation",
"capabilites" => "capabilities",
"capatibilities" => "capabilities",
"cariage" => "carriage",
"challange" => "challenge",
"challanges" => "challenges",
"changable" => "changeable",
"charachter" => "character",
"charachters" => "characters",
"charcter" => "character",
"childs" => "children",
"chnage" => "change",
"chnages" => "changes",
"choosen" => "chosen",
"collapsable" => "collapsible",
"colorfull" => "colorful",
"comand" => "command",
"comit" => "commit",
"commerical" => "commercial",
"comminucation" => "communication",
"commited" => "committed",
"commiting" => "committing",
"committ" => "commit",
"commoditiy" => "commodity",
"compability" => "compatibility",
"compatability" => "compatibility",
"compatable" => "compatible",
"compatibiliy" => "compatibility",
"compatibilty" => "compatibility",
"compleatly" => "completely",
"completly" => "completely",
"complient" => "compliant",
"compres" => "compress",
"compresion" => "compression",
"configuratoin" => "configuration",
"conjuction" => "conjunction",
"connectinos" => "connections",
"connnection" => "connection",
"connnections" => "connections",
"consistancy" => "consistency",
"containes" => "contains",
"containts" => "contains",
"contence" => "contents",
"continous" => "continuous",
"continueing" => "continuing",
"contraints" => "constraints",
"convertor" => "converter",
"convinient" => "convenient",
"corected" => "corrected",
"correponding" => "corresponding",
"correponds" => "corresponds",
"correspoding" => "corresponding",
"cryptocraphic" => "cryptographic",
"curently" => "currently",
"dafault" => "default",
"deafult" => "default",
"deamon" => "daemon",
"decompres" => "decompress",
"definate" => "definite",
"definately" => "definitely",
"delemiter" => "delimiter",
"dependancies" => "dependencies",
"dependancy" => "dependency",
"dependant" => "dependent",
"desactivate" => "deactivate",
"detabase" => "database",
"developement" => "development",
"developped" => "developed",
"developpement" => "development",
"developper" => "developer",
"deveolpment" => "development",
"devided" => "divided",
"dictionnary" => "dictionary",
"diplay" => "display",
"disapeared" => "disappeared",
"discontiguous" => "noncontiguous",
"dispertion" => "dispersion",
"dissapears" => "disappears",
"docuentation" => "documentation",
"documantation" => "documentation",
"documentaion" => "documentation",
"downlad" => "download",
"downlads" => "downloads",
"easilly" => "easily",
"ecspecially" => "especially",
"edditable" => "editable",
"editting" => "editing",
"eletronic" => "electronic",
"enchanced" => "enhanced",
"encorporating" => "incorporating",
"endianess" => "endianness",
"enhaced" => "enhanced",
"enlightnment" => "enlightenment",
"enocded" => "encoded",
"enterily" => "entirely",
"enviroiment" => "environment",
"enviroment" => "environment",
"environement" => "environment",
"environent" => "environment",
"equivelant" => "equivalent",
"equivilant" => "equivalent",
"excecutable" => "executable",
"exceded" => "exceeded",
"excellant" => "excellent",
"exlcude" => "exclude",
"exlcusive" => "exclusive",
"expecially" => "especially",
"explicitely" => "explicitly",
"expresion" => "expression",
"exprimental" => "experimental",
"extention" => "extension",
"failuer" => "failure",
"familar" => "familiar",
"fatser" => "faster",
"feauture" => "feature",
"feautures" => "features",
"fetaure" => "feature",
"fetaures" => "features",
"forse" => "force",
"fortan" => "fortran",
"forwardig" => "forwarding",
"framwork" => "framework",
"fuction" => "function",
"fuctions" => "functions",
"functionaly" => "functionally",
"functionnality" => "functionality",
"functonality" => "functionality",
"futhermore" => "furthermore",
"generiously" => "generously",
"grahical" => "graphical",
"grahpical" => "graphical",
"grapic" => "graphic",
"guage" => "gauge",
"halfs" => "halves",
"heirarchically" => "hierarchically",
"helpfull" => "helpful",
"hierachy" => "hierarchy",
"hierarchie" => "hierarchy",
"howver" => "however",
"immeadiately" => "immediately",
"implemantation" => "implementation",
"implemention" => "implementation",
"incomming" => "incoming",
"incompatabilities" => "incompatibilities",
"incompatable" => "incompatible",
"inconsistant" => "inconsistent",
"indendation" => "indentation",
"indended" => "intended",
"independant" => "independent",
"informatiom" => "information",
"informations" => "information",
"infromation" => "information",
"initalize" => "initialize",
"initators" => "initiators",
"initializiation" => "initialization",
"inofficial" => "unofficial",
"integreated" => "integrated",
"integrety" => "integrity",
"integrey" => "integrity",
"intendet" => "intended",
"interchangable" => "interchangeable",
"intermittant" => "intermittent",
"interupted" => "interrupted",
"jave" => "java",
"langage" => "language",
"langauage" => "language",
"langugage" => "language",
"lauch" => "launch",
"lesstiff" => "lesstif",
"libaries" => "libraries",
"libary" => "library",
"libraris" => "libraries",
"licenceing" => "licencing",
"loggging" => "logging",
"loggin" => "login",
"logile" => "logfile",
"machinary" => "machinery",
"maintainance" => "maintenance",
"maintainence" => "maintenance",
"makeing" => "making",
"malplace" => "misplace",
"malplaced" => "misplaced",
"managable" => "manageable",
"manoeuvering" => "maneuvering",
"mathimatical" => "mathematical",
"mathimatic" => "mathematic",
"mathimatics" => "mathematics",
"ment" => "meant",
"messsage" => "message",
"messsages" => "messages",
"microprocesspr" => "microprocessor",
"milliseonds" => "milliseconds",
"miscelleneous" => "miscellaneous",
"misformed" => "malformed",
"mispelled" => "misspelled",
"mmnemonic" => "mnemonic",
"modulues" => "modules",
"monochorome" => "monochrome",
"monochromo" => "monochrome",
"monocrome" => "monochrome",
"mroe" => "more",
"multidimensionnal" => "multidimensional",
"mulitplied" => "multiplied",
"mutiple" => "multiple",
"nam" => "name",
"nams" => "names",
"navagating" => "navigating",
"nead" => "need",
"neccesary" => "necessary",
"neccessary" => "necessary",
"necesary" => "necessary",
"negotation" => "negotiation",
"nescessary" => "necessary",
"nessessary" => "necessary",
"noticable" => "noticeable",
"notications" => "notifications",
"omitt" => "omit",
"ommitted" => "omitted",
"onself" => "oneself",
"optionnal" => "optional",
"optmizations" => "optimizations",
"orientatied" => "orientated",
"orientied" => "oriented",
"ouput" => "output",
"overaall" => "overall",
"overriden" => "overridden",
"pacakge" => "package",
"pachage" => "package",
"packacge" => "package",
"packege" => "package",
"packge" => "package",
"pakage" => "package",
"pallette" => "palette",
"paramameters" => "parameters",
"paramater" => "parameter",
"parametes" => "parameters",
"paramter" => "parameter",
"paramters" => "parameters",
"particularily" => "particularly",
"pased" => "passed",
"peprocessor" => "preprocessor",
"perfoming" => "performing",
"permissons" => "permissions",
"persistant" => "persistent",
"plattform" => "platform",
"pleaes" => "please",
"ploting" => "plotting",
"posible" => "possible",
"powerfull" => "powerful",
"preceeded" => "preceded",
"preceeding" => "preceding",
"precendence" => "precedence",
"precission" => "precision",
"prefered" => "preferred",
"prefferably" => "preferably",
"prepaired" => "prepared",
"primative" => "primitive",
"princliple" => "principle",
"priorty" => "priority",
"procceed" => "proceed",
"proccesors" => "processors",
"proces" => "process",
"processessing" => "processing",
"processpr" => "processor",
"processsing" => "processing",
"progams" => "programs",
"programers" => "programmers",
"programm" => "program",
"programms" => "programs",
"promps" => "prompts",
"pronnounced" => "pronounced",
"prononciation" => "pronunciation",
"pronouce" => "pronounce",
"pronunce" => "pronounce",
"propery" => "property",
"prosess" => "process",
"protable" => "portable",
"protcol" => "protocol",
"protecion" => "protection",
"protocoll" => "protocol",
"psychadelic" => "psychedelic",
"quering" => "querying",
"reasearch" => "research",
"reasearcher" => "researcher",
"reasearchers" => "researchers",
"recogniced" => "recognised",
"recognizeable" => "recognizable",
"recommanded" => "recommended",
"redircet" => "redirect",
"redirectrion" => "redirection",
"refence" => "reference",
"registerd" => "registered",
"registraration" => "registration",
"regulamentations" => "regulations",
"remoote" => "remote",
"removeable" => "removable",
"repectively" => "respectively",
"replacments" => "replacements",
"replys" => "replies",
"requiere" => "require",
"requred" => "required",
"resizeable" => "resizable",
"ressize" => "resize",
"ressource" => "resource",
"retransmited" => "retransmitted",
"runned" => "ran",
"runnning" => "running",
"safly" => "safely",
"savable" => "saveable",
"searchs" => "searches",
"secund" => "second",
"separatly" => "separately",
"sepcify" => "specify",
"seperated" => "separated",
"seperately" => "separately",
"seperate" => "separate",
"seperatly" => "separately",
"seperator" => "separator",
"sequencial" => "sequential",
"serveral" => "several",
"setts" => "sets",
"similiar" => "similar",
"simliar" => "similar",
"speach" => "speech",
"speciefied" => "specified",
"specifed" => "specified",
"specificaton" => "specification",
"specifing" => "specifying",
"speficied" => "specified",
"speling" => "spelling",
"splitted" => "split",
"staically" => "statically",
"standardss" => "standards",
"standart" => "standard",
"staticly" => "statically",
"subdirectoires" => "subdirectories",
"suble" => "subtle",
"succesfully" => "successfully",
"succesful" => "successful",
"sucessfully" => "successfully",
"superflous" => "superfluous",
"superseeded" => "superseded",
"suplied" => "supplied",
"suport" => "support",
"suppored" => "supported",
"supportin" => "supporting",
"suppoted" => "supported",
"suppported" => "supported",
"suppport" => "support",
"surpresses" => "suppresses",
"suspicously" => "suspiciously",
"synax" => "syntax",
"synchonized" => "synchronized",
"syncronize" => "synchronize",
"syncronizing" => "synchronizing",
"syncronus" => "synchronous",
"syste" => "system",
"sythesis" => "synthesis",
"taht" => "that",
"throught" => "through",
"transfering" => "transferring",
"trasmission" => "transmission",
"treshold" => "threshold",
"trigerring" => "triggering",
"unecessary" => "unnecessary",
"unexecpted" => "unexpected",
"unfortunatelly" => "unfortunately",
"unknonw" => "unknown",
"unkown" => "unknown",
"unuseful" => "useless",
"usefull" => "useful",
"usualy" => "usually",
"utilites" => "utilities",
"utillities" => "utilities",
"utilties" => "utilities",
"utiltity" => "utility",
"utitlty" => "utility",
"variantions" => "variations",
"varient" => "variant",
"verbse" => "verbose",
"verisons" => "versions",
"verison" => "version",
"verson" => "version",
"visiters" => "visitors",
"vitual" => "virtual",
"whataver" => "whatever",
"wheter" => "whether",
"wierd" => "weird",
"yur" => "your",
// Variable common_cpan
"refering" => "referring",
"writeable" => "writable",
"nineth" => "ninth",
"ommited" => "omitted",
"omited" => "omitted",
"requrie" => "require",
"existant" => "existent",
"explict" => "explicit",
"agument" => "augument",
"destionation" => "destination",
), array(
'teh' => 'the'
)
);
}
public static function getPartialWordRules() {
return array(
array(),
array(
'recieve' => 'receive',
'uft8' => 'utf8',
'lenght' => 'length',
'heigth' => 'height',
'fuction' => 'function',
)
);
}
}
diff --git a/src/lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php b/src/lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php
index 169221b6..f53addde 100644
--- a/src/lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php
+++ b/src/lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php
@@ -1,160 +1,144 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* You can extend this class and set ##"lint.xhpast.naminghook"## in your
* ##.arcconfig## to have an opportunity to override lint results for symbol
* names.
*
* @task override Overriding Symbol Name Lint Messages
* @task util Name Utilities
* @task internal Internals
* @group lint
* @stable
*/
abstract class ArcanistXHPASTLintNamingHook {
/* -( Internals )---------------------------------------------------------- */
/**
* The constructor is final because @{class:ArcanistXHPASTLinter} is
* responsible for hook instantiation.
*
* @return this
* @task internals
*/
final public function __construct() {
// <empty>
}
/* -( Overriding Symbol Name Lint Messages )------------------------------- */
/**
* Callback invoked for each symbol, which can override the default
* determination of name validity or accept it by returning $default. The
* symbol types are: xhp-class, class, interface, function, method, parameter,
* constant, and member.
*
* For example, if you want to ban all symbols with "quack" in them and
* otherwise accept all the defaults, except allow any naming convention for
* methods with "duck" in them, you might implement the method like this:
*
* if (preg_match('/quack/i', $name)) {
* return 'Symbol names containing "quack" are forbidden.';
* }
* if ($type == 'method' && preg_match('/duck/i', $name)) {
* return null; // Always accept.
* }
* return $default;
*
* @param string The symbol type.
* @param string The symbol name.
* @param string|null The default result from the main rule engine.
* @return string|null Null to accept the name, or a message to reject it
* with. You should return the default value if you don't
* want to specifically provide an override.
* @task override
*/
abstract public function lintSymbolName($type, $name, $default);
/* -( Name Utilities )----------------------------------------------------- */
/**
* Returns true if a symbol name is UpperCamelCase.
*
* @param string Symbol name.
* @return bool True if the symbol is UpperCamelCase.
* @task util
*/
public static function isUpperCamelCase($symbol) {
return preg_match('/^[A-Z][A-Za-z0-9]*$/', $symbol);
}
/**
* Returns true if a symbol name is lowerCamelCase.
*
* @param string Symbol name.
* @return bool True if the symbol is lowerCamelCase.
* @task util
*/
public static function isLowerCamelCase($symbol) {
return preg_match('/^[a-z][A-Za-z0-9]*$/', $symbol);
}
/**
* Returns true if a symbol name is UPPERCASE_WITH_UNDERSCORES.
*
* @param string Symbol name.
* @return bool True if the symbol is UPPERCASE_WITH_UNDERSCORES.
* @task util
*/
public static function isUppercaseWithUnderscores($symbol) {
return preg_match('/^[A-Z0-9_]+$/', $symbol);
}
/**
* Returns true if a symbol name is lowercase_with_underscores.
*
* @param string Symbol name.
* @return bool True if the symbol is lowercase_with_underscores.
* @task util
*/
public static function isLowercaseWithUnderscores($symbol) {
return preg_match('/^[a-z0-9_]+$/', $symbol);
}
/**
* Strip non-name components from PHP function symbols. Notably, this discards
* the "__" magic-method signifier, to make a symbol appropriate for testing
* with methods like @{method:isLowerCamelCase}.
*
* @param string Symbol name.
* @return string Stripped symbol.
* @task util
*/
public static function stripPHPFunction($symbol) {
// Allow initial "__" for magic methods like __construct; we could also
// enumerate these explicitly.
return preg_replace('/^__/', '', $symbol);
}
/**
* Strip non-name components from PHP variable symbols. Notably, this discards
* the "$", to make a symbol appropriate for testing with methods like
* @{method:isLowercaseWithUnderscores}.
*
* @param string Symbol name.
* @return string Stripped symbol.
* @task util
*/
public static function stripPHPVariable($symbol) {
return preg_replace('/^\$/', '', $symbol);
}
}
diff --git a/src/lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php b/src/lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php
index bac8a49b..f3b0a094 100644
--- a/src/lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php
+++ b/src/lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Test cases for @{class:ArcanistXHPASTLintNamingHook}.
*
* @group testcase
*/
final class ArcanistXHPASTLintNamingHookTestCase
extends ArcanistTestCase {
public function testCaseUtilities() {
$tests = array(
'UpperCamelCase' => array(1, 0, 0, 0),
'UpperCamelCaseROFL' => array(1, 0, 0, 0),
'lowerCamelCase' => array(0, 1, 0, 0),
'lowerCamelCaseROFL' => array(0, 1, 0, 0),
'UPPERCASE_WITH_UNDERSCORES' => array(0, 0, 1, 0),
'_UPPERCASE_WITH_UNDERSCORES_' => array(0, 0, 1, 0),
'__UPPERCASE__WITH__UNDERSCORES__' => array(0, 0, 1, 0),
'lowercase_with_underscores' => array(0, 0, 0, 1),
'_lowercase_with_underscores_' => array(0, 0, 0, 1),
'__lowercase__with__underscores__' => array(0, 0, 0, 1),
'mixedCASE_NoNsEnSe' => array(0, 0, 0, 0),
);
foreach ($tests as $test => $expect) {
$this->assertEqual(
$expect[0],
ArcanistXHPASTLintNamingHook::isUpperCamelCase($test),
"UpperCamelCase: '{$test}'");
$this->assertEqual(
$expect[1],
ArcanistXHPASTLintNamingHook::isLowerCamelCase($test),
"lowerCamelCase: '{$test}'");
$this->assertEqual(
$expect[2],
ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($test),
"UPPERCASE_WITH_UNDERSCORES: '{$test}'");
$this->assertEqual(
$expect[3],
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores($test),
"lowercase_with_underscores: '{$test}'");
}
}
public function testStripUtilities() {
// Variable stripping.
$this->assertEqual(
'stuff',
ArcanistXHPASTLintNamingHook::stripPHPVariable('stuff'));
$this->assertEqual(
'stuff',
ArcanistXHPASTLintNamingHook::stripPHPVariable('$stuff'));
// Function/method stripping.
$this->assertEqual(
'construct',
ArcanistXHPASTLintNamingHook::stripPHPFunction('construct'));
$this->assertEqual(
'construct',
ArcanistXHPASTLintNamingHook::stripPHPFunction('__construct'));
}
}
diff --git a/src/lint/renderer/ArcanistLintConsoleRenderer.php b/src/lint/renderer/ArcanistLintConsoleRenderer.php
index ed4ba030..7a13deab 100644
--- a/src/lint/renderer/ArcanistLintConsoleRenderer.php
+++ b/src/lint/renderer/ArcanistLintConsoleRenderer.php
@@ -1,248 +1,232 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Shows lint messages to the user.
*
* @group lint
*/
final class ArcanistLintConsoleRenderer implements ArcanistLintRenderer {
private $showAutofixPatches = false;
public function setShowAutofixPatches($show_autofix_patches) {
$this->showAutofixPatches = $show_autofix_patches;
return $this;
}
public function renderLintResult(ArcanistLintResult $result) {
$messages = $result->getMessages();
$path = $result->getPath();
$lines = explode("\n", $result->getData());
$text = array();
foreach ($messages as $message) {
if (!$this->showAutofixPatches && $message->isAutofix()) {
continue;
}
if ($message->isError()) {
$color = 'red';
} else {
$color = 'yellow';
}
$severity = ArcanistLintSeverity::getStringForSeverity(
$message->getSeverity());
$code = $message->getCode();
$name = $message->getName();
$description = phutil_console_wrap($message->getDescription(), 4);
$text[] = phutil_console_format(
" **<bg:{$color}> %s </bg>** (%s) __%s__\n%s\n",
$severity,
$code,
$name,
$description);
if ($message->hasFileContext()) {
$text[] = $this->renderContext($message, $lines);
}
}
if ($text) {
$prefix = phutil_console_format("**>>>** Lint for __%s__:\n\n\n", $path);
return $prefix . implode("\n", $text);
} else {
return null;
}
}
protected function renderContext(
ArcanistLintMessage $message,
array $line_data) {
$lines_of_context = 3;
$out = array();
$num_lines = count($line_data);
// make line numbers line up with array indexes
array_unshift($line_data, '');
$line_num = min($message->getLine(), $num_lines);
$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();
// Refine original and replacement text to eliminate start and end in common
if ($message->isPatchable()) {
$start = $message->getChar() - 1;
$patch = $message->getReplacementText();
$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);
if ($text) {
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)
? ($message->isPatchable() ? $start : $message->getChar() - 1)
: 0,
strlen($text_line));
}
$out[] = $this->renderLine($cursor, $data, $chevron, $diff);
}
}
// Print out replacement text.
if ($message->isPatchable()) {
// Strip trailing newlines, since "explode" will create an extra patch
// line for these.
if (strlen($patch) && ($patch[strlen($patch) - 1] === "\n")) {
$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 ($text) {
$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);
for (; $cursor < $end; $cursor++) {
// If there is no original text, we didn't print out a chevron or any
// highlighted text above, so print it out here. This allows messages
// which don't have any original/replacement information to still
// render with indicator chevrons.
if ($text || $message->isPatchable()) {
$chevron = false;
} else {
$chevron = ($cursor == $line_num);
}
$out[] = $this->renderLine($cursor, $line_data[$cursor], $chevron);
// With original text, we'll render the text highlighted above. If the
// lint message only has a line/char offset there's nothing to
// highlight, so print out a caret on the next line instead.
if ($chevron && $message->getChar()) {
$out[] = $this->renderCaret($message->getChar());
}
}
$out[] = null;
return implode("\n", $out);
}
private function renderCaret($pos) {
return str_repeat(' ', 16 + $pos).'^';
}
protected function renderLine($line, $data, $chevron = false, $diff = null) {
$chevron = $chevron ? '>>>' : '';
return sprintf(
" %3s %1s %6s %s",
$chevron,
$diff,
$line,
$data);
}
public function renderOkayResult() {
return
phutil_console_format("<bg:green>** OKAY **</bg> No lint warnings.\n");
}
}
diff --git a/src/lint/renderer/ArcanistLintJSONRenderer.php b/src/lint/renderer/ArcanistLintJSONRenderer.php
index 3ee744a4..1c038841 100644
--- a/src/lint/renderer/ArcanistLintJSONRenderer.php
+++ b/src/lint/renderer/ArcanistLintJSONRenderer.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Shows lint messages to the user.
*
* @group lint
*/
final class ArcanistLintJSONRenderer implements ArcanistLintRenderer {
const LINES_OF_CONTEXT = 3;
public function renderLintResult(ArcanistLintResult $result) {
$messages = $result->getMessages();
$path = $result->getPath();
$data = explode("\n", $result->getData());
array_unshift($data, ''); // make the line numbers work as array indices
$output = array($path => array());
foreach ($messages as $message) {
$output[$path][] = array(
'code' => $message->getCode(),
'name' => $message->getName(),
'severity' => $message->getSeverity(),
'line' => $message->getLine(),
'char' => $message->getChar(),
'context' => implode("\n", array_slice(
$data,
max(1, $message->getLine() - self::LINES_OF_CONTEXT),
self::LINES_OF_CONTEXT * 2 + 1
)),
'description' => $message->getDescription(),
);
}
return json_encode($output)."\n";
}
public function renderOkayResult() {
return "";
}
}
diff --git a/src/lint/renderer/ArcanistLintLikeCompilerRenderer.php b/src/lint/renderer/ArcanistLintLikeCompilerRenderer.php
index bc75d5ba..5f4d64a0 100644
--- a/src/lint/renderer/ArcanistLintLikeCompilerRenderer.php
+++ b/src/lint/renderer/ArcanistLintLikeCompilerRenderer.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Shows lint messages to the user.
*
* @group lint
*/
final class ArcanistLintLikeCompilerRenderer implements ArcanistLintRenderer {
public function renderLintResult(ArcanistLintResult $result) {
$lines = array();
$messages = $result->getMessages();
$path = $result->getPath();
foreach ($messages as $message) {
$severity = ArcanistLintSeverity::getStringForSeverity(
$message->getSeverity());
$line = $message->getLine();
$code = $message->getCode();
$description = $message->getDescription();
$lines[] = sprintf(
"%s:%d:%s (%s) %s\n",
$path,
$line,
$severity,
$code,
$description
);
}
return implode('', $lines);
}
public function renderOkayResult() {
return "";
}
}
diff --git a/src/lint/renderer/ArcanistLintRenderer.php b/src/lint/renderer/ArcanistLintRenderer.php
index aacd4ec6..47b793fe 100644
--- a/src/lint/renderer/ArcanistLintRenderer.php
+++ b/src/lint/renderer/ArcanistLintRenderer.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Shows lint messages to the user.
*
* @group lint
*/
interface ArcanistLintRenderer {
public function renderLintResult(ArcanistLintResult $result);
public function renderOkayResult();
}
diff --git a/src/lint/renderer/ArcanistLintSummaryRenderer.php b/src/lint/renderer/ArcanistLintSummaryRenderer.php
index 36147642..bb3542ba 100644
--- a/src/lint/renderer/ArcanistLintSummaryRenderer.php
+++ b/src/lint/renderer/ArcanistLintSummaryRenderer.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Shows lint messages to the user.
*
* @group lint
*/
final class ArcanistLintSummaryRenderer implements ArcanistLintRenderer {
public function renderLintResult(ArcanistLintResult $result) {
$messages = $result->getMessages();
$path = $result->getPath();
$text = array();
$text[] = $path.":";
foreach ($messages as $message) {
$name = $message->getName();
$severity = ArcanistLintSeverity::getStringForSeverity(
$message->getSeverity());
$line = $message->getLine();
$text[] = " {$severity} on line {$line}: {$name}";
}
$text[] = null;
return implode("\n", $text);
}
public function renderOkayResult() {
return
phutil_console_format("<bg:green>** OKAY **</bg> No lint warnings.\n");
}
}
diff --git a/src/parser/ArcanistBaseCommitParser.php b/src/parser/ArcanistBaseCommitParser.php
index a6a89318..5d8a1fb9 100644
--- a/src/parser/ArcanistBaseCommitParser.php
+++ b/src/parser/ArcanistBaseCommitParser.php
@@ -1,191 +1,175 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistBaseCommitParser {
private $api;
private $try;
private $verbose = false;
public function __construct(ArcanistRepositoryAPI $api) {
$this->api = $api;
return $this;
}
private function tokenizeBaseCommitSpecification($raw_spec) {
if (!$raw_spec) {
return array();
}
$spec = preg_split('/\s*,\s*/', $raw_spec);
$spec = array_filter($spec);
foreach ($spec as $rule) {
if (strpos($rule, ':') === false) {
throw new ArcanistUsageException(
"Rule '{$rule}' is invalid, it must have a type and name like ".
"'arc:upstream'.");
}
}
return $spec;
}
private function log($message) {
if ($this->verbose) {
fwrite(STDERR, $message."\n");
}
}
public function resolveBaseCommit(array $specs) {
$specs += array(
'args' => '',
'local' => '',
'project' => '',
'global' => '',
'system' => '',
);
foreach ($specs as $source => $spec) {
$specs[$source] = self::tokenizeBaseCommitSpecification($spec);
}
$this->try = array(
'args',
'local',
'project',
'global',
'system',
);
while ($this->try) {
$source = head($this->try);
if (!idx($specs, $source)) {
$this->log("No rules left from source '{$source}'.");
array_shift($this->try);
continue;
}
$this->log("Trying rules from source '{$source}'.");
$rules = &$specs[$source];
while ($rule = array_shift($rules)) {
$this->log("Trying rule '{$rule}'.");
$commit = $this->resolveRule($rule, $source);
if ($commit === false) {
// If a rule returns false, it means to go to the next ruleset.
break;
} else if ($commit !== null) {
$this->log("Resolved commit '{$commit}' from rule '{$rule}'.");
return $commit;
}
}
}
return null;
}
/**
* Handle resolving individual rules.
*/
private function resolveRule($rule, $source) {
// NOTE: Returning `null` from this method means "no match".
// Returning `false` from this method means "stop current ruleset".
list($type, $name) = explode(':', $rule, 2);
switch ($type) {
case 'literal':
return $name;
case 'git':
case 'hg':
return $this->api->resolveBaseCommitRule($rule, $source);
case 'arc':
return $this->resolveArcRule($rule, $name, $source);
default:
throw new ArcanistUsageException(
"Base commit rule '{$rule}' (from source '{$source}') ".
"is not a recognized rule.");
}
}
/**
* Handle resolving "arc:*" rules.
*/
private function resolveArcRule($rule, $name, $source) {
switch ($name) {
case 'verbose':
$this->verbose = true;
$this->log("Enabled verbose mode.");
break;
case 'prompt':
$reason = "it is what you typed when prompted.";
$this->api->setBaseCommitExplanation($reason);
return phutil_console_prompt('Against which commit?');
case 'local':
case 'global':
case 'project':
case 'args':
case 'system':
// Push the other source on top of the list.
array_unshift($this->try, $name);
$this->log("Switching to source '{$name}'.");
return false;
case 'yield':
// Cycle this source to the end of the list.
$this->try[] = array_shift($this->try);
$this->log("Yielding processing of rules from '{$source}'.");
return false;
case 'halt':
// Dump the whole stack.
$this->try = array();
$this->log("Halting all rule processing.");
return false;
case 'skip':
return null;
case 'empty':
case 'upstream':
case 'outgoing':
case 'bookmark':
case 'amended':
return $this->api->resolveBaseCommitRule($rule, $source);
default:
$matches = null;
if (preg_match('/^exec\((.*)\)$/', $name, $matches)) {
$root = $this->api->getWorkingCopyIdentity()->getProjectRoot();
$future = new ExecFuture($matches[1]);
$future->setCWD($root);
list($err, $stdout) = $future->resolve();
if (!$err) {
return trim($stdout);
} else {
return null;
}
}
throw new ArcanistUsageException(
"Base commit rule '{$rule}' (from source '{$source}') ".
"is not a recognized rule.");
}
}
}
diff --git a/src/parser/ArcanistBundle.php b/src/parser/ArcanistBundle.php
index 4b6b0df9..cdb11edf 100644
--- a/src/parser/ArcanistBundle.php
+++ b/src/parser/ArcanistBundle.php
@@ -1,813 +1,797 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Converts changesets between different formats.
*
* @group diff
*/
final class ArcanistBundle {
private $changes;
private $conduit;
private $blobs = array();
private $diskPath;
private $projectID;
private $baseRevision;
private $revisionID;
private $encoding;
private $loadFileDataCallback;
public function setConduit(ConduitClient $conduit) {
$this->conduit = $conduit;
return $this;
}
public function setProjectID($project_id) {
$this->projectID = $project_id;
return $this;
}
public function getProjectID() {
return $this->projectID;
}
public function setBaseRevision($base_revision) {
$this->baseRevision = $base_revision;
return $this;
}
public function setEncoding($encoding) {
$this->encoding = $encoding;
return $this;
}
public function getEncoding() {
return $this->encoding;
}
public function getBaseRevision() {
return $this->baseRevision;
}
public function setRevisionID($revision_id) {
$this->revisionID = $revision_id;
return $this;
}
public function getRevisionID() {
return $this->revisionID;
}
public static function newFromChanges(array $changes) {
$obj = new ArcanistBundle();
$obj->changes = $changes;
return $obj;
}
public static function newFromArcBundle($path) {
$path = Filesystem::resolvePath($path);
$future = new ExecFuture(
csprintf(
'tar tfO %s',
$path));
list($stdout, $file_list) = $future->resolvex();
$file_list = explode("\n", trim($file_list));
if (in_array('meta.json', $file_list)) {
$future = new ExecFuture(
csprintf(
'tar xfO %s meta.json',
$path));
$meta_info = $future->resolveJSON();
$version = idx($meta_info, 'version', 0);
$project_name = idx($meta_info, 'projectName');
$base_revision = idx($meta_info, 'baseRevision');
$revision_id = idx($meta_info, 'revisionID');
$encoding = idx($meta_info, 'encoding');
// this arc bundle was probably made before we started storing meta info
} else {
$version = 0;
$project_name = null;
$base_revision = null;
$revision_id = null;
$encoding = null;
}
$future = new ExecFuture(
csprintf(
'tar xfO %s changes.json',
$path));
$changes = $future->resolveJSON();
foreach ($changes as $change_key => $change) {
foreach ($change['hunks'] as $key => $hunk) {
list($hunk_data) = execx('tar xfO %s hunks/%s', $path, $hunk['corpus']);
$changes[$change_key]['hunks'][$key]['corpus'] = $hunk_data;
}
}
foreach ($changes as $change_key => $change) {
$changes[$change_key] = ArcanistDiffChange::newFromDictionary($change);
}
$obj = new ArcanistBundle();
$obj->changes = $changes;
$obj->diskPath = $path;
$obj->setProjectID($project_name);
$obj->setBaseRevision($base_revision);
$obj->setRevisionID($revision_id);
$obj->setEncoding($encoding);
return $obj;
}
public static function newFromDiff($data) {
$obj = new ArcanistBundle();
$parser = new ArcanistDiffParser();
$obj->changes = $parser->parseDiff($data);
return $obj;
}
private function __construct() {
}
public function writeToDisk($path) {
$changes = $this->getChanges();
$change_list = array();
foreach ($changes as $change) {
$change_list[] = $change->toDictionary();
}
$hunks = array();
foreach ($change_list as $change_key => $change) {
foreach ($change['hunks'] as $key => $hunk) {
$hunks[] = $hunk['corpus'];
$change_list[$change_key]['hunks'][$key]['corpus'] = count($hunks) - 1;
}
}
$blobs = array();
foreach ($change_list as $change) {
if (!empty($change['metadata']['old:binary-phid'])) {
$blobs[$change['metadata']['old:binary-phid']] = null;
}
if (!empty($change['metadata']['new:binary-phid'])) {
$blobs[$change['metadata']['new:binary-phid']] = null;
}
}
foreach ($blobs as $phid => $null) {
$blobs[$phid] = $this->getBlob($phid);
}
$meta_info = array(
'version' => 3,
'projectName' => $this->getProjectID(),
'baseRevision' => $this->getBaseRevision(),
'revisionID' => $this->getRevisionID(),
'encoding' => $this->getEncoding(),
);
$dir = Filesystem::createTemporaryDirectory();
Filesystem::createDirectory($dir.'/hunks');
Filesystem::createDirectory($dir.'/blobs');
Filesystem::writeFile($dir.'/changes.json', json_encode($change_list));
Filesystem::writeFile($dir.'/meta.json', json_encode($meta_info));
foreach ($hunks as $key => $hunk) {
Filesystem::writeFile($dir.'/hunks/'.$key, $hunk);
}
foreach ($blobs as $key => $blob) {
Filesystem::writeFile($dir.'/blobs/'.$key, $blob);
}
execx(
'(cd %s; tar -czf %s *)',
$dir,
Filesystem::resolvePath($path));
Filesystem::remove($dir);
}
public function toUnifiedDiff() {
$result = array();
$changes = $this->getChanges();
foreach ($changes as $change) {
$hunk_changes = $this->buildHunkChanges($change->getHunks());
if (!$hunk_changes) {
continue;
}
$old_path = $this->getOldPath($change);
$cur_path = $this->getCurrentPath($change);
$index_path = $cur_path;
if ($index_path === null) {
$index_path = $old_path;
}
$result[] = 'Index: '.$index_path;
$result[] = PHP_EOL;
$result[] = str_repeat('=', 67);
$result[] = PHP_EOL;
if ($old_path === null) {
$old_path = '/dev/null';
}
if ($cur_path === null) {
$cur_path = '/dev/null';
}
// When the diff is used by `patch`, `patch` ignores what is listed as the
// current path and just makes changes to the file at the old path (unless
// the current path is '/dev/null'.
// If the old path and the current path aren't the same (and neither is
// /dev/null), this indicates the file was moved or copied. By listing
// both paths as the new file, `patch` will apply the diff to the new
// file.
if ($cur_path !== '/dev/null' && $old_path !== '/dev/null') {
$old_path = $cur_path;
}
$result[] = '--- '.$old_path.PHP_EOL;
$result[] = '+++ '.$cur_path.PHP_EOL;
$result[] = $hunk_changes;
}
if (!$result) {
return '';
}
$diff = implode('', $result);
return $this->convertNonUTF8Diff($diff);
}
public function toGitPatch() {
$result = array();
$changes = $this->getChanges();
$binary_sources = array();
foreach ($changes as $change) {
if (!$this->isGitBinaryChange($change)) {
continue;
}
$type = $change->getType();
if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY ||
$type == ArcanistDiffChangeType::TYPE_COPY_AWAY ||
$type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
foreach ($change->getAwayPaths() as $path) {
$binary_sources[$path] = $change;
}
}
}
foreach (array_keys($changes) as $multicopy_key) {
$multicopy_change = $changes[$multicopy_key];
$type = $multicopy_change->getType();
if ($type != ArcanistDiffChangeType::TYPE_MULTICOPY) {
continue;
}
// Decompose MULTICOPY into one MOVE_HERE and several COPY_HERE because
// we need more information than we have in order to build a delete patch
// and represent it as a bunch of COPY_HERE plus a delete. For details,
// see T419.
// Basically, MULTICOPY means there are 2 or more corresponding COPY_HERE
// changes, so find one of them arbitrarily and turn it into a MOVE_HERE.
// TODO: We might be able to do this more cleanly after T230 is resolved.
$decompose_okay = false;
foreach ($changes as $change_key => $change) {
if ($change->getType() != ArcanistDiffChangeType::TYPE_COPY_HERE) {
continue;
}
if ($change->getOldPath() != $multicopy_change->getCurrentPath()) {
continue;
}
$decompose_okay = true;
$change = clone $change;
$change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE);
$changes[$change_key] = $change;
// The multicopy is now fully represented by MOVE_HERE plus one or more
// COPY_HERE, so throw it away.
unset($changes[$multicopy_key]);
break;
}
if (!$decompose_okay) {
throw new Exception(
"Failed to decompose multicopy changeset in order to generate diff.");
}
}
foreach ($changes as $change) {
$type = $change->getType();
$file_type = $change->getFileType();
if ($file_type == ArcanistDiffChangeType::FILE_DIRECTORY) {
// TODO: We should raise a FYI about this, so the user is aware
// that we omitted it, if the directory is empty or has permissions
// which git can't represent.
// Git doesn't support empty directories, so we simply ignore them. If
// the directory is nonempty, 'git apply' will create it when processing
// the changesets for files inside it.
continue;
}
if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) {
// Git will apply this in the corresponding MOVE_HERE.
continue;
}
$old_mode = idx($change->getOldProperties(), 'unix:filemode', '100644');
$new_mode = idx($change->getNewProperties(), 'unix:filemode', '100644');
$is_binary = $this->isGitBinaryChange($change);
if ($is_binary) {
$old_binary = idx($binary_sources, $this->getCurrentPath($change));
$change_body = $this->buildBinaryChange($change, $old_binary);
} else {
$change_body = $this->buildHunkChanges($change->getHunks());
}
if ($type == ArcanistDiffChangeType::TYPE_COPY_AWAY) {
// TODO: This is only relevant when patching old Differential diffs
// which were created prior to arc pruning TYPE_COPY_AWAY for files
// with no modifications.
if (!strlen($change_body) && ($old_mode == $new_mode)) {
continue;
}
}
$old_path = $this->getOldPath($change);
$cur_path = $this->getCurrentPath($change);
if ($old_path === null) {
$old_index = 'a/'.$cur_path;
$old_target = '/dev/null';
} else {
$old_index = 'a/'.$old_path;
$old_target = 'a/'.$old_path;
}
if ($cur_path === null) {
$cur_index = 'b/'.$old_path;
$cur_target = '/dev/null';
} else {
$cur_index = 'b/'.$cur_path;
$cur_target = 'b/'.$cur_path;
}
$result[] = "diff --git {$old_index} {$cur_index}".PHP_EOL;
if ($type == ArcanistDiffChangeType::TYPE_ADD) {
$result[] = "new file mode {$new_mode}".PHP_EOL;
}
if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE ||
$type == ArcanistDiffChangeType::TYPE_MOVE_HERE ||
$type == ArcanistDiffChangeType::TYPE_COPY_AWAY) {
if ($old_mode !== $new_mode) {
$result[] = "old mode {$old_mode}".PHP_EOL;
$result[] = "new mode {$new_mode}".PHP_EOL;
}
}
if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) {
$result[] = "copy from {$old_path}".PHP_EOL;
$result[] = "copy to {$cur_path}".PHP_EOL;
} else if ($type == ArcanistDiffChangeType::TYPE_MOVE_HERE) {
$result[] = "rename from {$old_path}".PHP_EOL;
$result[] = "rename to {$cur_path}".PHP_EOL;
} else if ($type == ArcanistDiffChangeType::TYPE_DELETE ||
$type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
$old_mode = idx($change->getOldProperties(), 'unix:filemode');
if ($old_mode) {
$result[] = "deleted file mode {$old_mode}".PHP_EOL;
}
}
if ($change_body) {
if (!$is_binary) {
$result[] = "--- {$old_target}".PHP_EOL;
$result[] = "+++ {$cur_target}".PHP_EOL;
}
$result[] = $change_body;
}
}
$diff = implode('', $result).PHP_EOL;
return $this->convertNonUTF8Diff($diff);
}
private function isGitBinaryChange(ArcanistDiffChange $change) {
$file_type = $change->getFileType();
return ($file_type == ArcanistDiffChangeType::FILE_BINARY ||
$file_type == ArcanistDiffChangeType::FILE_IMAGE);
}
private function convertNonUTF8Diff($diff) {
if ($this->encoding) {
$diff = phutil_utf8_convert($diff, $this->encoding, 'UTF-8');
}
return $diff;
}
public function getChanges() {
return $this->changes;
}
private function breakHunkIntoSmallHunks(ArcanistDiffHunk $base_hunk) {
$context = 3;
$results = array();
$lines = phutil_split_lines($base_hunk->getCorpus());
$n = count($lines);
$old_offset = $base_hunk->getOldOffset();
$new_offset = $base_hunk->getNewOffset();
$ii = 0;
$jj = 0;
while ($ii < $n) {
// Skip lines until we find the next line with changes. Note: this skips
// both ' ' (no changes) and '\' (no newline at end of file) lines. If we
// don't skip the latter, we may incorrectly generate a terminal hunk
// that has no actual change information when a file doesn't have a
// terminal newline and not changed near the end of the file. 'patch' will
// fail to apply the diff if we generate a hunk that does not actually
// contain changes.
for ($jj = $ii; $jj < $n; ++$jj) {
$char = $lines[$jj][0];
if ($char == '-' || $char == '+') {
break;
}
}
if ($jj >= $n) {
break;
}
$hunk_start = max($jj - $context, 0);
// NOTE: There are two tricky considerations here.
// We can not generate a patch with overlapping hunks, or 'git apply'
// rejects it after 1.7.3.4.
// We can not generate a patch with too much trailing context, or
// 'patch' rejects it.
// So we need to ensure that we generate disjoint hunks, but don't
// generate any hunks with too much context.
$old_lines = 0;
$new_lines = 0;
$hunk_adjust = 0;
$last_change = $jj;
$break_here = null;
for (; $jj < $n; ++$jj) {
if ($lines[$jj][0] == ' ') {
if ($jj - $last_change > $context) {
if ($break_here === null) {
// We haven't seen a change in $context lines, so this is a
// potential place to break the hunk. However, we need to keep
// looking in case there is another change fewer than $context
// lines away, in which case we have to merge the hunks.
$break_here = $jj;
}
}
if ($jj - $last_change > (($context + 1) * 2)) {
// We definitely aren't going to merge this with the next hunk, so
// break out of the loop. We'll end the hunk at $break_here.
break;
}
} else {
$break_here = null;
$last_change = $jj;
if ($lines[$jj][0] == '\\') {
// When we have a "\ No newline at end of file" line, it does not
// contribute to either hunk length.
++$hunk_adjust;
} else if ($lines[$jj][0] == '-') {
++$old_lines;
} else if ($lines[$jj][0] == '+') {
++$new_lines;
}
}
}
if ($break_here !== null) {
$jj = $break_here;
}
$hunk_length = min($jj, $n) - $hunk_start;
$count_length = ($hunk_length - $hunk_adjust);
$hunk = new ArcanistDiffHunk();
$hunk->setOldOffset($old_offset + $hunk_start - $ii);
$hunk->setNewOffset($new_offset + $hunk_start - $ii);
$hunk->setOldLength($count_length - $new_lines);
$hunk->setNewLength($count_length - $old_lines);
$corpus = array_slice($lines, $hunk_start, $hunk_length);
$corpus = implode('', $corpus);
$hunk->setCorpus($corpus);
$results[] = $hunk;
$old_offset += ($jj - $ii) - $new_lines;
$new_offset += ($jj - $ii) - $old_lines;
$ii = $jj;
}
return $results;
}
private function getOldPath(ArcanistDiffChange $change) {
$old_path = $change->getOldPath();
$type = $change->getType();
if (!strlen($old_path) ||
$type == ArcanistDiffChangeType::TYPE_ADD) {
$old_path = null;
}
return $old_path;
}
private function getCurrentPath(ArcanistDiffChange $change) {
$cur_path = $change->getCurrentPath();
$type = $change->getType();
if (!strlen($cur_path) ||
$type == ArcanistDiffChangeType::TYPE_DELETE ||
$type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
$cur_path = null;
}
return $cur_path;
}
private function buildHunkChanges(array $hunks) {
assert_instances_of($hunks, 'ArcanistDiffHunk');
$result = array();
foreach ($hunks as $hunk) {
$small_hunks = $this->breakHunkIntoSmallHunks($hunk);
foreach ($small_hunks as $small_hunk) {
$o_off = $small_hunk->getOldOffset();
$o_len = $small_hunk->getOldLength();
$n_off = $small_hunk->getNewOffset();
$n_len = $small_hunk->getNewLength();
$corpus = $small_hunk->getCorpus();
// NOTE: If the length is 1 it can be omitted. Since git does this,
// we also do it so that "arc export --git" diffs are as similar to
// real git diffs as possible, which helps debug issues.
if ($o_len == 1) {
$o_head = "{$o_off}";
} else {
$o_head = "{$o_off},{$o_len}";
}
if ($n_len == 1) {
$n_head = "{$n_off}";
} else {
$n_head = "{$n_off},{$n_len}";
}
$result[] = "@@ -{$o_head} +{$n_head} @@".PHP_EOL;
$result[] = $corpus;
$last = substr($corpus, -1);
if ($last !== false && $last != "\r" && $last != "\n") {
$result[] = PHP_EOL;
}
}
}
return implode('', $result);
}
public function setLoadFileDataCallback($callback) {
$this->loadFileDataCallback = $callback;
return $this;
}
private function getBlob($phid, $name = null) {
if ($this->loadFileDataCallback) {
return call_user_func($this->loadFileDataCallback, $phid);
}
if ($this->diskPath) {
list($blob_data) = execx('tar xfO %s blobs/%s', $this->diskPath, $phid);
return $blob_data;
}
$console = PhutilConsole::getConsole();
if ($this->conduit) {
if ($name) {
$console->writeErr("Downloading binary data for '%s'...\n", $name);
} else {
$console->writeErr("Downloading binary data...\n");
}
$data_base64 = $this->conduit->callMethodSynchronous(
'file.download',
array(
'phid' => $phid,
));
return base64_decode($data_base64);
}
throw new Exception("Nowhere to load blob '{$phid}' from!");
}
private function buildBinaryChange(ArcanistDiffChange $change, $old_binary) {
// In Git, when we write out a binary file move or copy, we need the
// original binary for the source and the current binary for the
// destination.
if ($old_binary) {
if ($old_binary->getOriginalFileData() !== null) {
$old_data = $old_binary->getOriginalFileData();
$old_phid = null;
} else {
$old_data = null;
$old_binary->getMetadata('old:binary-phid');
}
} else {
$old_data = $change->getOriginalFileData();
$old_phid = $change->getMetadata('old:binary-phid');
}
if ($old_data === null && $old_phid) {
$name = basename($change->getOldPath());
$old_data = $this->getBlob($old_phid, $name);
}
$old_length = strlen($old_data);
if ($old_data === null) {
$old_data = '';
$old_sha1 = str_repeat('0', 40);
} else {
$old_sha1 = sha1("blob {$old_length}\0{$old_data}");
}
$new_phid = $change->getMetadata('new:binary-phid');
$new_data = null;
if ($change->getCurrentFileData() !== null) {
$new_data = $change->getCurrentFileData();
} else if ($new_phid) {
$name = basename($change->getCurrentPath());
$new_data = $this->getBlob($new_phid, $name);
}
$new_length = strlen($new_data);
if ($new_data === null) {
$new_data = '';
$new_sha1 = str_repeat('0', 40);
} else {
$new_sha1 = sha1("blob {$new_length}\0{$new_data}");
}
$content = array();
$content[] = "index {$old_sha1}..{$new_sha1}".PHP_EOL;
$content[] = "GIT binary patch".PHP_EOL;
$content[] = "literal {$new_length}".PHP_EOL;
$content[] = $this->emitBinaryDiffBody($new_data).PHP_EOL;
$content[] = "literal {$old_length}".PHP_EOL;
$content[] = $this->emitBinaryDiffBody($old_data).PHP_EOL;
return implode('', $content);
}
private function emitBinaryDiffBody($data) {
if (!function_exists('gzcompress')) {
throw new Exception(
"This patch has binary data. The PHP zlib extension is required to ".
"apply patches with binary data to git. Install the PHP zlib ".
"extension to continue.");
}
// See emit_binary_diff_body() in diff.c for git's implementation.
$buf = '';
$deflated = gzcompress($data);
$lines = str_split($deflated, 52);
foreach ($lines as $line) {
$len = strlen($line);
// The first character encodes the line length.
if ($len <= 26) {
$buf .= chr($len + ord('A') - 1);
} else {
$buf .= chr($len - 26 + ord('a') - 1);
}
$buf .= self::encodeBase85($line);
$buf .= PHP_EOL;
}
return $buf;
}
public static function encodeBase85($data) {
// This is implemented awkwardly in order to closely mirror git's
// implementation in base85.c
// It is also implemeted awkwardly to work correctly on 32-bit machines.
// Broadly, this algorithm converts the binary input to printable output
// by transforming each 4 binary bytes of input to 5 printable bytes of
// output, one piece at a time.
//
// To do this, we convert the 4 bytes into a 32-bit integer, then use
// modulus and division by 85 to pick out printable bytes (85^5 is slightly
// larger than 2^32). In C, this algorithm is fairly easy to implement
// because the accumulator can be made unsigned.
//
// In PHP, there are no unsigned integers, so values larger than 2^31 break
// on 32-bit systems under modulus:
//
// $ php -r 'print (1 << 31) % 13;' # On a 32-bit machine.
// -11
//
// However, PHP's float type is an IEEE 754 64-bit double precision float,
// so we can safely store integers up to around 2^53 without loss of
// precision. To work around the lack of an unsigned type, we just use a
// double and perform the modulus with fmod().
//
// (Since PHP overflows integer operations into floats, we don't need much
// additional casting.)
static $map = array(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
'!', '#', '$', '%', '&', '(', ')', '*', '+', '-',
';', '<', '=', '>', '?', '@', '^', '_', '`', '{',
'|', '}', '~',
);
$buf = '';
$pos = 0;
$bytes = strlen($data);
while ($bytes) {
$accum = 0;
for ($count = 24; $count >= 0; $count -= 8) {
$val = ord($data[$pos++]);
$val = $val * (1 << $count);
$accum = $accum + $val;
if (--$bytes == 0) {
break;
}
}
$slice = '';
for ($count = 4; $count >= 0; $count--) {
$val = (int)fmod($accum, 85.0);
$accum = floor($accum / 85.0);
$slice .= $map[$val];
}
$buf .= strrev($slice);
}
return $buf;
}
}
diff --git a/src/parser/ArcanistCommentRemover.php b/src/parser/ArcanistCommentRemover.php
index ee06db52..705f7a20 100644
--- a/src/parser/ArcanistCommentRemover.php
+++ b/src/parser/ArcanistCommentRemover.php
@@ -1,44 +1,28 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistCommentRemover {
/**
* Remove comment lines from a commit message. Strips trailing lines only,
* and requires "#" to appear at the beginning of a line for it to be
* considered a comment.
*/
public static function removeComments($body) {
$lines = explode("\n", $body);
$lines = array_reverse($lines);
foreach ($lines as $key => $line) {
if (!strlen($line)) {
unset($lines[$key]);
continue;
}
if ($line[0] == '#') {
unset($lines[$key]);
continue;
}
break;
}
$lines = array_reverse($lines);
return implode("\n", $lines)."\n";
}
}
diff --git a/src/parser/ArcanistDiffParser.php b/src/parser/ArcanistDiffParser.php
index f75e8166..4e266973 100644
--- a/src/parser/ArcanistDiffParser.php
+++ b/src/parser/ArcanistDiffParser.php
@@ -1,1291 +1,1275 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Parses diffs from a working copy.
*
* @group diff
*/
final class ArcanistDiffParser {
protected $repositoryAPI;
protected $text;
protected $line;
protected $lineSaved;
protected $isGit;
protected $isMercurial;
protected $detectBinaryFiles = false;
protected $tryEncoding;
protected $rawDiff;
protected $writeDiffOnFailure;
protected $changes = array();
private $forcePath;
public function setRepositoryAPI(ArcanistRepositoryAPI $repository_api) {
$this->repositoryAPI = $repository_api;
return $this;
}
public function setDetectBinaryFiles($detect) {
$this->detectBinaryFiles = $detect;
return $this;
}
public function setTryEncoding($encoding) {
$this->tryEncoding = $encoding;
return $this;
}
public function forcePath($path) {
$this->forcePath = $path;
return $this;
}
public function setChanges(array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$this->changes = mpull($changes, null, 'getCurrentPath');
return $this;
}
public function parseSubversionDiff(ArcanistSubversionAPI $api, $paths) {
$this->setRepositoryAPI($api);
$diffs = array();
foreach ($paths as $path => $status) {
if ($status & ArcanistRepositoryAPI::FLAG_UNTRACKED ||
$status & ArcanistRepositoryAPI::FLAG_CONFLICT ||
$status & ArcanistRepositoryAPI::FLAG_MISSING) {
unset($paths[$path]);
}
}
$root = null;
$from = array();
foreach ($paths as $path => $status) {
$change = $this->buildChange($path);
if ($status & ArcanistRepositoryAPI::FLAG_ADDED) {
$change->setType(ArcanistDiffChangeType::TYPE_ADD);
} else if ($status & ArcanistRepositoryAPI::FLAG_DELETED) {
$change->setType(ArcanistDiffChangeType::TYPE_DELETE);
} else {
$change->setType(ArcanistDiffChangeType::TYPE_CHANGE);
}
$is_dir = is_dir($api->getPath($path));
if ($is_dir) {
$change->setFileType(ArcanistDiffChangeType::FILE_DIRECTORY);
// We have to go hit the diff even for directories because they may
// have property changes or moves, etc.
}
$is_link = is_link($api->getPath($path));
if ($is_link) {
$change->setFileType(ArcanistDiffChangeType::FILE_SYMLINK);
}
$diff = $api->getRawDiffText($path);
if ($diff) {
$this->parseDiff($diff);
}
$info = $api->getSVNInfo($path);
if (idx($info, 'Copied From URL')) {
if (!$root) {
$rinfo = $api->getSVNInfo('.');
$root = $rinfo['URL'].'/';
}
$cpath = $info['Copied From URL'];
$root_len = strlen($root);
if (!strncmp($cpath, $root, $root_len)) {
$cpath = substr($cpath, $root_len);
// The user can "svn cp /path/to/file@12345 x", which pulls a file out
// of version history at a specific revision. If we just use the path,
// we'll collide with possible changes to that path in the working
// copy below. In particular, "svn cp"-ing a path which no longer
// exists somewhere in the working copy and then adding that path
// gets us to the "origin change type" branches below with a
// TYPE_ADD state on the path. To avoid this, append the origin
// revision to the path so we'll necessarily generate a new change.
// TODO: In theory, you could have an '@' in your path and this could
// cause a collision, e.g. two files named 'f' and 'f@12345'. This is
// at least somewhat the user's fault, though.
if ($info['Copied From Rev']) {
if ($info['Copied From Rev'] != $info['Revision']) {
$cpath .= '@'.$info['Copied From Rev'];
}
}
$change->setOldPath($cpath);
$from[$path] = $cpath;
}
}
$type = $change->getType();
if (($type === ArcanistDiffChangeType::TYPE_MOVE_AWAY ||
$type === ArcanistDiffChangeType::TYPE_DELETE) &&
idx($info, 'Node Kind') === 'directory') {
$change->setFileType(ArcanistDiffChangeType::FILE_DIRECTORY);
}
}
foreach ($paths as $path => $status) {
$change = $this->buildChange($path);
if (empty($from[$path])) {
continue;
}
if (empty($this->changes[$from[$path]])) {
if ($change->getType() == ArcanistDiffChangeType::TYPE_COPY_HERE) {
// If the origin path wasn't changed (or isn't included in this diff)
// and we only copied it, don't generate a changeset for it. This
// keeps us out of trouble when we go to 'arc commit' and need to
// figure out which files should be included in the commit list.
continue;
}
}
$origin = $this->buildChange($from[$path]);
$origin->addAwayPath($change->getCurrentPath());
$type = $origin->getType();
switch ($type) {
case ArcanistDiffChangeType::TYPE_MULTICOPY:
case ArcanistDiffChangeType::TYPE_COPY_AWAY:
// "Add" is possible if you do some bizarre tricks with svn:ignore and
// "svn copy"'ing URLs straight from the repository; you can end up with
// a file that is a copy of itself. See T271.
case ArcanistDiffChangeType::TYPE_ADD:
break;
case ArcanistDiffChangeType::TYPE_DELETE:
$origin->setType(ArcanistDiffChangeType::TYPE_MOVE_AWAY);
break;
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
$origin->setType(ArcanistDiffChangeType::TYPE_MULTICOPY);
break;
case ArcanistDiffChangeType::TYPE_CHANGE:
$origin->setType(ArcanistDiffChangeType::TYPE_COPY_AWAY);
break;
default:
throw new Exception("Bad origin state {$type}.");
}
$type = $origin->getType();
switch ($type) {
case ArcanistDiffChangeType::TYPE_MULTICOPY:
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
$change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE);
break;
case ArcanistDiffChangeType::TYPE_ADD:
case ArcanistDiffChangeType::TYPE_COPY_AWAY:
$change->setType(ArcanistDiffChangeType::TYPE_COPY_HERE);
break;
default:
throw new Exception("Bad origin state {$type}.");
}
}
return $this->changes;
}
public function parseDiff($diff) {
$this->didStartParse($diff);
if ($this->getLine() === null) {
$this->didFailParse("Can't parse an empty diff!");
}
do {
$patterns = array(
// This is a normal SVN text change, probably from "svn diff".
'(?P<type>Index): (?P<cur>.+)',
// This is an SVN property change, probably from "svn diff".
'(?P<type>Property changes on): (?P<cur>.+)',
// This is a git commit message, probably from "git show".
'(?P<type>commit) (?P<hash>[a-f0-9]+)(?: \(.*\))?',
// This is a git diff, probably from "git show" or "git diff".
// Note that the filenames may appear quoted.
'(?P<type>diff --git) (?P<oldnew>.*)',
// This is a unified diff, probably from "diff -u" or synthetic diffing.
'(?P<type>---) (?P<old>.+)\s+\d{4}-\d{2}-\d{2}.*',
'(?P<binary>Binary) files '.
'(?P<old>.+)\s+\d{4}-\d{2}-\d{2} and '.
'(?P<new>.+)\s+\d{4}-\d{2}-\d{2} differ.*',
// This is a normal Mercurial text change, probably from "hg diff". It
// may have two "-r" blocks if it came from "hg diff -r x:y".
'(?P<type>diff -r) (?P<hgrev>[a-f0-9]+) (?:-r [a-f0-9]+ )?(?P<cur>.+)',
);
$line = $this->getLineTrimmed();
$match = null;
$ok = $this->tryMatchHeader($patterns, $line, $match);
$failed_parse = false;
if (!$ok && $this->isFirstNonEmptyLine()) {
// 'hg export' command creates so called "extended diff" that
// contains some meta information and comment at the beginning
// (isFirstNonEmptyLine() to check for beginning). Actual mercurial
// code detects where comment ends and unified diff starts by
// searching "diff -r" in the text.
$this->saveLine();
$line = $this->nextLineThatLooksLikeDiffStart();
if (!$this->tryMatchHeader($patterns, $line, $match)) {
// Restore line before guessing to display correct error.
$this->restoreLine();
$failed_parse = true;
}
} else if (!$ok) {
$failed_parse = true;
}
if ($failed_parse) {
$this->didFailParse(
"Expected a hunk header, like 'Index: /path/to/file.ext' (svn), ".
"'Property changes on: /path/to/file.ext' (svn properties), ".
"'commit 59bcc3ad6775562f845953cf01624225' (git show), ".
"'diff --git' (git diff), '--- filename' (unified diff), or " .
"'diff -r' (hg diff or patch).");
}
if (isset($match['type'])) {
if ($match['type'] == 'diff --git') {
list($old, $new) = self::splitGitDiffPaths($match['oldnew']);
$match['old'] = $old;
$match['cur'] = $new;
}
}
$change = $this->buildChange(idx($match, 'cur'));
if (isset($match['old'])) {
$change->setOldPath($match['old']);
}
if (isset($match['hash'])) {
$change->setCommitHash($match['hash']);
}
if (isset($match['binary'])) {
$change->setFileType(ArcanistDiffChangeType::FILE_BINARY);
$line = $this->nextNonemptyLine();
continue;
}
$line = $this->nextLine();
switch ($match['type']) {
case 'Index':
$this->parseIndexHunk($change);
break;
case 'Property changes on':
$this->parsePropertyHunk($change);
break;
case 'diff --git':
$this->setIsGit(true);
$this->parseIndexHunk($change);
break;
case 'commit':
$this->setIsGit(true);
$this->parseCommitMessage($change);
break;
case '---':
$ok = preg_match(
'@^(?:\+\+\+) (.*)\s+\d{4}-\d{2}-\d{2}.*$@',
$line,
$match);
if (!$ok) {
$this->didFailParse("Expected '+++ filename' in unified diff.");
}
$change->setCurrentPath($match[1]);
$line = $this->nextLine();
$this->parseChangeset($change);
break;
case 'diff -r':
$this->setIsMercurial(true);
$this->parseIndexHunk($change);
break;
default:
$this->didFailParse("Unknown diff type.");
break;
}
} while ($this->getLine() !== null);
$this->didFinishParse();
$this->loadSyntheticData();
return $this->changes;
}
protected function tryMatchHeader($patterns, $line, &$match) {
foreach ($patterns as $pattern) {
if (preg_match('@^'.$pattern.'$@', $line, $match)) {
return true;
}
}
return false;
}
protected function parseCommitMessage(ArcanistDiffChange $change) {
$change->setType(ArcanistDiffChangeType::TYPE_MESSAGE);
$message = array();
$line = $this->getLine();
if (preg_match('/^Merge: /', $line)) {
$this->nextLine();
}
$line = $this->getLine();
if (!preg_match('/^Author: /', $line)) {
$this->didFailParse("Expected 'Author:'.");
}
$line = $this->nextLine();
if (!preg_match('/^Date: /', $line)) {
$this->didFailParse("Expected 'Date:'.");
}
while (($line = $this->nextLineTrimmed()) !== null) {
if (strlen($line) && $line[0] != ' ') {
break;
}
// Strip leading spaces from Git commit messages. Note that empty lines
// are represented as just "\n"; don't touch those.
$message[] = preg_replace('/^ /', '', $this->getLine());
}
$message = rtrim(implode('', $message), "\r\n");
$change->setMetadata('message', $message);
}
/**
* Parse an SVN property change hunk. These hunks are ambiguous so just sort
* of try to get it mostly right. It's entirely possible to foil this parser
* (or any other parser) with a carefully constructed property change.
*/
protected function parsePropertyHunk(ArcanistDiffChange $change) {
$line = $this->getLine();
if (!preg_match('/^_+$/', $line)) {
$this->didFailParse("Expected '______________________'.");
}
$line = $this->nextLine();
while ($line !== null) {
$done = preg_match('/^(Index|Property changes on):/', $line);
if ($done) {
break;
}
$matches = null;
$ok = preg_match('/^(Modified|Added|Deleted): (.*)$/', $line, $matches);
if (!$ok) {
$this->didFailParse("Expected 'Added', 'Deleted', or 'Modified'.");
}
$op = $matches[1];
$prop = $matches[2];
list($old, $new) = $this->parseSVNPropertyChange($op, $prop);
if ($old !== null) {
$change->setOldProperty($prop, $old);
}
if ($new !== null) {
$change->setNewProperty($prop, $new);
}
$line = $this->getLine();
}
}
private function parseSVNPropertyChange($op, $prop) {
$old = array();
$new = array();
$target = null;
$line = $this->nextLine();
$prop_index = 2;
while ($line !== null) {
$done = preg_match(
'/^(Modified|Added|Deleted|Index|Property changes on):/',
$line);
if ($done) {
break;
}
$trimline = ltrim($line);
if ($trimline && $trimline[0] == '#') {
// in svn1.7, a line like ## -0,0 +1 ## is put between the Added: line
// and the line with the property change. If we have such a line, we'll
// just ignore it (:
$line = $this->nextLine();
$prop_index = 1;
$trimline = ltrim($line);
}
if ($trimline && $trimline[0] == '+') {
if ($op == 'Deleted') {
$this->didFailParse('Unexpected "+" section in property deletion.');
}
$target = 'new';
$line = substr($trimline, $prop_index);
} else if ($trimline && $trimline[0] == '-') {
if ($op == 'Added') {
$this->didFailParse('Unexpected "-" section in property addition.');
}
$target = 'old';
$line = substr($trimline, $prop_index);
} else if (!strncmp($trimline, 'Merged', 6)) {
if ($op == 'Added') {
$target = 'new';
} else {
// These can appear on merges. No idea how to interpret this (unclear
// what the old / new values are) and it's of dubious usefulness so
// just throw it away until someone complains.
$target = null;
}
$line = $trimline;
}
if ($target == 'new') {
$new[] = $line;
} else if ($target == 'old') {
$old[] = $line;
}
$line = $this->nextLine();
}
$old = rtrim(implode('', $old));
$new = rtrim(implode('', $new));
if (!strlen($old)) {
$old = null;
}
if (!strlen($new)) {
$new = null;
}
return array($old, $new);
}
protected function setIsGit($git) {
if ($this->isGit !== null && $this->isGit != $git) {
throw new Exception("Git status has changed!");
}
$this->isGit = $git;
return $this;
}
protected function getIsGit() {
return $this->isGit;
}
public function setIsMercurial($is_mercurial) {
$this->isMercurial = $is_mercurial;
return $this;
}
public function getIsMercurial() {
return $this->isMercurial;
}
protected function parseIndexHunk(ArcanistDiffChange $change) {
$is_git = $this->getIsGit();
$is_mercurial = $this->getIsMercurial();
$is_svn = (!$is_git && !$is_mercurial);
$move_source = null;
$line = $this->getLine();
if ($is_git) {
do {
$patterns = array(
'(?P<new>new) file mode (?P<newmode>\d+)',
'(?P<deleted>deleted) file mode (?P<oldmode>\d+)',
// These occur when someone uses `chmod` on a file.
'old mode (?P<oldmode>\d+)',
'new mode (?P<newmode>\d+)',
// These occur when you `mv` a file and git figures it out.
'similarity index ',
'rename from (?P<old>.*)',
'(?P<move>rename) to (?P<cur>.*)',
'copy from (?P<old>.*)',
'(?P<copy>copy) to (?P<cur>.*)'
);
$ok = false;
$match = null;
foreach ($patterns as $pattern) {
$ok = preg_match('@^'.$pattern.'@', $line, $match);
if ($ok) {
break;
}
}
if (!$ok) {
if ($line === null ||
preg_match('/^(diff --git|commit) /', $line)) {
// In this case, there are ONLY file mode changes, or this is a
// pure move. If it's a move, flag these changesets so we can build
// synthetic changes later, enabling us to show file contents in
// Differential -- git only gives us a block like this:
//
// diff --git a/README b/READYOU
// similarity index 100%
// rename from README
// rename to READYOU
//
// ...i.e., there is no associated diff.
$change->setNeedsSyntheticGitHunks(true);
if ($move_source) {
$move_source->setNeedsSyntheticGitHunks(true);
}
return;
}
break;
}
if (!empty($match['oldmode'])) {
$change->setOldProperty('unix:filemode', $match['oldmode']);
}
if (!empty($match['newmode'])) {
$change->setNewProperty('unix:filemode', $match['newmode']);
}
if (!empty($match['deleted'])) {
$change->setType(ArcanistDiffChangeType::TYPE_DELETE);
}
if (!empty($match['new'])) {
// If you replace a symlink with a normal file, git renders the change
// as a "delete" of the symlink plus an "add" of the new file. We
// prefer to represent this as a change.
if ($change->getType() == ArcanistDiffChangeType::TYPE_DELETE) {
$change->setType(ArcanistDiffChangeType::TYPE_CHANGE);
} else {
$change->setType(ArcanistDiffChangeType::TYPE_ADD);
}
}
if (!empty($match['old'])) {
$match['old'] = self::unescapeFilename($match['old']);
$change->setOldPath($match['old']);
}
if (!empty($match['cur'])) {
$match['cur'] = self::unescapeFilename($match['cur']);
$change->setCurrentPath($match['cur']);
}
if (!empty($match['copy'])) {
$change->setType(ArcanistDiffChangeType::TYPE_COPY_HERE);
$old = $this->buildChange($change->getOldPath());
$type = $old->getType();
if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) {
$old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY);
} else {
$old->setType(ArcanistDiffChangeType::TYPE_COPY_AWAY);
}
$old->addAwayPath($change->getCurrentPath());
}
if (!empty($match['move'])) {
$change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE);
$old = $this->buildChange($change->getOldPath());
$type = $old->getType();
if ($type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
// Great, no change.
} else if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) {
$old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY);
} else if ($type == ArcanistDiffChangeType::TYPE_COPY_AWAY) {
$old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY);
} else {
$old->setType(ArcanistDiffChangeType::TYPE_MOVE_AWAY);
}
// We'll reference this above.
$move_source = $old;
$old->addAwayPath($change->getCurrentPath());
}
$line = $this->nextNonemptyLine();
} while (true);
}
$line = $this->getLine();
if ($is_svn) {
$ok = preg_match('/^=+\s*$/', $line);
if (!$ok) {
$this->didFailParse("Expected '=======================' divider line.");
} else {
// Adding an empty file in SVN can produce an empty line here.
$line = $this->nextNonemptyLine();
}
} else if ($is_git) {
$ok = preg_match('/^index .*$/', $line);
if (!$ok) {
// TODO: "hg diff -g" diffs ("mercurial git-style diffs") do not include
// this line, so we can't parse them if we fail on it. Maybe introduce
// a flag saying "parse this diff using relaxed git-style diff rules"?
// $this->didFailParse("Expected 'index af23f...a98bc' header line.");
} else {
// NOTE: In the git case, where this patch is the last change in the
// file, we may have a final terminal newline. Skip over it so that
// we'll hit the '$line === null' block below. This is covered by the
// 'git-empty-file.gitdiff' test case.
$line = $this->nextNonemptyLine();
}
}
// If there are files with only whitespace changes and -b or -w are
// supplied as command-line flags to `diff', svn and git both produce
// changes without any body.
if ($line === null ||
preg_match(
'/^(Index:|Property changes on:|diff --git|commit) /',
$line)) {
return;
}
$is_binary_add = preg_match(
'/^Cannot display: file marked as a binary type\.$/',
rtrim($line));
if ($is_binary_add) {
$this->nextLine(); // Cannot display: file marked as a binary type.
$this->nextNonemptyLine(); // svn:mime-type = application/octet-stream
$this->markBinary($change);
return;
}
// We can get this in git, or in SVN when a file exists in the repository
// WITHOUT a binary mime-type and is changed and given a binary mime-type.
$is_binary_diff = preg_match(
'/^Binary files .* and .* differ$/',
rtrim($line));
if ($is_binary_diff) {
$this->nextNonemptyLine(); // Binary files x and y differ
$this->markBinary($change);
return;
}
// This occurs under "hg diff --git" when a binary file is removed. See
// test case "hg-binary-delete.hgdiff". (I believe it never occurs under
// git, which reports the "files X and /dev/null differ" string above. Git
// can not apply these patches.)
$is_hg_binary_delete = preg_match(
'/^Binary file .* has changed$/',
rtrim($line));
if ($is_hg_binary_delete) {
$this->nextNonemptyLine();
$this->markBinary($change);
return;
}
// With "git diff --binary" (not a normal mode, but one users may explicitly
// invoke and then, e.g., copy-paste into the web console) or "hg diff
// --git" (normal under hg workflows), we may encounter a literal binary
// patch.
$is_git_binary_patch = preg_match(
'/^GIT binary patch$/',
rtrim($line));
if ($is_git_binary_patch) {
$this->nextLine();
$this->parseGitBinaryPatch();
$line = $this->getLine();
if (preg_match('/^literal/', $line)) {
// We may have old/new binaries (change) or just a new binary (hg add).
// If there are two blocks, parse both.
$this->parseGitBinaryPatch();
}
$this->markBinary($change);
return;
}
if ($is_git) {
// "git diff -b" ignores whitespace, but has an empty hunk target
if (preg_match('@^diff --git .*$@', $line)) {
$this->nextLine();
return null;
}
}
$old_file = $this->parseHunkTarget();
$new_file = $this->parseHunkTarget();
$change->setOldPath($old_file);
$this->parseChangeset($change);
}
private function parseGitBinaryPatch() {
// TODO: We could decode the patches, but it's a giant mess so don't bother
// for now. We'll pick up the data from the working copy in the common
// case ("arc diff").
$line = $this->getLine();
if (!preg_match('/^literal /', $line)) {
$this->didFailParse("Expected 'literal NNNN' to start git binary patch.");
}
do {
$line = $this->nextLineTrimmed();
if ($line === '' || $line === null) {
// Some versions of Mercurial apparently omit the terminal newline,
// although it's unclear if Git will ever do this. In either case,
// rely on the base85 check for sanity.
$this->nextNonemptyLine();
return;
} else if (!preg_match('/^[a-zA-Z]/', $line)) {
$this->didFailParse("Expected base85 line length character (a-zA-Z).");
}
} while (true);
}
protected function parseHunkTarget() {
$line = $this->getLine();
$matches = null;
$remainder = '(?:\s*\(.*\))?';
if ($this->getIsMercurial()) {
// Something like "Fri Aug 26 01:20:50 2005 -0700", don't bother trying
// to parse it.
$remainder = '\t.*';
}
$ok = preg_match(
'@^[-+]{3} (?:[ab]/)?(?P<path>.*?)'.$remainder.'$@',
$line,
$matches);
if (!$ok) {
$this->didFailParse(
"Expected hunk target '+++ path/to/file.ext (revision N)'.");
}
$this->nextLine();
return $matches['path'];
}
protected function markBinary(ArcanistDiffChange $change) {
$change->setFileType(ArcanistDiffChangeType::FILE_BINARY);
return $this;
}
protected function parseChangeset(ArcanistDiffChange $change) {
$all_changes = array();
do {
$hunk = new ArcanistDiffHunk();
$line = $this->getLineTrimmed();
$real = array();
// In the case where only one line is changed, the length is omitted.
// The final group is for git, which appends a guess at the function
// context to the diff.
$matches = null;
$ok = preg_match(
'/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*?)?$/U',
$line,
$matches);
if (!$ok) {
// It's possible we hit the style of an svn1.7 property change.
// This is a 4-line Index block, followed by an empty line, followed
// by a "Property changes on:" section similar to svn1.6.
if ($line == '') {
$line = $this->nextNonemptyLine();
$ok = preg_match('/^Property changes on:/', $line);
if (!$ok) {
$this->didFailParse("Confused by empty line");
}
$line = $this->nextLine();
return $this->parsePropertyHunk($change);
}
$this->didFailParse("Expected hunk header '@@ -NN,NN +NN,NN @@'.");
}
$hunk->setOldOffset($matches[1]);
$hunk->setNewOffset($matches[3]);
// Cover for the cases where length wasn't present (implying one line).
$old_len = idx($matches, 2);
if (!strlen($old_len)) {
$old_len = 1;
}
$new_len = idx($matches, 4);
if (!strlen($new_len)) {
$new_len = 1;
}
$hunk->setOldLength($old_len);
$hunk->setNewLength($new_len);
$add = 0;
$del = 0;
$advance = false;
while ((($line = $this->nextLine()) !== null)) {
if (strlen($line)) {
$char = $line[0];
} else {
$char = '~';
}
switch ($char) {
case '\\':
if (!preg_match('@\\ No newline at end of file@', $line)) {
$this->didFailParse(
"Expected '\ No newline at end of file'.");
}
if ($new_len) {
$real[] = $line;
$hunk->setIsMissingOldNewline(true);
} else {
$real[] = $line;
$hunk->setIsMissingNewNewline(true);
}
if (!$new_len) {
$advance = true;
break 2;
}
break;
case '+':
if (!$new_len) {
break 2;
}
++$add;
--$new_len;
$real[] = $line;
break;
case '-':
if (!$old_len) {
break 2;
}
++$del;
--$old_len;
$real[] = $line;
break;
case ' ':
if (!$old_len && !$new_len) {
break 2;
}
--$old_len;
--$new_len;
$real[] = $line;
break;
case "\r":
case "\n":
case '~':
$advance = true;
break 2;
default:
break 2;
}
}
if ($old_len != 0 || $new_len != 0) {
$this->didFailParse("Found the wrong number of hunk lines.");
}
$corpus = implode('', $real);
$is_binary = false;
if ($this->detectBinaryFiles) {
$is_binary = !phutil_is_utf8($corpus);
$try_encoding = $this->tryEncoding;
if ($is_binary && $try_encoding) {
$is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus);
if (!$is_binary) {
$corpus = phutil_utf8_convert($corpus, 'UTF-8', $try_encoding);
if (!phutil_is_utf8($corpus)) {
throw new Exception(
"Failed to convert a hunk from '{$try_encoding}' to UTF-8. ".
"Check that the specified encoding is correct.");
}
}
}
}
if ($is_binary) {
// SVN happily treats binary files which aren't marked with the right
// mime type as text files. Detect that junk here and mark the file
// binary. We'll catch stuff with unicode too, but that's verboten
// anyway. If there are too many false positives with this we might
// need to make it threshold-triggered instead of triggering on any
// unprintable byte.
$change->setFileType(ArcanistDiffChangeType::FILE_BINARY);
} else {
$hunk->setCorpus($corpus);
$hunk->setAddLines($add);
$hunk->setDelLines($del);
$change->addHunk($hunk);
}
if ($advance) {
$line = $this->nextNonemptyLine();
}
} while (preg_match('/^@@ /', $line));
}
protected function buildChange($path = null) {
$change = null;
if ($path !== null) {
if (!empty($this->changes[$path])) {
return $this->changes[$path];
}
}
if ($this->forcePath) {
return $this->changes[$this->forcePath];
}
$change = new ArcanistDiffChange();
if ($path !== null) {
$change->setCurrentPath($path);
$this->changes[$path] = $change;
} else {
$this->changes[] = $change;
}
return $change;
}
protected function didStartParse($text) {
$this->rawDiff = $text;
// Eat leading whitespace. This may happen if the first change in the diff
// is an SVN property change.
$text = ltrim($text);
// Try to strip ANSI color codes from colorized diffs. ANSI color codes
// might be present in two cases:
//
// - You piped a colorized diff into 'arc --raw' or similar (normally
// we're able to disable colorization on diffs we control the generation
// of).
// - You're diffing a file which actually contains ANSI color codes.
//
// The former is vastly more likely, but we try to distinguish between the
// two cases by testing for a color code at the beginning of a line. If
// we find one, we know it's a colorized diff (since the beginning of the
// line should be "+", "-" or " " if the code is in the diff text).
//
// While it's possible a diff might be colorized and fail this test, it's
// unlikely, and it covers hg's color extension which seems to be the most
// stubborn about colorizing text despite stdout not being a TTY.
//
// We might incorrectly strip color codes from a colorized diff of a text
// file with color codes inside it, but this case is stupid and pathological
// and you've dug your own grave.
$ansi_color_pattern = '\x1B\[[\d;]*m';
if (preg_match('/^'.$ansi_color_pattern.'/m', $text)) {
$text = preg_replace('/'.$ansi_color_pattern.'/', '', $text);
}
$this->text = phutil_split_lines($text);
$this->line = 0;
}
protected function getLine() {
if ($this->text === null) {
throw new Exception("Not parsing!");
}
if (isset($this->text[$this->line])) {
return $this->text[$this->line];
}
return null;
}
protected function getLineTrimmed() {
$line = $this->getLine();
if ($line !== null) {
$line = trim($line, "\r\n");
}
return $line;
}
protected function nextLine() {
$this->line++;
return $this->getLine();
}
protected function nextLineTrimmed() {
$line = $this->nextLine();
if ($line !== null) {
$line = trim($line, "\r\n");
}
return $line;
}
protected function nextNonemptyLine() {
while (($line = $this->nextLine()) !== null) {
if (strlen(trim($line)) !== 0) {
break;
}
}
return $this->getLine();
}
protected function nextLineThatLooksLikeDiffStart() {
while (($line = $this->nextLine()) !== null) {
if (preg_match('/^\s*diff\s+-r/', $line)) {
break;
}
}
return $this->getLine();
}
protected function saveLine() {
$this->lineSaved = $this->line;
}
protected function restoreLine() {
$this->line = $this->lineSaved;
}
protected function isFirstNonEmptyLine() {
$count = count($this->text);
for ($i = 0; $i < $count; $i++) {
if (strlen(trim($this->text[$i])) != 0) {
return ($i == $this->line);
}
}
// Entire file is empty.
return false;
}
protected function didFinishParse() {
$this->text = null;
}
public function setWriteDiffOnFailure($write) {
$this->writeDiffOnFailure = $write;
return $this;
}
protected function didFailParse($message) {
$context = 5;
$min = max(0, $this->line - $context);
$max = min($this->line + $context, count($this->text) - 1);
$context = '';
for ($ii = $min; $ii <= $max; $ii++) {
$context .= sprintf(
"%8.8s %6.6s %s",
($ii == $this->line) ? '>>> ' : '',
$ii + 1,
$this->text[$ii]);
}
$out = array();
$out[] = "Diff Parse Exception: {$message}";
if ($this->writeDiffOnFailure) {
$temp = new TempFile();
$temp->setPreserveFile(true);
Filesystem::writeFile($temp, $this->rawDiff);
$out[] = "Raw input file was written to: ".(string)$temp;
}
$out[] = $context;
$out = implode("\n\n", $out);
throw new Exception($out);
}
/**
* Unescape escaped filenames, e.g. from "git diff".
*/
private static function unescapeFilename($name) {
if (preg_match('/^".+"$/', $name)) {
return stripcslashes(substr($name, 1, -1));
} else {
return $name;
}
}
private function loadSyntheticData() {
if (!$this->changes) {
return;
}
$repository_api = $this->repositoryAPI;
if (!$repository_api) {
return;
}
$changes = $this->changes;
foreach ($changes as $change) {
$path = $change->getCurrentPath();
// Certain types of changes (moves and copies) don't contain change data
// when expressed in raw "git diff" form. Augment any such diffs with
// textual data.
if ($change->getNeedsSyntheticGitHunks() &&
($repository_api instanceof ArcanistGitAPI)) {
$diff = $repository_api->getRawDiffText($path, $moves = false);
// NOTE: We're reusing the parser and it doesn't reset change state
// between parses because there's an oddball SVN workflow in Phabricator
// which relies on being able to inject changes.
// TODO: Fix this.
$parser = clone $this;
$parser->setChanges(array());
$raw_changes = $parser->parseDiff($diff);
foreach ($raw_changes as $raw_change) {
if ($raw_change->getCurrentPath() == $path) {
$change->setFileType($raw_change->getFileType());
foreach ($raw_change->getHunks() as $hunk) {
// Git thinks that this file has been added. But we know that it
// has been moved or copied without a change.
$hunk->setCorpus(
preg_replace('/^\+/m', ' ', $hunk->getCorpus()));
$change->addHunk($hunk);
}
break;
}
}
$change->setNeedsSyntheticGitHunks(false);
}
if ($change->getFileType() != ArcanistDiffChangeType::FILE_BINARY &&
$change->getFileType() != ArcanistDiffChangeType::FILE_IMAGE) {
continue;
}
$change->setOriginalFileData($repository_api->getOriginalFileData($path));
$change->setCurrentFileData($repository_api->getCurrentFileData($path));
}
$this->changes = $changes;
}
/**
* Strip prefixes off paths from `git diff`. By default git uses a/ and b/,
* but you can set `diff.mnemonicprefix` to get a different set of prefixes,
* or use `--no-prefix`, `--src-prefix` or `--dst-prefix` to set these to
* other arbitrary values.
*
* We strip the default and mnemonic prefixes, and trust the user knows what
* they're doing in the other cases.
*
* @param string Path to strip.
* @return string Stripped path.
*/
public static function stripGitPathPrefix($path) {
static $regex;
if ($regex === null) {
$prefixes = array(
// These are the defaults.
'a/',
'b/',
// These show up when you set "diff.mnemonicprefix".
'i/',
'c/',
'w/',
'o/',
'1/',
'2/',
);
foreach ($prefixes as $key => $prefix) {
$prefixes[$key] = preg_quote($prefix, '@');
}
$regex = '@^('.implode('|', $prefixes).')@S';
}
return preg_replace($regex, '', $path);
}
/**
* Split the paths on a "diff --git" line into old and new paths. This
* is difficult because they may be ambiguous if the files contain spaces.
*
* @param string Text from a diff line after "diff --git ".
* @return pair<string, string> Old and new paths.
*/
public static function splitGitDiffPaths($paths) {
$matches = null;
$paths = rtrim($paths, "\r\n");
$patterns = array(
// Try quoted paths, used for unicode filenames or filenames with quotes.
'@^(?P<old>"(?:\\\\.|[^"\\\\]+)+") (?P<new>"(?:\\\\.|[^"\\\\]+)+")$@',
// Try paths without spaces.
'@^(?P<old>[^ ]+) (?P<new>[^ ]+)$@',
// Try paths with well-known prefixes.
'@^(?P<old>[abicwo12]/.*) (?P<new>[abicwo12]/.*)$@',
// Try the exact same string twice in a row separated by a space.
// This can hit a false positive for moves from files like "old file old"
// to "file", but such a case combined with custom diff prefixes is
// incredibly obscure.
'@^(?P<old>.*) (?P<new>\\1)$@',
);
foreach ($patterns as $pattern) {
if (preg_match($pattern, $paths, $matches)) {
break;
}
}
if (!$matches) {
throw new Exception(
"Input diff contains ambiguous line 'diff --git {$paths}'. This line ".
"is ambiguous because there are spaces in the file names, so the ".
"parser can not determine where the file names begin and end. To ".
"resolve this ambiguity, use standard prefixes ('a/' and 'b/') when ".
"generating diffs.");
}
$old = $matches['old'];
$old = self::unescapeFilename($old);
$old = self::stripGitPathPrefix($old);
$new = $matches['new'];
$new = self::unescapeFilename($new);
$new = self::stripGitPathPrefix($new);
return array($old, $new);
}
}
diff --git a/src/parser/__tests__/ArcanistBaseCommitParserTestCase.php b/src/parser/__tests__/ArcanistBaseCommitParserTestCase.php
index 0f64914f..297f51f6 100644
--- a/src/parser/__tests__/ArcanistBaseCommitParserTestCase.php
+++ b/src/parser/__tests__/ArcanistBaseCommitParserTestCase.php
@@ -1,165 +1,149 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistBaseCommitParserTestCase extends ArcanistTestCase {
public function testBasics() {
// Verify that the very basics of base commit resolution work.
$this->assertCommit(
'Empty Rules',
null,
array(
));
$this->assertCommit(
'Literal',
'xyz',
array(
'args' => 'literal:xyz',
));
}
public function testResolutionOrder() {
// Rules should be resolved in order: args, local, project, global. These
// test cases intentionally scramble argument order to test that resolution
// order is independent of argument order.
$this->assertCommit(
'Order: Args',
'y',
array(
'local' => 'literal:n',
'project' => 'literal:n',
'args' => 'literal:y',
'global' => 'literal:n',
));
$this->assertCommit(
'Order: Local',
'y',
array(
'project' => 'literal:n',
'local' => 'literal:y',
'global' => 'literal:n',
));
$this->assertCommit(
'Order: Project',
'y',
array(
'project' => 'literal:y',
'global' => 'literal:n',
));
$this->assertCommit(
'Order: Global',
'y',
array(
'global' => 'literal:y',
));
}
public function testHalt() {
// 'arc:halt' should halt all processing.
$this->assertCommit(
'Halt',
null,
array(
'args' => 'arc:halt',
'local' => 'literal:xyz',
));
}
public function testYield() {
// 'arc:yield' should yield to other rulesets.
$this->assertCommit(
'Yield',
'xyz',
array(
'args' => 'arc:yield, literal:abc',
'local' => 'literal:xyz',
));
// This one should return to 'args' after exhausting 'local'.
$this->assertCommit(
'Yield + Return',
'abc',
array(
'args' => 'arc:yield, literal:abc',
'local' => 'arc:skip',
));
}
public function testJump() {
// This should resolve to 'abc' without hitting any of the halts.
$this->assertCommit(
'Jump',
'abc',
array(
'args' => 'arc:project, arc:halt',
'local' => 'literal:abc',
'project' => 'arc:global, arc:halt',
'global' => 'arc:local, arc:halt',
));
}
public function testJumpReturn() {
// After jumping to project, we should return to 'args'.
$this->assertCommit(
'Jump Return',
'xyz',
array(
'args' => 'arc:project, literal:xyz',
'local' => 'arc:halt',
'project' => '',
'global' => 'arc:halt',
));
}
private function assertCommit($desc, $commit, $rules) {
$parser = $this->buildParser();
$result = $parser->resolveBaseCommit($rules);
$this->assertEqual($commit, $result, $desc);
}
private function buildParser() {
// TODO: This is a little hacky beacuse we're using the Arcanist repository
// itself to execute tests with, but it should be OK until we get proper
// isolation for repository-oriented test cases.
$root = dirname(phutil_get_library_root('arcanist'));
$copy = ArcanistWorkingCopyIdentity::newFromPath($root);
$repo = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity($copy);
return new ArcanistBaseCommitParser($repo);
}
}
diff --git a/src/parser/__tests__/ArcanistBundleTestCase.php b/src/parser/__tests__/ArcanistBundleTestCase.php
index a6837483..88301ef0 100644
--- a/src/parser/__tests__/ArcanistBundleTestCase.php
+++ b/src/parser/__tests__/ArcanistBundleTestCase.php
@@ -1,849 +1,833 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistBundleTestCase extends ArcanistTestCase {
private function loadResource($name) {
return Filesystem::readFile($this->getResourcePath($name));
}
private function getResourcePath($name) {
return dirname(__FILE__).'/bundle/'.$name;
}
private function loadDiff($old, $new) {
list($err, $stdout) = exec_manual(
'diff --unified=65535 --label %s --label %s -- %s %s',
'file 9999-99-99',
'file 9999-99-99',
$this->getResourcePath($old),
$this->getResourcePath($new));
$this->assertEqual(
1,
$err,
"Expect `diff` to find changes between '{$old}' and '{$new}'.");
return $stdout;
}
private function loadOneChangeBundle($old, $new) {
$diff = $this->loadDiff($old, $new);
return ArcanistBundle::newFromDiff($diff);
}
/**
* Unarchive a saved git repository and apply each commit as though via
* "arc patch", verifying that the resulting tree hash is identical to the
* tree hash produced by the real commit.
*/
public function testGitRepository() {
if (phutil_is_windows()) {
$this->assertSkipped('This test is not supported under Windows.');
}
$archive = dirname(__FILE__).'/bundle.git.tgz';
$patches = dirname(__FILE__).'/patches/';
$fixture = PhutilDirectoryFixture::newFromArchive($archive);
chdir($fixture->getPath());
list($commits) = execx(
'git log --format=%s',
'%H %T %s');
$commits = explode("\n", trim($commits));
// The very first commit doesn't have a meaningful parent, so don't examine
// it.
array_pop($commits);
foreach ($commits as $commit) {
list($commit_hash, $tree_hash, $subject) = explode(' ', $commit, 3);
execx('git reset --hard %s --', $commit_hash);
$repository_api = new ArcanistGitAPI($fixture->getPath());
$repository_api->setDefaultBaseCommit();
$diff = $repository_api->getFullGitDiff();
$parser = new ArcanistDiffParser();
$parser->setRepositoryAPI($repository_api);
$changes = $parser->parseDiff($diff);
$this->makeChangeAssertions($commit_hash, $changes);
$bundle = ArcanistBundle::newFromChanges($changes);
execx('git reset --hard %s^ --', $commit_hash);
$patch = $bundle->toGitPatch();
$expect_path = $patches.'/'.$commit_hash.'.gitpatch';
$expect = null;
if (Filesystem::pathExists($expect_path)) {
$expect = Filesystem::readFile($expect_path);
}
if ($patch === $expect) {
$this->assertEqual($expect, $patch);
} else {
Filesystem::writeFile($expect_path.'.real', $patch);
throw new Exception(
"Expected patch and actual patch for {$commit_hash} differ. ".
"Wrote actual patch to '{$expect_path}.real'.");
}
try {
id(new ExecFuture('git apply --index --reject'))
->write($patch)
->resolvex();
} catch (CommandException $ex) {
$temp = new TempFile(substr($commit_hash, 0, 8).'.patch');
$temp->setPreserveFile(true);
Filesystem::writeFile($temp, $patch);
PhutilConsole::getConsole()->writeErr(
"Wrote failing patch to '%s'.\n",
$temp);
throw $ex;
}
execx('git commit -m %s', $subject);
list($result_hash) = execx('git log -n1 --format=%s', '%T');
$result_hash = trim($result_hash);
$this->assertEqual(
$tree_hash,
$result_hash,
"Commit {$commit_hash}: {$subject}");
}
}
private function makeChangeAssertions($commit, array $raw_changes) {
$changes = array();
// Verify that there are no duplicate changes, and rekey the changes on
// affected path because we don't care about the order in which the
// changes appear.
foreach ($raw_changes as $change) {
$this->assertEqual(
true,
empty($changes[$change->getCurrentPath()]),
"Unique Path: ".$change->getCurrentPath());
$changes[$change->getCurrentPath()] = $change;
}
switch ($commit) {
case 'c573c25d1a767d270fed504cd993e78aba936338':
// "Copy a koan over text, editing the original koan."
// Git doesn't really do anything meaningful with this.
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
$c = $changes['text'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
break;
case 'd26628e588cf7d16368845b121c6ac6c781e81d0':
// "Copy a koan, modifying both the source and destination."
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_AWAY,
$c->getType());
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
break;
case 'b0c9663ecda5f666f62dad245a3a7549aac5e636':
// "Remove a koan copy."
$this->assertEqual(1, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
break;
case 'b6ecdb3b4801f3028d88ba49940a558360847dbf':
// "Copy a koan and edit the destination."
// Git does not detect this as a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '30d23787e1ecd254c884afbe37afa612f61e3904':
// "Move and edit a koan."
$this->assertEqual(2, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case 'c0ba9bfe3695f95c3f558bc5797eeba421d32483':
// "Remove two koans."
$this->assertEqual(2, count($changes));
$c = $changes['koan3'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
$c = $changes['koan4'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
break;
case '2658fd01d5355abe5d4c7ead3a0e7b4b3449fe77':
// "Multicopy a koan."
$this->assertEqual(3, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MULTICOPY,
$c->getType());
$c = $changes['koan3'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$c = $changes['koan4'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case '1c5fe4e2243bb19d6b3bf15896177b13768e6eb6':
// "Copy a koan."
// Git does not detect this as a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '6d9eb65a2c2b56dee64d72f59554c1cca748dd34':
// "Move a koan."
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case '141452e2a775ee86409e8779dd2eda767b4fe8ab':
// "Add a koan."
$this->assertEqual(1, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '5dec8bf28557f078d1987c4e8cfb53d08310f522':
// "Copy an image, and replace the original."
// `image_2.png` is copied to `image.png` and then replaced.
$this->assertEqual(2, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_AWAY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
'c9ec1b952480da09b393ba672d9b13da',
md5($c->getCurrentFileData()));
break;
case 'fb28468d25a5fdd063aca4ca559454c998a0af51':
// "Multicopy image."
// `image.png` is copied to `image_2.png` and `image_3.png` and then
// deleted. Git detects this as a move and an add.
$this->assertEqual(3, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MULTICOPY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
$c = $changes['image_3.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case 'df340e88d8aba12e8f2b8827f01f0cd9f35eb758':
// "Remove binary image."
// `image_2.png` is deleted.
$this->assertEqual(1, count($changes));
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
break;
case '3f5c6d735e64c25a04f83be48ef184b25b5282f0':
// "Copy binary image."
// `image_2.png` is copied to `image.png`. Git does not detect this as
// a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case 'b454edb3bb29890ee5b3af5ef66ce6a24d15d882':
// "Move binary image."
// `image.png` is moved to `image_2.png`.
$this->assertEqual(2, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case '5de5f3dfda1b7db2eb054e57699f05aaf1f4483e':
// "Add a binary image."
// `image.png` is added.
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case '176a4c2c3fd88b2d598ce41a55d9c3958be9fd2d':
// "Convert \r\n newlines to \n newlines."
case 'a73b28e139296d23ade768f2346038318b331f94':
// "Add text with \r\n newlines."
case '337ccec314075a2bdb4a912ef467d35d04a713e4':
// "Convert \n newlines to \r\n newlines.";
case '6d5e64a4a7a6a036c53b1d087184cb2c70099f2c':
// "Remove tabs."
case '49395994a1a8a06287e40a3b318be4349e8e0288':
// "Add tabs."
case 'a5a53c424f3c2a7e85f6aee35e834c8ec5b3dbe3':
// "Add trailing newline."
case 'd53dc614090c6c7d6d023e170877d7f611f18f5a':
// "Remove trailing newline."
case 'f19fb9fa1385c01b53bdb6d8842dd154e47151ec':
// "Edit a text file."
$this->assertEqual(1, count($changes));
$c = $changes['text'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_TEXT,
$c->getFileType());
break;
case '228d7be4840313ed805c25c15bba0f7b188af3e6':
// "Add a text file."
// This commit is never reached because we skip the 0th commit junk.
$this->assertEqual(true, "This is never reached.");
break;
default:
throw new Exception(
"Commit {$commit} has no change assertions!");
}
}
public function testTrailingContext() {
// Diffs need to generate without extra trailing context, or 'patch' will
// choke on them.
$this->assertEqual(
$this->loadResource('trailing-context.diff'),
$this->loadOneChangeBundle(
'trailing-context.old',
'trailing-context.new')->toUnifiedDiff());
}
public function testDisjointHunks() {
// Diffs need to generate without overlapping hunks.
$this->assertEqual(
$this->loadResource('disjoint-hunks.diff'),
$this->loadOneChangeBundle(
'disjoint-hunks.old',
'disjoint-hunks.new')->toUnifiedDiff());
}
public function testNonlocalTrailingNewline() {
// Diffs without changes near the end of the file should not generate a
// bogus, change-free hunk if the file has no trailing newline.
$this->assertEqual(
$this->loadResource('trailing-newline.diff'),
$this->loadOneChangeBundle(
'trailing-newline.old',
'trailing-newline.new')->toUnifiedDiff());
}
public function testEncodeBase85() {
$data = '';
for ($ii = 0; $ii <= 255; $ii++) {
$data .= chr($ii);
}
for ($ii = 255; $ii >= 0; $ii--) {
$data .= chr($ii);
}
$expect = Filesystem::readFile(dirname(__FILE__).'/base85/expect1.txt');
$expect = trim($expect);
$this->assertEqual(
$expect,
ArcanistBundle::encodeBase85($data));
// This is just a large block of random binary data, it has no special
// significance.
$data =
"\x56\x4c\xb3\x63\xe5\x4a\x9f\x03\xa3\x4c\xdd\x5d\x85\x86\x10".
"\x30\x3f\xc1\x28\x51\xd8\xb2\x1a\xc3\x79\x15\x85\x31\x66\xf9".
"\x8e\xe1\x20\x8f\x12\xa1\x94\x0e\xbf\xb6\x9c\xb5\xc0\x15\x43".
"\x3d\xad\xed\x00\x3c\x16\xfa\x76\x2f\xed\x99\x3a\x78\x3e\xd1".
"\x91\xf8\xb0\xca\xb9\x29\xfe\xd4\x0f\x16\x70\x19\xad\xd9\x42".
"\x15\xb4\x8f\xd6\x8f\x80\x62\xe9\x48\x77\x9f\x38\x6d\x3f\xd6".
"\x0e\x40\x68\x68\x93\xae\x75\x6d\x7f\x75\x9c\x80\x69\x94\x22".
"\x87\xb6\xc0\x62\x6b\xab\x49\xb8\x91\xe9\x96\xbf\x04\xc2\x50".
"\x30\xae\xea\xc1\x70\x8e\x91\xd0\xb6\xec\x56\x14\x78\xd5\x8a".
"\x8c\x52\xd1\x3c\xde\x65\x21\xec\x93\xab\xcf\x7e\xf5\xfd\x6d".
"\x2d\x69\xb9\x2e\xa3\x42\x7b\x4d\xa5\xfb\x28\x6d\x74\xa3\x7b".
"\x3a\xc5\x34\x7c\x63\xa9\xf9\x8e\x34\x14\x42\xb0\xf1\x0e\xe2".
"\xd0\xd2\x04\x81\xff\x62\xd5\xd9\x46\x3b\x36\x88\x8a\x93\x55".
"\x02\x2c\xff\x9f\x48\xd6\x7a\xcb\xbf\x6a\x33\xaa\x6b\x08\x4c".
"\x96\x98\x89\x53\x56\xb4\xb3\x9b\x06\xb1\xa0\x13\x69\xfa\x6a".
"\xa8\x0d\x6a\xda\xb2\x6f\x62\x0b\xa8\xf6\x59\x29\x46\x7d\x04".
"\x44\xeb\x90\x6f\xd7\xc7\xb6\xca\xc5\xeb\xde\x10\x9b\xbd\xf2".
"\x66\x8e\xd0\x0b\xda\x8c\xeb\x90\x73\x73\x33\xe7\x6f\x26\x57".
"\x4e\xfc\x95\xe0\xfc\x62\x93\xa7\x28\xe6\x0c\x46\x73\xdd\x01".
"\xce\x43\x9b\x4e\x16\x74\x5b\x36\x92\x5a\x66\x4c\xe3\x9e\x90".
"\x2d\x9a\x1a\x3d\x69\x39\x67\x04\xd6\xf8\x5f\x45\xee\xbb\xd4".
"\x63\xcf\x8c\x9b\x31\x69\x98\x1a\x98\x57\x4b\xa9\x49\xf6\x1b".
"\x76\x28\xd7\xe3\x8f\x63\x95\x5b\x06\xe2\xa8\x66\x60\xf9\x49".
"\x4e\x40\x53\x32\x9b\x74\x36\xc0\x56\xf4\x33\xec\x83\xd2\x2c".
"\x69\x60\x55\x11\x3b\x4f\xd6\x0a\xf6\x04\x38\x75\xb6\xc2\x82".
"\x4d\xfa\x83\x56\xba\x35\x42\xc3\xcb\xdc\x28\xf4\x69\x48\xa9".
"\xe0\x51\x41\x79\x66\xfe\x61\xd1\xf2\x9f\x7b\xde\xc4\x3e\x8f".
"\x8f\xb6\x9c\x0a\x74\xf8\x71\x03\x37\x37\x30\x8d\x2a\x6a\xc9".
"\x51\xa1\xe2\x34\xe5\x42\xdb\x4f\x61\x4e\x16\xfc\x23\x72\x12".
"\x46\x53\x12\x82\x3e\x44\x63\x23\x82\xaa\xab\x7e\x8d\x70\x66".
"\xf1\x94\x86\x02\xc5\x3e\x9c\x79\x17\x1e\x9f\x13\x89\x3d\x25".
"\x45\xc9\x3b\x1e\xa0\x1a\x03\x20\x1c\x81\x6b\xfc\xb5\xc9\xe2".
"\xda\xb1\x87\x34\xa0\xb2\x72\x36\x68\x12\x05\x53\x7c\x68\x6b".
"\x1e\x2a\x56\x2a\x7e\x7f\xd0\x9c\x13\xa9\xb2\x4c\xe6\x8a\x65".
"\xd7\x67\xad\xf3\xf3\x2b\x9c\xe8\x10\x07\x8a\xe2\x20\x67\xe4".
"\x51\x47\xc1\x22\x91\x05\x22\x39\x1a\xef\x54\xd2\x8a\x88\x55".
"\x3f\x83\xba\x73\xd4\x95\xc7\xb8\xa2\xfd\x4d\x4e\x5d\xff\xdd".
"\xaf\x1a\xc2\x7e\xb5\xfa\x86\x5f\x93\x38\x5d\xca\x9a\x5a\x7e".
"\xb7\x47\xd5\x5c\x6b\xf3\x32\x03\x11\x44\xe9\x49\x12\x40\x82".
"\x67\x7d\x2a\x5a\x61\x81\xbd\x24\xaa\xd7\x7c\xc9\xcf\xaf\xb0".
"\x3e\xb0\x43\xcd\xce\x21\xe4\x1b\x5a\xd6\x40\xf5\x0e\x23\xef".
"\x70\xf4\xc6\xd2\xd7\x36\xd7\x20\xda\x8d\x39\x46\xea\xfc\x78".
"\x55\xa2\x02\xd6\x77\x21\xc8\x97\x1e\xdf\x45\xde\x93\xa7\x74".
"\xd8\x59\x10\x24\x8a\xe8\xcd\xe9\x00\xb5\x4e\xe6\x49\xb0\xde".
"\x14\x1a\x5d\xdd\x38\x47\xb0\xc7\x1e\xec\x7c\x76\xc9\x21\x3c".
"\x3a\x85\x4f\x71\x97\xed\x4a\x94\x2c\x51\x48\x9c\x43\x90\x70".
"\xe9\x0e\x84\x55\xd2\xa4\x48\xfa\xfd\x54\x12\x11\xb9\x32\xfc".
"\x1d\x66\xe7\x42\xe3\x5e\x65\xf4\x3d\xea\x1a\x53\xe3\x7b\x4b".
"\xee\xdb\x74\xce\x30\xd3\x04\xcb\xda\xa4\xdd\xad\x98\x3a\x76".
"\xe8\xba\x1b\x03\x53\xed\x46\x5d\xef\xd4\x34\xc2\x8d\xef\xae".
"\x51\x35\x0f\x4d\x40\xaa\x3a\xdb\x50\x1a\xbe\x5f\x8b\xb8\x24".
"\x40\x19\x8f\x8a\x6b\x44\x4f\x9b\xe0\xf4\x9c\x4b\xc4\x23\x37".
"\xf0\xb3\xe1\x58\x9d\x0e\xd9\xa9\xf7\x3e\x86\x43\x9b\x5b\x90".
"\x3c\xc0\x20\xa0\xc5\x86\x4f\xc6\xcb\xb5\xcb\xd4\x88\xc6\x72".
"\x57\xa7\x57\x2c\x34\x26\x91\x44\x15\xa8\xf4\x88\xca\x74\x56".
"\x9e\x12\x6c\xdf\x52\xef\xc0\xb4\x5c\x16\xe8\xaa\xf7\xb6\xf3".
"\x7c\xda\xcd\x42\xf9\x1c\x40\x88\x44\x68\x4f\x1b\x5a\x7b\x8f".
"\xc3\x47\x48\xd3\xf3\xe5\xf5\x66\x35\x48\xbe\x64\xdf\xfe\x35".
"\xf1\xc3\xe4\xa8\xfc\x86\xfb\x69\x20\xc9\xf4\x16\x96\xc1\x7a".
"\x51\x14\x77\xa4\x6e\x13\xe8\x59\x35\x24\xf1\xe5\xfe\xe9\x98".
"\x0d\xd1\xe8\xce\x9c\x7f\xf8\x3b\x79\x39\x3a\x1d\xa3\x77\xef".
"\x4f\x4b\x59\x73\x03\xb3\xfe\xae\x70\x2a\x3a\xf0\x79\x9d\x7e".
"\x9b\xaa\xb1\x18\xf9\x43\x69\xf3\x55\x46\xad\x38\xa2\xf1\xcb".
"\xce\x37\xa9\x88\x20\x38\xea\x19\x29\x95\x8c\x75\x06\x9d\x1d".
"\x9e\xf2\xb7\x64\x98\x21\x36\x90\x92\xf8\xb8\x89\x1e\x5c\x5d".
"\x09\x3b\x52\xc5\x6a\x87\x7e\x46\xca\x8c\xdf\xe7\xca\xa9\x7b".
"\x11\x63\x0f\x9e\x42\x9a\x3e\xe0\x8b\x80\x9e\x91\x76\x88\x9a".
"\xa1\xe2\x96\xae\xfb\x18\x39\xdc\x92\x99\x34\xfd\x98\x20\xa8".
"\x89\x61\x2c\x26\xe0\xb8\x83\xa7\xe7\x50\x42\x8f\xfc\x36\x66".
"\x6b\x25\xc5\x6d\xb4\x31\xe1\x4d\x0f\x2e\xf8\x44\xe2\xb6\x6a".
"\x6d\xfe\x83\x9e\x2c\x07\x2f\x15\x41\xf3\xe7\xa6\x18\x2b\x84".
"\x7e\xeb\x43\xcc\xbb\xdb\xa9\x54\x5c\xbc\x59\x6a\xdc\x26\x2a".
"\xf4\x59\xa7\x75\xa4\xac\xed\x73\x8f\x16\x43\x0d\x97\x10\x2c".
"\x70\xef\x9e\xb2\xc9\xdf\xe6\xa7\x9b\x08\x79\xa3\xf7\x99\xf5".
"\x59\xe4\xd5\x89\x10\xe5\xc9\xf7\xe7\x29\x72\x06\xc6\x54\xc3".
"\xcd\xd0\xff\x69\xf8\xdf\x19\xf2\x66\x1c\x69\x40\xbc\x97\xf1".
"\x49\x5e\x78\x62\x52\x46\x7f\xcf\x44\x50\x8b\x5f\xe7\xa8\xeb".
"\xd5\x84\x24\x81\xc0\x2c\x65\xf7\x95\xbd\xf2\x8e\x43\xfb\x6a".
"\x49\x3c\x6a\xe5\x2a\x39\xf0\xfa\x89\x59\x5f\x39\x75\xb4\x6f".
"\x04\xf1\xe0\x2c\xcd\x77\x34\xec\x6b\x45\x16\xe3\x18\x24\x05".
"\xb9\x68\xc1\x4e\x71\x4b\xff\x88\x18\xea\x0d\x56\x49\x55\xdf".
"\xe5\xb0\x59\xdb\x74\x9e\x0b\x38\x03\x9f\x10\x6f\xd9\x34\x07".
"\x44\x29\x08\xb1\xd4\x77\xc6\x84\x0d\xbb\xb5\xd5\x09\x05\x19".
"\x01\x62\x29\x45\x52\x1d\xc6\x4f\x25\x78\x7e\xbc\xae\x07\xb3".
"\xd4\xe0\x19\x91\x03\xd6\x8d\x2f\x00\xc9\xb2\x66\x3b\x4e\x3d".
"\x75\xf7\x23\x9a\x3e\xa4\xd5\x7f\x75\x47\xd0\xbc\xc3\xc8\x2a".
"\xdc\x85\x09\x6c\x0c\x90\x38\xd8\xef\xcf\xf4\x7a\x1b\xc7\x76".
"\xe0\xdb\x81\xa8\x1b\x2b\x8d\xd4\x36\x90\x76\xde\x8a\x90\xc8".
"\x5b\x05\x00\xeb\xb3\x20\xce\x6e\x5c\xb9\x35\x3d\x95\x3a\x79".
"\x4a\x60\xeb\x23\x11\xfb\x90\x2d\xf6\xb7\x05\x4a\x43\x41\x79".
"\x51\xaa\xe6\x90\x0a\x71\x87\x80\xbe\xb0\x89\x0f\xd3\x84\x19".
"\xce\x6c\xf9\xbb\x1b\x15\x4d\x0f\x33\x65\xf7\x9e\x3a\xd9\x8c".
"\x02\x43\xcf\xdf\xb2\x60\xc1\x4c\xe9\xa5\x3c\xaf\xfa\x41\x2d".
"\xb9\x1f\x45\x32\xcb\x39\x2f\x94\xae\x44\x6d\x69\xc1\xc9\x57".
"\x8c\xe5\xf4\xa4\x3a\xb6\x70\x61\xf9\xbb\x41\xdc\x78\xf0\xf7".
"\xbf\xa8\x8e\xe3\x77\x51\xce\x25\x2f\xdf\x27\x6b\x07\x30\x9f".
"\xce\xdb\x59\x58\xaa\xb2\x2e\xdc\x90\x92\x82\x55\xfe\x25\x36".
"\x49\x7f\x6d\x2d\x39\x51\xef\x3d\xc8\xa3\x87\x0b\xe7\xf2\xac".
"\x90\xa0\x1d\xd8\xc7\xea\x93\x53\x3b\x21\x84\x2e\x52\x6c\xfb".
"\x4f\x31\xda\xd1\xea\x45\x3e\xdc\xeb\x52\x81\x8c\x2b\xf4\x2a".
"\xbc\x01\xc4\xe7\x68\x36\x9c\xd5\x2d\xc1\x61\xcb\x9a\x5f\x18".
"\x00\x6a\xc8\x9a\x4e\xfd\x31\x5b\xce\x90\x4e\x45\xff\x7f\xea".
"\xb2\x26\xad\xc1\x3a\x21\xa9\xe8\x7c\x14\xae\x81\x1e\xbe\xa3".
"\x6d\xda\x92\x1b\xeb\xf2\x69\x76\x3e\xf1\x2b\xf7\x1a\x45\xd5".
"\xb3\x81\xb1\xbe\x80\x7f\x24\xba\x0e\xd5\x68\x34\x3f\x1a\x29".
"\x15\x0e\xc2\x26\x62\x0c\xaa\xa9\x20\x4c\x61\x65\x49\x07\xbe".
"\x69\xf4\xc9\xec\x2f\x1c\xfa\x59\x2e\x72\xc0\x17\xc5\x4c\xfa".
"\xba\x2f\x64\xab\xa9\xb4\xcb\xdc\xcb\x25\x5f\xcf\x0c\x87\xcc".
"\xf0\x36\x2b\xce\x81\x5a\x22\x85\xa0\x50\x50\x97\x8e\xda\x36".
"\x80\x74\xb5\x1e\x02\x3f\xd7\xc8\x29\x11\xeb\x1d\x3d\x74\x9f".
"\x26\x1a\xa4\x3d\xf9\x0e\xf0\x2d\x5c\xa9\x43\xbf\x51\x6c\x8d".
"\xe6\x78\xe0\x67\x57\xf0\xc8\x0e\x97\x9c\x57\x23\x30\xac\x63".
"\xdf\x46\x98\xa4\xaf\x4e\xa7\xe5\xac\x31\xbd\xeb\x6a\xa0\xb0".
"\xe4\x94\x7e\x51\xf6\x89\x81\x3e\xab\x4f\x64\xb7\xc5\x51\x71".
"\xcd\x74\x02\xa9\x02\x99\x5c\xab\x0e\x14\x47\x3b\x04\xc1\x9b".
"\x59\x1a\x93\x92\x4c\x71\x20\x5f\x6e\xd3\xf3\xa7\x47\x1b\x39".
"\x3e\x73\x69\xe2\xec\xcb\x52\xb3\x5c\x7a\x95\x25\x3f\x16\x98".
"\x60\xa8\xa2\x5d\xc4\x5a\x67\xe4\x11\x06\x06\xf9\x7a\xb4\x14".
"\xe0\xbc\x7b\x13\x1d\x0f\xf2\xca\x0b\xd4\xaa\x71\x35\x3e\xd6".
"\x2e\x2e\x5d\x7b\x15\xc9\x23\x1a\xa9\x24\x31\x48\xd4\xcf\x4a".
"\xf4\x32\x17\x9b\x1d\x4b\xfe\x49\x69\xd6\xc0\x8f\xb9\xdb\x72".
"\x52\x2c\xe8\xf3\xc4\xfc\x46\xf5\xb8\x1b\x05\x06\xcf\xcc\x23".
"\x34\xbf\x25\x6a\xea\x3c\xc7\x64\xd4\xd5\xb3\x67\xed\x24\x27".
"\xd3\x67\xc1\xbd\x9f\x7b\x7d\x19\x04\x5c\xd1\x96\x7e\xa5\xc7".
"\xbb\xb2\x84\x68\x98\x38\x11\x90\xfb\x62\x15\xfd\xe6\xb7\x24".
"\x77\xb2\x78\xc7\x73\x91\xc9\x60\x1d\x91\x6d\x04\x2b\x41\xe9".
"\xc9\xfa\xe4\x98\x54\x83\x9a\x6e\x76\x8c\x21\xf9\x91\x38\x1f".
"\xdc\xfe\x13\x09\x30\xd7\x53\x63\x62\xba\xe3\x2c\x70\xd5\xfc".
"\x78\x35\x36\x79\x5d\xb6\x0e\x35\x3d\x46\x87\xfb\xf5\x64\x1f".
"\x3e\xfd\x2f\x1c\xbb\xed\x95\x2d\xd6\x63\xdc\xa7\x6a\x39\x8f".
"\xbd\xcb\x79\x95\xe9\x45\xbf\xe4\x3e\x05\x55\x00\xdb\x33\x28".
"\x3a\x6c\xe2\x35\xbb\xac\x70\x52\x2b\xac\x4e\x11\x44\x58\x16".
"\x21\xb4\xae\x0d\x6a\xb9\xdc\x85\x5d\x90\x11\x26\x85\xdb\xc3".
"\xf0\x38\x6f\x8a\xff\x12\xf0\xc9\x9e\xf0\xfc\xae\x94\x11\x4d".
"\xce\x96\x29\x09\x6c\xf4\x2a\x6c\xda\x1e\x4c\x4a\xa2\x96\x5a".
"\xef\xc6\x38\x5c\x60\xa2\x28\x13\x58\x73\x96\xde\x59\x2a\x57".
"\x64\x6c\x14\x94\x8a\x2e\x8e\x21\x3f\xa2\x43\xde\xf6\x2d\x23".
"\x74\x5c\xbd\x7a\x10\xdb\x17\xa8\x93\xd0\x74\x86\x9d\x33\x07".
"\x48\xee\xac\x18\x6d\x64\x61\x7b\x61\x2b\xa4\xa2\xab\x99\x59".
"\xbe\x19\xd7\x19\x41\x1e\x61\x87\xad\x40\x5b\x69\x8c\x32\xf5".
"\xb6\x49\xbe\x1f\xad\xd8\x0f\x3e\xd9\x62\xac\x3a\x76\xde\x32".
"\xa3\xb2\x41\x95\xad\x17\x23\xab\xa1\x37\x9c\xab\x73\x79\x70".
"\xd6\x66\x0d\x6e\x4d\x8b\xa0\xac\xe3\x44\x1e\x0a\xee\xf0\x74".
"\x64\xd8\x44\xd1\x6c\xa6\xd5\x36\x2e\xd9\x55\x6e\x90\x63\xb7".
"\xf7\x8e\xc6\x28\xa3\x40\x00\x60\x9a\x3c\xfe\xff\x03\x30\x11".
"\x18\x92\x2f\x5b\x23\xe1\x4e\x99\xe4\x82\xc9\x51\xe2\x15\x6a".
"\x76\x5c\x67\xae\xa3\xa2\x9c\x85\x51\xe0\x44\x89\x63\xa5\x71".
"\x99\xbc\x2d\x9c\xab\x9a\xfb\x20\x37\x58\xd6\x2d\x8b\x7d\x42".
"\x13\x35\x44\x4c\x11\x97\x66\x27\x17\xac\x44\xe8\x6a\x03\x78".
"\xa2\x88\xc6\x36\x71\x5a\x5a\x5a\x72\xa3\xe9\x72\x0c\x91\x31".
"\xfc\xae\x7b\xa0\x75\x21\x0a\xc1\x4b\x95\xcb\xe3\xc2\xee\x03".
"\x0f\xb8\xb2\x51\xc7\xc8\x9c\x8d\x6d\x3a\xe7\x4e\x2c\xaa\xeb".
"\x5e\x49\x93\xe0\x8f\xa1\x54\x93\xe7\x7c\x5d\x31\xc7\x05\x00".
"\x28\x14\x57\x47\xb3\x05\x2d\x17\x92\x28\x45\xee\x85\x3a\x59".
"\xb6\xa6\x04\xc0\x5c\x07\x1f\xe6\x5b\x36\x53\x62\x82\x64\xd5".
"\xb6\xf2\xf5\x67\x19\x11\xee\xd2\x70\xc5\x14\x63\xc1\x75\xe1".
"\x24\xe5\x01\x59\x52\x7c\x88\x17\xb4\xe0\x15\xe9\x12\x05\xcd".
"\x88\x7a\xd5\xea\x45\xc3\xbb\x65\xd4\xdd\x0d\xde\x36\x94\x98".
"\x0d\x2c\xfb\x3c\x2f\x69\xd0\x28\xe2\x85\xd9\x27\xf3\x7a\xad".
"\x50\x68\x96\x54\x5e\xeb\xbc\x2a\x74\xde\xf3\x4e\x8b\x27\x0a".
"\xcf\x4c\x60\x40\xe8\xc5\x72\xab\x8c\xfd\xe9\xab\xff\x51\xe5".
"\xd6\xea\x9e\x34\x73\xe1\xe6\xf8\x5b\xb1\x10\xf0\xf9\x2d\x23".
"\x0e\xfe\xe5\xf4\x8d\xb6\x6d\x37\x14\xed\x54\x97\x92\x5c\x68".
"\x40\x88\xf1\x43\x29\xef\x5e\x96\x77\xa2\xe8\x3c\xae\x7f\xb1".
"\x99\x17\xa7\x0c\x6f\xe2\x43\x32\x9b\x14\x43\xf2\x15\x6b\x13".
"\x10\x68\x56\x0b\xaa\x06\x2e\xc0\xf8\xde\x9e\x54\x9d\xba\xff".
"\x76\x26\x6d\x5e\x9e\x88\x3a\x2b\x9b\x20\x43\xb9\x1a\x0e\x58".
"\x65\xec\xdb\x9e\x97\xb8\xfb\x03\x6c\xb0\x7f\xa2\xf1\xf4\x27".
"\x24\x21\x47\x51\x21\x40\x45\x28\x71\xf7\xa1\x6b\xbe\x0e\xc8".
"\x3f\x9b\xda\x62\x9d\x73\xf7\x5f\x70\x6c\xba\x1e\xeb\x16\x5c".
"\x2e\x44\x0a\x22\x02\x6c\xbe\xb9\x69\x93\xfd\xa5\x33\x26\x64".
"\x24\x6c\xc2\x3d\x2f\xf3\xd1\x97\xde\x60\x43\x1c\x0d\x1b\x94".
"\xb3\x48\x45\x7c\xd5\xd0\x71\x4d\xad\xbf\xa4\x0a\x22\x27\x04".
"\x38\x84\x19\x66\x63\xf0\xf3\xfc\xb0\xf3\x1d\xea\xba\xb9\xe4".
"\xe5\x80\xed\xe3\xf1\x78\x24\xc3\x25\x27\x71\x81\xc2\xec\x54".
"\xed\xcc\x63\xf7\x39\xcd\x83\xdf\x32\x88\xc0\x3b\xd4\x62\xb8".
"\xea\x34\xd8\xcf\xbc\x3a\x89\x38\x64\x60\x44\xde\xb6\x76\x59".
"\xb1\x95\x6a\x26\x08\xf0\xf4\x71\x25\x8b\xf8\x81\xdd\x0d\x2f".
"\x8c\xe2\x70\xc2\x96\xc2\xd8\x9b\xe4\x3f\xec\x8b\xfd\xbd\xc9".
"\x36\x33\xb7\xbc\x59\x37\x19\x09\x30\x5e\xef\x67\xae\x67\x48".
"\x72\x0b\xf4\x2a\x82\xff\xcb\xd7\xd9\x9d\x6d\x7c\xa6\x20\x42".
"\x50\x2b\x0a\x2f\x45\x99\x5b\x76\x6d\x99\x39\xa9\xb6\x32\x06".
"\x11\xf8\x19\xd1\x3f\xc0\xd6\x1f\x67\xfa\xd5\xae\x7a\x71\x8c".
"\xbc\x3d\xb4\x5f\x5c\x81\x7c\xa1\x39\x70\x0a\x17\x24\xb7\x22".
"\x86\x50\xd8\x1f\xc8\x6c\x59\x9a\xdc\xf0\x71\x01\xda\xd8\x53".
"\x98\x1c\x73\x36\xf1\x09\x86\xc9\xa7\x26\x25\xc0\x03\x3e\x13".
"\x4e\x29\xeb\xf0\x8d\xe3\x38\x03\x54\xee\x37\xfb\x51\x2e\xb4".
"\xf6\x12\x1f\xb2\x8c\x66\x75\x00\x30\x5b\xef\x59\xf9\x63\xa9".
"\x74\x07\x91\xe4\x9c\xb7\xc9\x89\xd9\xa9\x51\x93\xcb\xb1\xa7".
"\x64\x08\x79\x8f\xb4\x6d\x09\xd7\xc5\xbf\x0a\xdb\x50\xe0\x1c".
"\x83\xca\xf8\xcf\xa7\x81\xbb\x0b\xe6\xcf\x1b\x0e\x0a\xe0\xcd".
"\x68\xe2\xde\xc4\x2d\xba\x55\xc7\xc7\x1e\x6c\x5e\xca\x9b\x20".
"\x75\x96\x94\x92\x84\xec\xf5\x22\x25\x78\x67\xcd\xbe\x01\xfe".
"\x53\xa5\xcc\x6a\x40\x33\x83\xa4\x7a\x44\x93\x0b\xf9\x4c\xb2".
"\x95\xb6\x7e\x4b\xa4\xc8\x86\xfe\x8a\xf1\x77\x40\x56\x13\xc1".
"\x31\x2c\x8c\x4a\xa8\x89\x61\x0c\x39\x33\x78\x8c\xd5\x50\x3b".
"\x89\xc3\xd3\x80\x1c\xa7\xb6\x36\xc2\x00\x8d\x0a\x7f\xcc\xd3".
"\x20\x74\x60\x70\x36\x7d\xda\xdc\xc4\x49\x04\xf0\xe6\x6c\xd1".
"\xbe\xcb\xfb\xf1\xa2\xd6\xd4\xe4\x97\x3f\x35\x09\x5b\xda\x06".
"\x6b\x6d\x86\x53\x23\x0c\x26\x51\x2a\x15\xaa\xe2\x73\xfb\xc7".
"\x41\x54\xdc\x5d\x99\x0b\x0a\x1e\xd4\xdb\x70\xa3\x8e\xfd\x5b".
"\xf0\xa8\x3e\x9b\xff\x57\x98\xbc\xd9\x2a\x56\xd3\x19\xf9\x0b".
"\xd9\x67\x0f\x10\x9c\x23\xe5\x6b\x12\xc6\xb6\x4b\xd1\x0c\xe9".
"\x45\x36\xdf\x54\x6f\xcc\xfe\xb5\xcc\xb9\xfe\xde\xc8\xb5\xc9".
"\x04\x59\x61\x75\x1e\x72\x37\x54\xfd\xc6\xc3\x7e\x74\xae\x55".
"\x31\x6a\xbc\x8a\xd8\x45\x91\xe2\x8d\x20\x97\x71\xe7\x55\xd6".
"\x8a\xb8\x82\x2a\x27\x4f\xdc\x53\x89\x28\xf7\x3a\xfe\x07\xef".
"\x60\xb2\x32\x7c\xbc\x13\xc4\x3d\xda\xd7\xfb\xb8\x61\x7d\x69".
"\xae\x0e\x9a\x71\xd6\x00\x26\x97\xff\xdb\xe6\xbe\x45\x7a\xb5".
"\x00\x31\xfd\x70\xcc\xd7\x34\x88\xe4\x05\x61\xf5\x72\x1d\x14".
"\xf0\x7e\x90\xdb\x0e\xc7\xda\xd4\xf3\x99\xd4\x60\xd9\xa7\xc8".
"\x5b\x33\x34\xb5\x23\x74\x2c\x5f\x6b\x56\x95\x9c\x1b\x2a\xac".
"\xf9\xfe\x46\xc3\xf1\x9b\x24\x7e\x4b\xca\x25\x58\x41\x10\x63".
"\xe8\xe7\x68\xda\xcc\xb6\x4d\x5b\x8f\xc9\xa9\x31\xeb\x5c\x2a".
"\xcf\x9d\x89\xd5\x51\x93\x80\x30\xf4\xc9\x2c\x8c\xb8\x8c\x62".
"\xd6\x33\xbd\x95\x9f\xfa\x19\xf2\x48\x28\x09\x73\xc9\x53\x61".
"\x94\x3a\x62\x68\x6c\xc6\xd6\x0a\xb4\xae\x27\x96\xfb\x29\xd7".
"\x46\x67\x11\x7a\xe8\x3a\x9a\x3f\xf4\x9a\x75\xed\x24\x67\x45".
"\x79\xdc\x8b\x19\xf2\xef\x57\xaa\xc7\x84\xff\x9d\x2d\xc3\xa8".
"\x85\x54\xb7\x9d\xe1\xd6\x2b\xe9\x31\x9d\x6c\xb8\x4e\x76\x50".
"\x80\x44\x46\x8f\x5e\x7e\x20\xaa\xa0\x8a\x36\x6b\xef\xd1\x75".
"\xf8\x3f\x20\xdd\x09\x73\xbf\xa5\xf7\xb4\x87\xb2\x44\xc0\x0f".
"\x10\xc0\x95\x2e\x8a\x42\xfa\xc3\x49\x17\xb9\xb5\x1a\xc3\x80".
"\x93\x0c\xd8\xe3\xcd\xa4\x38\x61\x7a\x22\x73\x8e\x32\x8f\x55".
"\x9c\x91\x08\xd9\x65\xa9\x02\x28\xc6\x59\xc8\x51\x32\x20\x48".
"\xea\x2c\xae\x0e\xa6\x35\x5b\xe2\x63\xf9\xf2\x9d\x5f\xe3\x45".
"\xdc\x41\xba\xfb\x40\xcc\x8d\xde\x6c\x3d\x50\x97\x9d\x83\xa0".
"\xda\x41\x61\xba\xaf\xf8\x74\xd2\x21\x7b\x09\xcc\x83\xe1\x08".
"\x01\x04\x42\xce\xcb\xec\x1d\x6b\xb7\x6f\x0f\x4b\xd4\x53\x90".
"\x55\x3b\xcf\x9f\x93\xb8\xad\xce\x5f\x13\x83\xb3\x89\x6f\x5a".
"\x1b\xa4\xf5\x95\x4b\xb4\x22\x22\x1d\x35\xaa\xfa\xc7\x14\x8c".
"\xcd\x50\x66\x14\x47\xff\x67\xb2\xf8\x12\x09\xb3\x8a\xe5\x7d".
"\xb8\xc9\xe4\x89\xf7\xa4\xb5\x70\xfa\x2d\xeb\x95\x89\xec\xbb".
"\x49\x59\xd2\xc1\x6d\x0e\x06\xe4\x5e\xd5\x13\x13\x0d\x72\x6e".
"\xf0\x6d\xa9\xd5\xe7\x54\x68\x35\xcd\xd0\xd5\xa6\xe5\xb2\xe4".
"\xb1\x19\xe4\xf1\xe3\x8a\x56\x4c\x3b\x3d\xb8\x03\xfe\x22\x2f".
"\xc6\xdc\x88\x7b\xca\x5c\xc6\xdd\x17\x34\x08\x22\xf0\x17\x61".
"\x0e\x60\x9c\xb4\x27\x57\x30\x6e\xb8\x4f\xdd\x25\x7b\xef\x9e".
"\x8e\x88\x6b\xd8\x10\x23\xc2\x44\x53\x73\x64\x8f\x40\x22\xe1".
"\xe8\xa2\xb0\x3f\x8a\x07\x66\xcd\x64\x4f\x9c\x1e\x89\x76\x04".
"\x6d\xab\xc2\xbb\x16\x85\x80\x01\xa5\xb1\xe2\x12\x04\x2e\x39".
"\x87\x8c\xee\xbc\xfb\x07\x6d\x03\x4c\x3a\xa5\x7b\x95\xd9\xd7".
"\xd6\xee\x2b\xe9\xcb\xe6\xec\xa8\x84\x6a\x42\xf9\xb2\x25\xc8".
"\xf3\x6a\xaa\x34\x3b\xd9\x72\xd9\x70\x81\x3b\xd4\x5e\x66\x97".
"\x1b\xe6\x2b\x88\x71\x82\xa3\x8a\x98\xb0\x16\xd9\xbb\x97\x8b".
"\x57\x79\x41\x56\x6e\xc2\x8f\xdf\xfa\x5b\xc7\x68\x5b\xb8\x09".
"\x41\x31\x7c\x19\xe1\x95\x2e\x05\x4c\xac\x38\x81\xda\xb3\x8b".
"\x3e\x1c\x79\x9a\x31\xac\x3e\x3d\x6d\xab\xf3\x5a\x5e\xc7\x6e".
"\x8e\x39\xcd\x7b\x6f\x62\xee\xb9\x73\xdd\x82\x42\x6f\x09\xe4".
"\xc3\xae\x92\xe8\x18\x99\xa0\x5e\xa2\x12\xf4\xe2\xe0\xe6\x95".
"\x58\x3a\x45\xad\xfe\x23\x79\x5f\x82\xce\x95\x88\x73\xeb\x46".
"\xc8\x00\xac\xc3\x2a\xdc\x7e\xab\x9b\xf8\xbb\x46\x5c\xa8\x46".
"\xbc\xfd\x99\xae\x4c\xa7\x77\xeb\x7c\x58\xbf\xbb\x52\x68\x62".
"\x3d\x0b\x79\x64\x38\x65\xa7\xcb\x7b\xe9\xb2\x33\xb5\x59\x52".
"\x7b\x17\xb4\x02\x2b\x07\x0d\x3a\x11\x57\x92\xa5\x22\x2b\xbc".
"\xe6\x97\x05\x12\x05\xe7\x91\xe3\xfa\xae\x15\xbe\x20\xe5\x5c".
"\x71\x24\x80\x85\xc9\x66\xc1\x53\x5c\x8f\x08\xd4\x52\xe1\x10".
"\xb6\xd6\x20\x08\x01\x79\x33\x9f\x1b\xbd\xa0\xab\x7c\xb1\xd9".
"\xdc\xca\x44\x22\x49\xb7\xb7\x3d\x84\xac\x92\xf4\xfa\x0a\xc9".
"\xc5\xb2\x42\x2b\x9a\x63\xbb\x8a\x82\x04\x2f\xf7\xe9\x30\x05".
"\x67\x32\xd1\x41\x1a\x69\x6e\xb9\xf8\x5f\x6d\xb7\xe5\x4e\x85".
"\x21\xfa\x16\x8a\x44\xfd\xf6\xd9\xa2\x5f\x68\x2b\xf3\xe2\x3c".
"\x8a\x69\xd2\xc1\x38\xed\x83\xef\x0d\x53\x86\x93\x32\x23\xc6".
"\x14\x0c\xb0\xb6\x6e\x77\xa4\x20\x0f\xb1\x6e\xe2\xce\xca\x6f".
"\x93\x1c\x3a\x8f\xd0\xd2\x5a\x6e\x30\xd6\x8e\x5f\x4b\xa5\xef".
"\xa9\x62\xeb\x28\xa0\x5e\x3f\xc1\xbc\x0a\x68\xab\xd7\xfa\xa2".
"\xb7\x8f\x12\xb0\x99\xbc\x93\x20\xb8\x95\x8d\xca\xc7\xa7\xd9".
"\x2e\x19\xac\x06\xb9\x4e\x56\x8e\x74\xef\x2a\x04\xd8\x75\x04".
"\x38\x2a\xc7\xa0\xa4\x89\xf3\xa4\x8a\xd4\x2c\x2c\x58\x6f\x00".
"\x03\x23\xb8\xaf\x02\x48\x7d\x50\x46\x6f\x5a\x08\x41\xe3\x56".
"\x6d\xcb\xe2\x4f\xea\x8e\xab\x74\xcd\xf9\xef\xcf\xf9\x1e\xf1".
"\xf8\xb9\x6c\xaa\x3b\x37\xd1\x21\x42\x67\xec\xd6\x44\x55\x33".
"\xe8\x1d\xa4\x18\xf3\x73\x82\xb4\x50\x59\xc2\x34\x36\x05\xeb";
$expect = Filesystem::readFile(dirname(__FILE__).'/base85/expect2.txt');
$expect = trim($expect);
$this->assertEqual(
$expect,
ArcanistBundle::encodeBase85($data));
}
}
diff --git a/src/parser/__tests__/ArcanistCommentRemoverTestCase.php b/src/parser/__tests__/ArcanistCommentRemoverTestCase.php
index cc559f19..26603e2b 100644
--- a/src/parser/__tests__/ArcanistCommentRemoverTestCase.php
+++ b/src/parser/__tests__/ArcanistCommentRemoverTestCase.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistCommentRemoverTestCase extends ArcanistTestCase {
public function testRemover() {
$test = <<<EOTEXT
Here is a list:
# Stuff
# More Stuff
The end.
# Instructional comments.
# Appear here.
# At the bottom.
EOTEXT;
$expect = <<<EOTEXT
Here is a list:
# Stuff
# More Stuff
The end.
EOTEXT;
$this->assertEqual($expect, ArcanistCommentRemover::removeComments($test));
}
}
diff --git a/src/parser/__tests__/ArcanistDiffParserTestCase.php b/src/parser/__tests__/ArcanistDiffParserTestCase.php
index ed664145..d1657d0c 100644
--- a/src/parser/__tests__/ArcanistDiffParserTestCase.php
+++ b/src/parser/__tests__/ArcanistDiffParserTestCase.php
@@ -1,639 +1,623 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Test cases for @{class:ArcanistDiffParser}.
*
* @group testcase
*/
final class ArcanistDiffParserTestCase extends ArcanistTestCase {
public function testParser() {
$root = dirname(__FILE__).'/diff/';
foreach (Filesystem::listDirectory($root, $hidden = false) as $file) {
$this->parseDiff($root.$file);
}
}
private function parseDiff($diff_file) {
$contents = Filesystem::readFile($diff_file);
$file = basename($diff_file);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($contents);
switch ($file) {
case 'colorized.hggitdiff':
$this->assertEqual(1, count($changes));
break;
case 'basic-missing-both-newlines-plus.udiff':
case 'basic-missing-both-newlines.udiff':
case 'basic-missing-new-newline-plus.udiff':
case 'basic-missing-new-newline.udiff':
case 'basic-missing-old-newline-plus.udiff':
case 'basic-missing-old-newline.udiff':
$expect_old = strpos($file, '-old-') || strpos($file, '-both-');
$expect_new = strpos($file, '-new-') || strpos($file, '-both-');
$expect_two = strpos($file, '-plus');
$this->assertEqual(count($changes), $expect_two ? 2 : 1);
$change = reset($changes);
$this->assertEqual(true, $change !== null);
$hunks = $change->getHunks();
$this->assertEqual(1, count($hunks));
$hunk = reset($hunks);
$this->assertEqual((bool)$expect_old, $hunk->getIsMissingOldNewline());
$this->assertEqual((bool)$expect_new, $hunk->getIsMissingNewNewline());
break;
case 'basic-binary.udiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$change->getFileType());
break;
case 'basic-multi-hunk.udiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$hunks = $change->getHunks();
$this->assertEqual(4, count($hunks));
$this->assertEqual('right', $change->getCurrentPath());
$this->assertEqual('left', $change->getOldPath());
break;
case 'basic-multi-hunk-content.svndiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$hunks = $change->getHunks();
$this->assertEqual(2, count($hunks));
$there_is_a_literal_trailing_space_here = ' ';
$corpus_0 = <<<EOCORPUS
asdfasdf
+% quack
%
-%
%%
%%
%%%
EOCORPUS;
$corpus_1 = <<<EOCORPUS
%%%%%
%%%%%
{$there_is_a_literal_trailing_space_here}
-!
+! quack
EOCORPUS;
$this->assertEqual(
$corpus_0,
$hunks[0]->getCorpus());
$this->assertEqual(
$corpus_1,
$hunks[1]->getCorpus());
break;
case 'svn-ignore-whitespace-only.svndiff':
$this->assertEqual(2, count($changes));
$hunks = reset($changes)->getHunks();
$this->assertEqual(0, count($hunks));
break;
case 'svn-property-add.svndiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$hunks = reset($changes)->getHunks();
$this->assertEqual(1, count($hunks));
$this->assertEqual(
array(
'duck' => 'quack',
),
$change->getNewProperties()
);
break;
case 'svn-property-modify.svndiff':
$this->assertEqual(2, count($changes));
$change = array_shift($changes);
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
array(
'svn:ignore' => '*.phpz',
),
$change->getOldProperties()
);
$this->assertEqual(
array(
'svn:ignore' => '*.php',
),
$change->getNewProperties()
);
$change = array_shift($changes);
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
array(
'svn:special' => '*',
),
$change->getOldProperties()
);
$this->assertEqual(
array(
'svn:special' => 'moo',
),
$change->getNewProperties()
);
break;
case 'svn-property-delete.svndiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
$change->getOldProperties(),
array(
'svn:special' => '*',
));
$this->assertEqual(
array(
),
$change->getNewProperties());
break;
case 'svn-property-merged.svndiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(count($change->getHunks()), 0);
$this->assertEqual(
$change->getOldProperties(),
array());
$this->assertEqual(
$change->getNewProperties(),
array());
break;
case 'svn-property-merge.svndiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(count($change->getHunks()), 0);
$this->assertEqual(
$change->getOldProperties(),
array(
));
$this->assertEqual(
$change->getNewProperties(),
array(
'svn:mergeinfo' => <<<EOTEXT
Merged /tfb/branches/internmove/www/html/js/help/UIFaq.js:r83462-126155
Merged /tfb/branches/ads-create-v3/www/html/js/help/UIFaq.js:r140558-142418
EOTEXT
));
break;
case 'svn-binary-add.svndiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$change->getFileType());
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
array(
'svn:mime-type' => 'application/octet-stream',
),
$change->getNewProperties()
);
break;
case 'svn-binary-diff.svndiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$change->getFileType());
$this->assertEqual(count($change->getHunks()), 0);
break;
case 'git-delete-file.gitdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$change->getType());
$this->assertEqual(
'scripts/intern/test/testfile2',
$change->getCurrentPath());
$this->assertEqual(1, count($change->getHunks()));
break;
case 'git-binary-change.gitdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$change->getFileType());
$this->assertEqual(0, count($change->getHunks()));
break;
case 'git-filemode-change.gitdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(1, count($change->getHunks()));
$this->assertEqual(
array(
'unix:filemode' => '100644',
),
$change->getOldProperties()
);
$this->assertEqual(
array(
'unix:filemode' => '100755',
),
$change->getNewProperties()
);
break;
case 'git-filemode-change-only.gitdiff':
$this->assertEqual(count($changes), 2);
$change = reset($changes);
$this->assertEqual(count($change->getHunks()), 0);
$this->assertEqual(
array(
'unix:filemode' => '100644',
),
$change->getOldProperties()
);
$this->assertEqual(
array(
'unix:filemode' => '100755',
),
$change->getNewProperties()
);
break;
case 'svn-empty-file.svndiff':
$this->assertEqual(2, count($changes));
$change = array_shift($changes);
$this->assertEqual(0, count($change->getHunks()));
break;
case 'git-ignore-whitespace-only.gitdiff':
$this->assertEqual(count($changes), 2);
$change = array_shift($changes);
$this->assertEqual(count($change->getHunks()), 0);
$this->assertEqual(
$change->getOldPath(),
'scripts/intern/test/testfile2');
$this->assertEqual(
$change->getCurrentPath(),
'scripts/intern/test/testfile2');
$change = array_shift($changes);
$this->assertEqual(count($change->getHunks()), 1);
$this->assertEqual(
$change->getOldPath(),
'scripts/intern/test/testfile3');
$this->assertEqual(
$change->getCurrentPath(),
'scripts/intern/test/testfile3');
break;
case 'git-move.gitdiff':
case 'git-move-edit.gitdiff':
case 'git-move-plus.gitdiff':
$extra_changeset = (bool)strpos($file, '-plus');
$has_hunk = (bool)strpos($file, '-edit');
$this->assertEqual($extra_changeset ? 3 : 2, count($changes));
$change = array_shift($changes);
$this->assertEqual($has_hunk ? 1 : 0,
count($change->getHunks()));
$this->assertEqual(
$change->getType(),
ArcanistDiffChangeType::TYPE_MOVE_HERE);
$target = $change;
$change = array_shift($changes);
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$change->getType()
);
$this->assertEqual(
$change->getCurrentPath(),
$target->getOldPath());
$this->assertEqual(
true,
in_array($target->getCurrentPath(), $change->getAwayPaths()));
break;
case 'git-merge-header.gitdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MESSAGE,
$change->getType());
$this->assertEqual(
'501f6d519703458471dbea6284ec5f49d1408598',
$change->getCommitHash());
break;
case 'git-new-file.gitdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$change->getType());
break;
case 'git-copy.gitdiff':
$this->assertEqual(2, count($changes));
$change = array_shift($changes);
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$change->getType());
$this->assertEqual(
'flib/intern/widgets/ui/UIWidgetRSSBox.php',
$change->getCurrentPath());
$change = array_shift($changes);
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_AWAY,
$change->getType());
$this->assertEqual(
'lib/display/intern/ui/widget/UIWidgetRSSBox.php',
$change->getCurrentPath());
break;
case 'git-copy-plus.gitdiff':
$this->assertEqual(2, count($changes));
$change = array_shift($changes);
$this->assertEqual(3, count($change->getHunks()));
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$change->getType());
$this->assertEqual(
'flib/intern/widgets/ui/UIWidgetGraphConnect.php',
$change->getCurrentPath());
$change = array_shift($changes);
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_AWAY,
$change->getType());
$this->assertEqual(
'lib/display/intern/ui/widget/UIWidgetLunchtime.php',
$change->getCurrentPath());
break;
case 'svn-property-multiline.svndiff':
$this->assertEqual(1, count($changes));
$change = array_shift($changes);
$this->assertEqual(0, count($change->getHunks()));
$this->assertEqual(
array(
'svn:ignore' => 'tags',
),
$change->getOldProperties()
);
$this->assertEqual(
array(
'svn:ignore' => "tags\nasdf\nlol\nwhat",
),
$change->getNewProperties()
);
break;
case 'git-empty-files.gitdiff':
$this->assertEqual(2, count($changes));
while ($change = array_shift($changes)) {
$this->assertEqual(0, count($change->getHunks()));
}
break;
case 'git-mnemonicprefix.gitdiff':
// Check parsing of diffs created with `diff.mnemonicprefix`
// configuration option set to `true`.
$this->assertEqual(1, count($changes));
$this->assertEqual(1, count(reset($changes)->getHunks()));
break;
case 'git-commit.gitdiff':
case 'git-commit-logdecorate.gitdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MESSAGE,
$change->getType());
$this->assertEqual(
'76e2f1339c298c748aa0b52030799ed202a6537b',
$change->getCommitHash());
$this->assertEqual(
<<<EOTEXT
Deprecating UIActionButton (Part 1)
Summary: Replaces calls to UIActionButton with <ui:button>. I tested most
of these calls, but there were some that I didn't know how to
reach, so if you are one of the owners of this code, please test
your feature in my sandbox: www.ngao.devrs013.facebook.com
@brosenthal, I removed some logic that was setting a disabled state
on a UIActionButton, which is actually a no-op.
Reviewed By: brosenthal
Other Commenters: sparker, egiovanola
Test Plan: www.ngao.devrs013.facebook.com
Explicitly tested:
* ads creation flow (add keyword)
* ads manager (conversion tracking)
* help center (create a discussion)
* new user wizard (next step button)
Revert: OK
DiffCamp Revision: 94064
git-svn-id: svn+ssh://tubbs/svnroot/tfb/trunk/www@223593 2c7ba8d8
EOTEXT
, $change->getMetadata('message')
);
break;
case 'git-binary.gitdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$change->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$change->getFileType());
break;
case 'git-odd-filename.gitdiff':
$this->assertEqual(2, count($changes));
$change = reset($changes);
$this->assertEqual(
'old/'."\342\210\206".'.jpg',
$change->getOldPath());
$this->assertEqual(
'new/'."\342\210\206".'.jpg',
$change->getCurrentPath());
break;
case 'hg-binary-change.hgdiff':
case 'hg-solo-binary-change.hgdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$change->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$change->getFileType());
break;
case 'hg-binary-delete.hgdiff':
$this->assertEqual(1, count($changes));
$change = reset($changes);
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$change->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$change->getFileType());
break;
case 'git-replace-symlink.gitdiff':
$this->assertEqual(1, count($changes));
$change = array_shift($changes);
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$change->getType());
break;
case 'svn-1.7-property-added.svndiff':
$this->assertEqual(1, count($changes));
$change = head($changes);
$new_properties = $change->getNewProperties();
$this->assertEqual(2, count($new_properties));
$this->assertEqual('*', idx($new_properties, 'svn:executable'));
$this->assertEqual('text/html', idx($new_properties, 'svn:mime-type'));
break;
case 'hg-diff-range.hgdiff':
$this->assertEqual(1, count($changes));
$change = array_shift($changes);
$this->assertEqual(
'Test.java',
$change->getOldPath());
$this->assertEqual(
'Test.java',
$change->getCurrentPath());
break;
case 'hg-patch.hgdiff':
$this->assertEqual(1, count($changes));
break;
case 'custom-prefixes.gitdiff':
$this->assertEqual(1, count($changes));
$change = head($changes);
$this->assertEqual(
'dst/file',
$change->getCurrentPath());
break;
case 'more-newlines.svndiff':
$this->assertEqual(1, count($changes));
break;
default:
throw new Exception("No test block for diff file {$diff_file}.");
break;
}
}
public function testGitPrefixStripping() {
static $tests = array(
'a/file.c' => 'file.c',
'b/file.c' => 'file.c',
'i/file.c' => 'file.c',
'c/file.c' => 'file.c',
'w/file.c' => 'file.c',
'o/file.c' => 'file.c',
'1/file.c' => 'file.c',
'2/file.c' => 'file.c',
'src/file.c' => 'src/file.c',
'file.c' => 'file.c',
);
foreach ($tests as $input => $expect) {
$this->assertEqual(
$expect,
ArcanistDiffParser::stripGitPathPrefix($input),
"Strip git prefix from '{$input}'.");
}
}
public function testGitPathSplitting() {
static $tests = array(
"a/old.c b/new.c" => array('old.c', 'new.c'),
"a/old.c b/new.c\n" => array('old.c', 'new.c'),
"a/old.c b/new.c\r\n" => array('old.c', 'new.c'),
"old.c new.c" => array('old.c', 'new.c'),
"1/old.c 2/new.c" => array('old.c', 'new.c'),
'"a/\\"quotes1\\"" "b/\\"quotes2\\""' => array(
'"quotes1"',
'"quotes2"',
),
'"a/\\"quotes and spaces1\\"" "b/\\"quotes and spaces2\\""' => array(
'"quotes and spaces1"',
'"quotes and spaces2"',
),
'"a/\\342\\230\\2031" "b/\\342\\230\\2032"' => array(
"\xE2\x98\x831",
"\xE2\x98\x832",
),
"a/Core Data/old.c b/Core Data/new.c" => array(
'Core Data/old.c',
'Core Data/new.c',
),
"some file with spaces.c some file with spaces.c" => array(
'some file with spaces.c',
'some file with spaces.c',
),
);
foreach ($tests as $input => $expect) {
$result = ArcanistDiffParser::splitGitDiffPaths($input);
$this->assertEqual(
$expect,
$result,
"Split: {$input}");
}
static $ambiguous = array(
"old file with spaces.c new file with spaces.c",
);
foreach ($ambiguous as $input) {
$caught = null;
try {
ArcanistDiffParser::splitGitDiffPaths($input);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
($caught instanceof Exception),
"Ambiguous: {$input}");
}
}
}
diff --git a/src/parser/diff/ArcanistDiffChange.php b/src/parser/diff/ArcanistDiffChange.php
index d811249b..bd9799b9 100644
--- a/src/parser/diff/ArcanistDiffChange.php
+++ b/src/parser/diff/ArcanistDiffChange.php
@@ -1,274 +1,258 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Represents a change to an individual path.
*
* @group diff
*/
final class ArcanistDiffChange {
protected $metadata = array();
protected $oldPath;
protected $currentPath;
protected $awayPaths = array();
protected $oldProperties = array();
protected $newProperties = array();
protected $commitHash;
protected $type = ArcanistDiffChangeType::TYPE_CHANGE;
protected $fileType = ArcanistDiffChangeType::FILE_TEXT;
protected $hunks = array();
private $needsSyntheticGitHunks;
private $currentFileData;
private $originalFileData;
public function setOriginalFileData($original_file_data) {
$this->originalFileData = $original_file_data;
return $this;
}
public function getOriginalFileData() {
return $this->originalFileData;
}
public function setCurrentFileData($current_file_data) {
$this->currentFileData = $current_file_data;
return $this;
}
public function getCurrentFileData() {
return $this->currentFileData;
}
public function toDictionary() {
$hunks = array();
foreach ($this->hunks as $hunk) {
$hunks[] = $hunk->toDictionary();
}
return array(
'metadata' => $this->metadata,
'oldPath' => $this->oldPath,
'currentPath' => $this->currentPath,
'awayPaths' => $this->awayPaths,
'oldProperties' => $this->oldProperties,
'newProperties' => $this->newProperties,
'type' => $this->type,
'fileType' => $this->fileType,
'commitHash' => $this->commitHash,
'hunks' => $hunks,
);
}
public static function newFromDictionary(array $dict) {
$hunks = array();
foreach ($dict['hunks'] as $hunk) {
$hunks[] = ArcanistDiffHunk::newFromDictionary($hunk);
}
$obj = new ArcanistDiffChange();
$obj->metadata = $dict['metadata'];
$obj->oldPath = $dict['oldPath'];
$obj->currentPath = $dict['currentPath'];
// TODO: The backend is shipping down some bogus data, e.g. diff 199453.
// Should probably clean this up.
$obj->awayPaths = nonempty($dict['awayPaths'], array());
$obj->oldProperties = nonempty($dict['oldProperties'], array());
$obj->newProperties = nonempty($dict['newProperties'], array());
$obj->type = $dict['type'];
$obj->fileType = $dict['fileType'];
$obj->commitHash = $dict['commitHash'];
$obj->hunks = $hunks;
return $obj;
}
public function getChangedLines($type) {
$lines = array();
foreach ($this->hunks as $hunk) {
$lines += $hunk->getChangedLines($type);
}
return $lines;
}
public function getAllMetadata() {
return $this->metadata;
}
public function setMetadata($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadata($key) {
return idx($this->metadata, $key);
}
public function setCommitHash($hash) {
$this->commitHash = $hash;
return $this;
}
public function getCommitHash() {
return $this->commitHash;
}
public function addAwayPath($path) {
$this->awayPaths[] = $path;
return $this;
}
public function getAwayPaths() {
return $this->awayPaths;
}
public function setFileType($type) {
$this->fileType = $type;
return $this;
}
public function getFileType() {
return $this->fileType;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setOldProperty($key, $value) {
$this->oldProperties[$key] = $value;
return $this;
}
public function setNewProperty($key, $value) {
$this->newProperties[$key] = $value;
return $this;
}
public function getOldProperties() {
return $this->oldProperties;
}
public function getNewProperties() {
return $this->newProperties;
}
public function setCurrentPath($path) {
$this->currentPath = $this->filterPath($path);
return $this;
}
public function getCurrentPath() {
return $this->currentPath;
}
public function setOldPath($path) {
$this->oldPath = $this->filterPath($path);
return $this;
}
public function getOldPath() {
return $this->oldPath;
}
public function addHunk(ArcanistDiffHunk $hunk) {
$this->hunks[] = $hunk;
return $this;
}
public function getHunks() {
return $this->hunks;
}
public function convertToBinaryChange() {
$this->hunks = array();
$this->setFileType(ArcanistDiffChangeType::FILE_BINARY);
return $this;
}
protected function filterPath($path) {
if ($path == '/dev/null') {
return null;
}
return $path;
}
public function renderTextSummary() {
$type = $this->getType();
$file = $this->getFileType();
$char = ArcanistDiffChangeType::getSummaryCharacterForChangeType($type);
$attr = ArcanistDiffChangeType::getShortNameForFileType($file);
if ($attr) {
$attr = '('.$attr.')';
}
$summary = array();
$summary[] = sprintf(
"%s %5.5s %s",
$char,
$attr,
$this->getCurrentPath());
if (ArcanistDiffChangeType::isOldLocationChangeType($type)) {
foreach ($this->getAwayPaths() as $path) {
$summary[] = ' to: '.$path;
}
}
if (ArcanistDiffChangeType::isNewLocationChangeType($type)) {
$summary[] = ' from: '.$this->getOldPath();
}
return implode("\n", $summary);
}
public function getSymlinkTarget() {
if ($this->getFileType() != ArcanistDiffChangeType::FILE_SYMLINK) {
throw new Exception("Not a symlink!");
}
$hunks = $this->getHunks();
$hunk = reset($hunks);
$corpus = $hunk->getCorpus();
$match = null;
if (!preg_match('/^\+(?:link )?(.*)$/m', $corpus, $match)) {
throw new Exception("Failed to extract link target!");
}
return trim($match[1]);
}
public function setNeedsSyntheticGitHunks($needs_synthetic_git_hunks) {
$this->needsSyntheticGitHunks = $needs_synthetic_git_hunks;
return $this;
}
public function getNeedsSyntheticGitHunks() {
return $this->needsSyntheticGitHunks;
}
}
diff --git a/src/parser/diff/ArcanistDiffChangeType.php b/src/parser/diff/ArcanistDiffChangeType.php
index fb3ea464..60dd3fc4 100644
--- a/src/parser/diff/ArcanistDiffChangeType.php
+++ b/src/parser/diff/ArcanistDiffChangeType.php
@@ -1,129 +1,113 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Defines constants for file types and operations in changesets.
*
* @group diff
*/
final class ArcanistDiffChangeType {
const TYPE_ADD = 1;
const TYPE_CHANGE = 2;
const TYPE_DELETE = 3;
const TYPE_MOVE_AWAY = 4;
const TYPE_COPY_AWAY = 5;
const TYPE_MOVE_HERE = 6;
const TYPE_COPY_HERE = 7;
const TYPE_MULTICOPY = 8;
const TYPE_MESSAGE = 9;
const TYPE_CHILD = 10;
const FILE_TEXT = 1;
const FILE_IMAGE = 2;
const FILE_BINARY = 3;
const FILE_DIRECTORY = 4;
const FILE_SYMLINK = 5;
const FILE_DELETED = 6;
const FILE_NORMAL = 7;
public static function getSummaryCharacterForChangeType($type) {
static $types = array(
self::TYPE_ADD => 'A',
self::TYPE_CHANGE => 'M',
self::TYPE_DELETE => 'D',
self::TYPE_MOVE_AWAY => 'V',
self::TYPE_COPY_AWAY => 'P',
self::TYPE_MOVE_HERE => 'V',
self::TYPE_COPY_HERE => 'P',
self::TYPE_MULTICOPY => 'P',
self::TYPE_MESSAGE => 'Q',
self::TYPE_CHILD => '@',
);
return idx($types, coalesce($type, '?'), '~');
}
public static function getShortNameForFileType($type) {
static $names = array(
self::FILE_TEXT => null,
self::FILE_DIRECTORY => 'dir',
self::FILE_IMAGE => 'img',
self::FILE_BINARY => 'bin',
self::FILE_SYMLINK => 'sym',
);
return idx($names, coalesce($type, '?'), '???');
}
public static function isOldLocationChangeType($type) {
static $types = array(
ArcanistDiffChangeType::TYPE_MOVE_AWAY => true,
ArcanistDiffChangeType::TYPE_COPY_AWAY => true,
ArcanistDiffChangeType::TYPE_MULTICOPY => true,
);
return isset($types[$type]);
}
public static function isNewLocationChangeType($type) {
static $types = array(
ArcanistDiffChangeType::TYPE_MOVE_HERE => true,
ArcanistDiffChangeType::TYPE_COPY_HERE => true,
);
return isset($types[$type]);
}
public static function isDeleteChangeType($type) {
static $types = array(
ArcanistDiffChangeType::TYPE_DELETE => true,
ArcanistDiffChangeType::TYPE_MOVE_AWAY => true,
ArcanistDiffChangeType::TYPE_MULTICOPY => true,
);
return isset($types[$type]);
}
public static function isCreateChangeType($type) {
static $types = array(
ArcanistDiffChangeType::TYPE_ADD => true,
ArcanistDiffChangeType::TYPE_COPY_HERE => true,
ArcanistDiffChangeType::TYPE_MOVE_HERE => true,
);
return isset($types[$type]);
}
public static function isModifyChangeType($type) {
static $types = array(
ArcanistDiffChangeType::TYPE_CHANGE => true,
);
return isset($types[$type]);
}
public static function getFullNameForChangeType($type) {
static $types = array(
self::TYPE_ADD => 'Added',
self::TYPE_CHANGE => 'Modified',
self::TYPE_DELETE => 'Deleted',
self::TYPE_MOVE_AWAY => 'Moved Away',
self::TYPE_COPY_AWAY => 'Copied Away',
self::TYPE_MOVE_HERE => 'Moved Here',
self::TYPE_COPY_HERE => 'Copied Here',
self::TYPE_MULTICOPY => 'Deleted After Multiple Copy',
self::TYPE_MESSAGE => 'Commit Message',
self::TYPE_CHILD => 'Contents Modified',
);
return idx($types, coalesce($type, '?'), 'Unknown');
}
}
diff --git a/src/parser/diff/ArcanistDiffHunk.php b/src/parser/diff/ArcanistDiffHunk.php
index 150815de..78269f43 100644
--- a/src/parser/diff/ArcanistDiffHunk.php
+++ b/src/parser/diff/ArcanistDiffHunk.php
@@ -1,189 +1,173 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Represents a contiguous set of added and removed lines in a diff.
*
* @group diff
*/
final class ArcanistDiffHunk {
protected $oldOffset;
protected $oldLength;
protected $newOffset;
protected $newLength;
protected $addLines;
protected $delLines;
protected $isMissingOldNewline = false;
protected $isMissingNewNewline = false;
protected $corpus;
public function toDictionary() {
return array(
'oldOffset' => $this->oldOffset,
'newOffset' => $this->newOffset,
'oldLength' => $this->oldLength,
'newLength' => $this->newLength,
'addLines' => $this->addLines,
'delLines' => $this->delLines,
'isMissingOldNewline' => $this->isMissingOldNewline,
'isMissingNewNewline' => $this->isMissingNewNewline,
'corpus' => (string)$this->corpus,
);
}
public static function newFromDictionary(array $dict) {
$obj = new ArcanistDiffHunk();
$obj->oldOffset = $dict['oldOffset'];
$obj->newOffset = $dict['newOffset'];
$obj->oldLength = $dict['oldLength'];
$obj->newLength = $dict['newLength'];
$obj->addLines = $dict['addLines'];
$obj->delLines = $dict['delLines'];
$obj->isMissingOldNewline = $dict['isMissingOldNewline'];
$obj->isMissingNewNewline = $dict['isMissingNewNewline'];
$obj->corpus = $dict['corpus'];
return $obj;
}
public function getChangedLines($type) {
$old_map = array();
$new_map = array();
$cover_map = array();
$oline = $this->getOldOffset();
$nline = $this->getNewOffset();
foreach (explode("\n", $this->getCorpus()) as $line) {
$char = strlen($line) ? $line[0] : '~';
switch ($char) {
case '-':
$old_map[$oline] = true;
$cover_map[$oline] = true;
++$oline;
break;
case '+':
$new_map[$nline] = true;
if ($oline > 1) {
$cover_map[$oline - 1] = true;
}
$cover_map[$oline] = true;
++$nline;
break;
default:
++$oline;
++$nline;
break;
}
}
switch ($type) {
case 'new':
return $new_map;
case 'old':
return $old_map;
case 'cover':
return $cover_map;
default:
throw new Exception("Unknown line change type '{$type}'.");
}
}
public function setOldOffset($old_offset) {
$this->oldOffset = $old_offset;
return $this;
}
public function getOldOffset() {
return $this->oldOffset;
}
public function setNewOffset($new_offset) {
$this->newOffset = $new_offset;
return $this;
}
public function getNewOffset() {
return $this->newOffset;
}
public function setOldLength($old_length) {
$this->oldLength = $old_length;
return $this;
}
public function getOldLength() {
return $this->oldLength;
}
public function setNewLength($new_length) {
$this->newLength = $new_length;
return $this;
}
public function getNewLength() {
return $this->newLength;
}
public function setAddLines($add_lines) {
$this->addLines = $add_lines;
return $this;
}
public function getAddLines() {
return $this->addLines;
}
public function setDelLines($del_lines) {
$this->delLines = $del_lines;
return $this;
}
public function getDelLines() {
return $this->delLines;
}
public function setCorpus($corpus) {
$this->corpus = $corpus;
return $this;
}
public function getCorpus() {
return $this->corpus;
}
public function setIsMissingOldNewline($missing) {
$this->isMissingOldNewline = (bool)$missing;
return $this;
}
public function getIsMissingOldNewline() {
return $this->isMissingOldNewline;
}
public function setIsMissingNewNewline($missing) {
$this->isMissingNewNewline = (bool)$missing;
return $this;
}
public function getIsMissingNewNewline() {
return $this->isMissingNewNewline;
}
}
diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php
index 34113f45..e9a170c5 100644
--- a/src/repository/api/ArcanistGitAPI.php
+++ b/src/repository/api/ArcanistGitAPI.php
@@ -1,974 +1,958 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Interfaces with Git working copies.
*
* @group workingcopy
*/
final class ArcanistGitAPI extends ArcanistRepositoryAPI {
private $status;
private $relativeCommit = null;
private $repositoryHasNoCommits = false;
const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16;
/**
* For the repository's initial commit, 'git diff HEAD^' and similar do
* not work. Using this instead does work.
*/
const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
public static function newHookAPI($root) {
return new ArcanistGitAPI($root);
}
protected function buildLocalFuture(array $argv) {
$argv[0] = 'git '.$argv[0];
$future = newv('ExecFuture', $argv);
$future->setCWD($this->getPath());
return $future;
}
public function getSourceControlSystemName() {
return 'git';
}
public function getMetadataPath() {
return $this->getPath('.git');
}
public function getHasCommits() {
return !$this->repositoryHasNoCommits;
}
public function setRelativeCommit($relative_commit) {
$this->relativeCommit = $relative_commit;
return $this;
}
public function getLocalCommitInformation() {
if ($this->repositoryHasNoCommits) {
// Zero commits.
throw new Exception(
"You can't get local commit information for a repository with no ".
"commits.");
} else if ($this->relativeCommit == self::GIT_MAGIC_ROOT_COMMIT) {
// One commit.
$against = 'HEAD';
} else {
// 2..N commits. We include commits reachable from HEAD which are
// not reachable from the relative commit; this is consistent with
// user expectations even though it is not actually the diff range.
// Particularly:
//
// |
// D <----- master branch
// |
// C Y <- feature branch
// | /|
// B X
// | /
// A
// |
//
// If "A, B, C, D" are master, and the user is at Y, when they run
// "arc diff B" they want (and get) a diff of B vs Y, but they think about
// this as being the commits X and Y. If we log "B..Y", we only show
// Y. With "Y --not B", we show X and Y.
$against = csprintf('%s --not %s', 'HEAD', $this->getRelativeCommit());
}
// NOTE: Windows escaping of "%" symbols apparently is inherently broken;
// when passed throuhgh escapeshellarg() they are replaced with spaces.
// TODO: Learn how cmd.exe works and find some clever workaround?
// NOTE: If we use "%x00", output is truncated in Windows.
list($info) = $this->execxLocal(
phutil_is_windows()
? 'log %C --format=%C --'
: 'log %C --format=%s --',
$against,
// NOTE: "%B" is somewhat new, use "%s%n%n%b" instead.
'%H%x01%T%x01%P%x01%at%x01%an%x01%s%x01%s%n%n%b%x02');
$commits = array();
$info = trim($info, " \n\2");
if (!strlen($info)) {
return array();
}
$info = explode("\2", $info);
foreach ($info as $line) {
list($commit, $tree, $parents, $time, $author, $title, $message)
= explode("\1", trim($line), 7);
$message = rtrim($message);
$commits[$commit] = array(
'commit' => $commit,
'tree' => $tree,
'parents' => array_filter(explode(' ', $parents)),
'time' => $time,
'author' => $author,
'summary' => $title,
'message' => $message,
);
}
return $commits;
}
public function getRelativeCommit() {
if ($this->relativeCommit === null) {
// Detect zero-commit or one-commit repositories. There is only one
// relative-commit value that makes any sense in these repositories: the
// empty tree.
list($err) = $this->execManualLocal('rev-parse --verify HEAD^');
if ($err) {
list($err) = $this->execManualLocal('rev-parse --verify HEAD');
if ($err) {
$this->repositoryHasNoCommits = true;
}
$this->relativeCommit = self::GIT_MAGIC_ROOT_COMMIT;
if ($this->repositoryHasNoCommits) {
$this->setBaseCommitExplanation(
"the repository has no commits.");
} else {
$this->setBaseCommitExplanation(
"the repository has only one commit.");
}
return $this->relativeCommit;
}
if ($this->getBaseCommitArgumentRules() ||
$this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) {
$base = $this->resolveBaseCommit();
if (!$base) {
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
$this->relativeCommit = $base;
return $this->relativeCommit;
}
$do_write = false;
$default_relative = null;
$working_copy = $this->getWorkingCopyIdentity();
if ($working_copy) {
$default_relative = $working_copy->getConfig(
'git.default-relative-commit');
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in 'git.default-relative-commit' in '.arcconfig'. This ".
"setting overrides other settings.");
}
if (!$default_relative) {
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
$default_relative = trim($upstream);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' (the Git upstream ".
"of the current branch) HEAD.");
}
}
if (!$default_relative) {
$default_relative = $this->readScratchFile('default-relative-commit');
$default_relative = trim($default_relative);
if ($default_relative) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in '.git/arc/default-relative-commit'.");
}
}
if (!$default_relative) {
// TODO: Remove the history lesson soon.
echo phutil_console_format(
"<bg:green>** Select a Default Commit Range **</bg>\n\n");
echo phutil_console_wrap(
"You're running a command which operates on a range of revisions ".
"(usually, from some revision to HEAD) but have not specified the ".
"revision that should determine the start of the range.\n\n".
"Previously, arc assumed you meant 'HEAD^' when you did not specify ".
"a start revision, but this behavior does not make much sense in ".
"most workflows outside of Facebook's historic git-svn workflow.\n\n".
"arc no longer assumes 'HEAD^'. You must specify a relative commit ".
"explicitly when you invoke a command (e.g., `arc diff HEAD^`, not ".
"just `arc diff`) or select a default for this working copy.\n\n".
"In most cases, the best default is 'origin/master'. You can also ".
"select 'HEAD^' to preserve the old behavior, or some other remote ".
"or branch. But you almost certainly want to select ".
"'origin/master'.\n\n".
"(Technically: the merge-base of the selected revision and HEAD is ".
"used to determine the start of the commit range.)");
$prompt = "What default do you want to use? [origin/master]";
$default = phutil_console_prompt($prompt);
if (!strlen(trim($default))) {
$default = 'origin/master';
}
$default_relative = $default;
$do_write = true;
}
list($object_type) = $this->execxLocal(
'cat-file -t %s',
$default_relative);
if (trim($object_type) !== 'commit') {
throw new Exception(
"Relative commit '{$default_relative}' is not the name of a commit!");
}
if ($do_write) {
// Don't perform this write until we've verified that the object is a
// valid commit name.
$this->writeScratchFile('default-relative-commit', $default_relative);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as you ".
"just specified.");
}
list($merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$default_relative);
$this->relativeCommit = trim($merge_base);
}
return $this->relativeCommit;
}
private function getDiffFullOptions($detect_moves_and_renames = true) {
$options = array(
self::getDiffBaseOptions(),
'--no-color',
'--src-prefix=a/',
'--dst-prefix=b/',
'-U'.$this->getDiffLinesOfContext(),
);
if ($detect_moves_and_renames) {
$options[] = '-M';
$options[] = '-C';
}
return implode(' ', $options);
}
private function getDiffBaseOptions() {
$options = array(
// Disable external diff drivers, like graphical differs, since Arcanist
// needs to capture the diff text.
'--no-ext-diff',
// Disable textconv so we treat binary files as binary, even if they have
// an alternative textual representation. TODO: Ideally, Differential
// would ship up the binaries for 'arc patch' but display the textconv
// output in the visual diff.
'--no-textconv',
);
return implode(' ', $options);
}
public function getFullGitDiff() {
$options = $this->getDiffFullOptions();
list($stdout) = $this->execxLocal(
"diff {$options} %s --",
$this->getRelativeCommit());
return $stdout;
}
/**
* @param string Path to generate a diff for.
* @param bool If true, detect moves and renames. Otherwise, ignore
* moves/renames; this is useful because it prompts git to
* generate real diff text.
*/
public function getRawDiffText($path, $detect_moves_and_renames = true) {
$options = $this->getDiffFullOptions($detect_moves_and_renames);
list($stdout) = $this->execxLocal(
"diff {$options} %s -- %s",
$this->getRelativeCommit(),
$path);
return $stdout;
}
public function getBranchName() {
// TODO: consider:
//
// $ git rev-parse --abbrev-ref `git symbolic-ref HEAD`
//
// But that may fail if you're not on a branch.
list($stdout) = $this->execxLocal('branch --no-color');
// Assume that any branch beginning with '(' means 'no branch', or whatever
// 'no branch' is in the current locale.
$matches = null;
if (preg_match('/^\* ([^\(].*)$/m', $stdout, $matches)) {
return $matches[1];
}
return null;
}
public function getSourceControlPath() {
// TODO: Try to get something useful here.
return null;
}
public function getGitCommitLog() {
$relative = $this->getRelativeCommit();
if ($this->repositoryHasNoCommits) {
// No commits yet.
return '';
} else if ($relative == self::GIT_MAGIC_ROOT_COMMIT) {
// First commit.
list($stdout) = $this->execxLocal(
'log --format=medium HEAD');
} else {
// 2..N commits.
list($stdout) = $this->execxLocal(
'log --first-parent --format=medium %s..HEAD',
$this->getRelativeCommit());
}
return $stdout;
}
public function getGitHistoryLog() {
list($stdout) = $this->execxLocal(
'log --format=medium -n%d %s',
self::SEARCH_LENGTH_FOR_PARENT_REVISIONS,
$this->getRelativeCommit());
return $stdout;
}
public function getSourceControlBaseRevision() {
list($stdout) = $this->execxLocal(
'rev-parse %s',
$this->getRelativeCommit());
return rtrim($stdout, "\n");
}
public function getCanonicalRevisionName($string) {
$match = null;
if (preg_match('/@([0-9]+)$/', $string, $match)) {
list($stdout) = $this->execxLocal(
'svn find-rev r%d',
$match[1]);
} else {
list($stdout) = $this->execxLocal(
'show -s --format=%C %s',
'%H',
$string);
}
return rtrim($stdout);
}
public function getWorkingCopyStatus() {
if (!isset($this->status)) {
$options = $this->getDiffBaseOptions();
// -- parallelize these slow cpu bound git calls.
// Find committed changes.
$committed_future = $this->buildLocalFuture(
array(
"diff {$options} --raw %s --",
$this->getRelativeCommit(),
));
// Find uncommitted changes.
$uncommitted_future = $this->buildLocalFuture(
array(
"diff {$options} --raw %s --",
$this->repositoryHasNoCommits
? self::GIT_MAGIC_ROOT_COMMIT
: 'HEAD',
));
// Untracked files
$untracked_future = $this->buildLocalFuture(
array(
'ls-files --others --exclude-standard',
));
// TODO: This doesn't list unstaged adds. It's not clear how to get that
// list other than "git status --porcelain" and then parsing it. :/
// Unstaged changes
$unstaged_future = $this->buildLocalFuture(
array(
'ls-files -m',
));
$futures = array(
$committed_future,
$uncommitted_future,
$untracked_future,
$unstaged_future
);
Futures($futures)->resolveAll();
// -- read back and process the results
list($stdout, $stderr) = $committed_future->resolvex();
$files = $this->parseGitStatus($stdout);
list($stdout, $stderr) = $uncommitted_future->resolvex();
$uncommitted_files = $this->parseGitStatus($stdout);
foreach ($uncommitted_files as $path => $mask) {
$mask |= self::FLAG_UNCOMMITTED;
if (!isset($files[$path])) {
$files[$path] = 0;
}
$files[$path] |= $mask;
}
list($stdout, $stderr) = $untracked_future->resolvex();
$stdout = rtrim($stdout, "\n");
if (strlen($stdout)) {
$stdout = explode("\n", $stdout);
foreach ($stdout as $file) {
$files[$file] = self::FLAG_UNTRACKED;
}
}
list($stdout, $stderr) = $unstaged_future->resolvex();
$stdout = rtrim($stdout, "\n");
if (strlen($stdout)) {
$stdout = explode("\n", $stdout);
foreach ($stdout as $file) {
$files[$file] = isset($files[$file])
? ($files[$file] | self::FLAG_UNSTAGED)
: self::FLAG_UNSTAGED;
}
}
$this->status = $files;
}
return $this->status;
}
public function amendCommit($message) {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$this->execxLocal(
'commit --amend --allow-empty -F %s',
$tmp_file);
}
public function getPreReceiveHookStatus($old_ref, $new_ref) {
$options = $this->getDiffBaseOptions();
list($stdout) = $this->execxLocal(
"diff {$options} --raw %s %s --",
$old_ref,
$new_ref);
return $this->parseGitStatus($stdout, $full = true);
}
private function parseGitStatus($status, $full = false) {
static $flags = array(
'A' => self::FLAG_ADDED,
'M' => self::FLAG_MODIFIED,
'D' => self::FLAG_DELETED,
);
$status = trim($status);
$lines = array();
foreach (explode("\n", $status) as $line) {
if ($line) {
$lines[] = preg_split("/[ \t]/", $line);
}
}
$files = array();
foreach ($lines as $line) {
$mask = 0;
$flag = $line[4];
$file = $line[5];
foreach ($flags as $key => $bits) {
if ($flag == $key) {
$mask |= $bits;
}
}
if ($full) {
$files[$file] = array(
'mask' => $mask,
'ref' => rtrim($line[3], '.'),
);
} else {
$files[$file] = $mask;
}
}
return $files;
}
public function getBlame($path) {
// TODO: 'git blame' supports --porcelain and we should probably use it.
list($stdout) = $this->execxLocal(
'blame --date=iso -w -M %s -- %s',
$this->getRelativeCommit(),
$path);
$blame = array();
foreach (explode("\n", trim($stdout)) as $line) {
if (!strlen($line)) {
continue;
}
// lines predating a git repo's history are blamed to the oldest revision,
// with the commit hash prepended by a ^. we shouldn't count these lines
// as blaming to the oldest diff's unfortunate author
if ($line[0] == '^') {
continue;
}
$matches = null;
$ok = preg_match(
'/^([0-9a-f]+)[^(]+?[(](.*?) +\d\d\d\d-\d\d-\d\d/',
$line,
$matches);
if (!$ok) {
throw new Exception("Bad blame? `{$line}'");
}
$revision = $matches[1];
$author = $matches[2];
$blame[] = array($author, $revision);
}
return $blame;
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getRelativeCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision($path, 'HEAD');
}
private function parseGitTree($stdout) {
$result = array();
$stdout = trim($stdout);
if (!strlen($stdout)) {
return $result;
}
$lines = explode("\n", $stdout);
foreach ($lines as $line) {
$matches = array();
$ok = preg_match(
'/^(\d{6}) (blob|tree) ([a-z0-9]{40})[\t](.*)$/',
$line,
$matches);
if (!$ok) {
throw new Exception("Failed to parse git ls-tree output!");
}
$result[$matches[4]] = array(
'mode' => $matches[1],
'type' => $matches[2],
'ref' => $matches[3],
);
}
return $result;
}
private function getFileDataAtRevision($path, $revision) {
// NOTE: We don't want to just "git show {$revision}:{$path}" since if the
// path was a directory at the given revision we'll get a list of its files
// and treat it as though it as a file containing a list of other files,
// which is silly.
list($stdout) = $this->execxLocal(
'ls-tree %s -- %s',
$revision,
$path);
$info = $this->parseGitTree($stdout);
if (empty($info[$path])) {
// No such path, or the path is a directory and we executed 'ls-tree dir/'
// and got a list of its contents back.
return null;
}
if ($info[$path]['type'] != 'blob') {
// Path is or was a directory, not a file.
return null;
}
list($stdout) = $this->execxLocal(
'cat-file blob %s',
$info[$path]['ref']);
return $stdout;
}
/**
* Returns names of all the branches in the current repository.
*
* @return list<dict<string, string>> Dictionary of branch information.
*/
public function getAllBranches() {
list($branch_info) = $this->execxLocal(
'branch --no-color');
$lines = explode("\n", rtrim($branch_info));
$result = array();
foreach ($lines as $line) {
if (preg_match('/^[* ]+\(no branch\)/', $line)) {
// This is indicating that the working copy is in a detached state;
// just ignore it.
continue;
}
list($current, $name) = preg_split('/\s+/', $line, 2);
$result[] = array(
'current' => !empty($current),
'name' => $name,
);
}
return $result;
}
public function getWorkingCopyRevision() {
list($stdout) = $this->execxLocal('rev-parse HEAD');
return rtrim($stdout, "\n");
}
public function isHistoryDefaultImmutable() {
return false;
}
public function supportsAmend() {
return true;
}
public function supportsRelativeLocalCommits() {
return true;
}
public function setDefaultBaseCommit() {
$this->setRelativeCommit('HEAD^');
return $this;
}
public function hasLocalCommit($commit) {
try {
if (!$this->getCanonicalRevisionName($commit)) {
return false;
}
} catch (CommandException $exception) {
return false;
}
return true;
}
public function parseRelativeLocalCommit(array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException("Specify only one commit.");
}
$base = reset($argv);
if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
$merge_base = $base;
$this->setBaseCommitExplanation(
"you explicitly specified the empty tree.");
} else {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$base);
if ($err) {
throw new ArcanistUsageException(
"Unable to find any git commit named '{$base}' in this repository.");
}
$this->setBaseCommitExplanation(
"it is the merge-base of '{$base}' and HEAD, as you explicitly ".
"specified.");
}
$this->setRelativeCommit(trim($merge_base));
}
public function getAllLocalChanges() {
$diff = $this->getFullGitDiff();
if (!strlen(trim($diff))) {
return array();
}
$parser = new ArcanistDiffParser();
return $parser->parseDiff($diff);
}
public function supportsLocalBranchMerge() {
return true;
}
public function performLocalBranchMerge($branch, $message) {
if (!$branch) {
throw new ArcanistUsageException(
"Under git, you must specify the branch you want to merge.");
}
$err = phutil_passthru(
'(cd %s && git merge --no-ff -m %s %s)',
$this->getPath(),
$message,
$branch);
if ($err) {
throw new ArcanistUsageException("Merge failed!");
}
}
public function getFinalizedRevisionMessage() {
return "You may now push this commit upstream, as appropriate (e.g. with ".
"'git push', or 'git svn dcommit', or by printing and faxing it).";
}
public function getCommitMessage($commit) {
list($message) = $this->execxLocal(
'log -n1 --format=%C %s --',
'%s%n%n%b',
$commit);
return $message;
}
public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query) {
$messages = $this->getGitCommitLog();
if (!strlen($messages)) {
return array();
}
$parser = new ArcanistDiffParser();
$messages = $parser->parseDiff($messages);
// First, try to find revisions by explicit revision IDs in commit messages.
$reason_map = array();
$revision_ids = array();
foreach ($messages as $message) {
$object = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$message->getMetadata('message'));
if ($object->getRevisionID()) {
$revision_ids[] = $object->getRevisionID();
$reason_map[$object->getRevisionID()] = $message->getCommitHash();
}
}
if ($revision_ids) {
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'ids' => $revision_ids,
));
foreach ($results as $key => $result) {
$hash = substr($reason_map[$result['id']], 0, 16);
$results[$key]['why'] =
"Commit message for '{$hash}' has explicit 'Differential Revision'.";
}
return $results;
}
// If we didn't succeed, try to find revisions by hash.
$hashes = array();
foreach ($this->getLocalCommitInformation() as $commit) {
$hashes[] = array('gtcm', $commit['commit']);
$hashes[] = array('gttr', $commit['tree']);
}
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'commitHashes' => $hashes,
));
foreach ($results as $key => $result) {
$results[$key]['why'] =
"A git commit or tree hash in the commit range is already attached ".
"to the Differential revision.";
}
return $results;
}
public function updateWorkingCopy() {
$this->execxLocal('pull');
}
public function getCommitSummary($commit) {
if ($commit == self::GIT_MAGIC_ROOT_COMMIT) {
return '(The Empty Tree)';
}
list($summary) = $this->execxLocal(
'log -n 1 --format=%C %s',
'%s',
$commit);
return trim($summary);
}
public function resolveBaseCommitRule($rule, $source) {
list($type, $name) = explode(':', $rule, 2);
switch ($type) {
case 'git':
$matches = null;
if (preg_match('/^merge-base\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$matches[1]);
if (!$err) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$matches[1]}' and HEAD, as ".
"specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return trim($merge_base);
}
} else if (preg_match('/^branch-unique\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$matches[1]);
if ($err) {
return null;
}
$merge_base = trim($merge_base);
list($commits) = $this->execxLocal(
'log --format=%C %s..HEAD --',
'%H',
$merge_base);
$commits = array_filter(explode("\n", $commits));
if (!$commits) {
return null;
}
$commits[] = $merge_base;
$head_branch_count = null;
foreach ($commits as $commit) {
list($branches) = $this->execxLocal(
'branch --contains %s',
$commit);
$branches = array_filter(explode("\n", $branches));
if ($head_branch_count === null) {
// If this is the first commit, it's HEAD. Count how many
// branches it is on; we want to include commits on the same
// number of branches. This covers a case where this branch
// has sub-branches and we're running "arc diff" here again
// for whatever reason.
$head_branch_count = count($branches);
} else if (count($branches) > $head_branch_count) {
foreach ($branches as $key => $branch) {
$branches[$key] = trim($branch, ' *');
}
$branches = implode(', ', $branches);
$this->setBaseCommitExplanation(
"it is the first commit between '{$merge_base}' (the ".
"merge-base of '{$matches[1]}' and HEAD) which is also ".
"contained by another branch ({$branches}).");
return $commit;
}
}
} else {
list($err) = $this->execManualLocal(
'cat-file -t %s',
$name);
if (!$err) {
$this->setBaseCommitExplanation(
"it is specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return $name;
}
}
break;
case 'arc':
switch ($name) {
case 'empty':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return self::GIT_MAGIC_ROOT_COMMIT;
case 'amended':
$text = $this->getCommitMessage('HEAD');
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$text);
if ($message->getRevisionID()) {
$this->setBaseCommitExplanation(
"HEAD has been amended with 'Differential Revision:', ".
"as specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return 'HEAD^';
}
break;
case 'upstream':
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
list($upstream_merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$upstream);
$this->setBaseCommitExplanation(
"it is the merge-base of the upstream of the current branch ".
"and HEAD, and matched the rule '{$rule}' in your {$source} ".
"'base' configuration.");
return $upstream_merge_base;
}
break;
}
default:
return null;
}
return null;
}
}
diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php
index a552481f..dd890d82 100644
--- a/src/repository/api/ArcanistMercurialAPI.php
+++ b/src/repository/api/ArcanistMercurialAPI.php
@@ -1,749 +1,733 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Interfaces with the Mercurial working copies.
*
* @group workingcopy
*/
final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private $status;
private $base;
private $relativeCommit;
private $branch;
private $workingCopyRevision;
private $localCommitInfo;
private $includeDirectoryStateInDiffs;
protected function buildLocalFuture(array $argv) {
// Mercurial has a "defaults" feature which basically breaks automation by
// allowing the user to add random flags to any command. This feature is
// "deprecated" and "a bad idea" that you should "forget ... existed"
// according to project lead Matt Mackall:
//
// http://markmail.org/message/hl3d6eprubmkkqh5
//
// There is an HGPLAIN environmental variable which enables "plain mode"
// and hopefully disables this stuff.
if (phutil_is_windows()) {
$argv[0] = 'set HGPLAIN=1 & hg '.$argv[0];
} else {
$argv[0] = 'HGPLAIN=1 hg '.$argv[0];
}
$future = newv('ExecFuture', $argv);
$future->setCWD($this->getPath());
return $future;
}
public function getSourceControlSystemName() {
return 'hg';
}
public function getMetadataPath() {
return $this->getPath('.hg');
}
public function getSourceControlBaseRevision() {
return $this->getCanonicalRevisionName($this->getRelativeCommit());
}
public function getCanonicalRevisionName($string) {
list($stdout) = $this->execxLocal(
'log -l 1 --template %s -r %s --',
'{node}',
$string);
return $stdout;
}
public function getSourceControlPath() {
return '/';
}
public function getBranchName() {
if (!$this->branch) {
list($stdout) = $this->execxLocal('branch');
$this->branch = trim($stdout);
}
return $this->branch;
}
public function setRelativeCommit($commit) {
try {
$commit = $this->getCanonicalRevisionName($commit);
} catch (Exception $ex) {
throw new ArcanistUsageException(
"Commit '{$commit}' is not a valid Mercurial commit identifier.");
}
$this->relativeCommit = $commit;
$this->status = null;
$this->localCommitInfo = null;
return $this;
}
public function getRelativeCommit() {
if (empty($this->relativeCommit)) {
if ($this->getBaseCommitArgumentRules() ||
$this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) {
$base = $this->resolveBaseCommit();
if (!$base) {
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
$this->relativeCommit = $base;
return $this->relativeCommit;
}
list($err, $stdout) = $this->execManualLocal(
'outgoing --branch %s --style default',
$this->getBranchName());
if (!$err) {
$logs = ArcanistMercurialParser::parseMercurialLog($stdout);
} else {
// Mercurial (in some versions?) raises an error when there's nothing
// outgoing.
$logs = array();
}
if (!$logs) {
$this->setBaseCommitExplanation(
"you have no outgoing commits, so arc assumes you intend to submit ".
"uncommitted changes in the working copy.");
// In Mercurial, we support operations against uncommitted changes.
$this->setRelativeCommit($this->getWorkingCopyRevision());
return $this->relativeCommit;
}
$outgoing_revs = ipull($logs, 'rev');
// This is essentially an implementation of a theoretical `hg merge-base`
// command.
$against = $this->getWorkingCopyRevision();
while (true) {
// NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is
// new as of July 2011, so do this in a compatible way. Also, "hg log"
// and "hg outgoing" don't necessarily show parents (even if given an
// explicit template consisting of just the parents token) so we need
// to separately execute "hg parents".
list($stdout) = $this->execxLocal(
'parents --style default --rev %s',
$against);
$parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
list($p1, $p2) = array_merge($parents_logs, array(null, null));
if ($p1 && !in_array($p1['rev'], $outgoing_revs)) {
$against = $p1['rev'];
break;
} else if ($p2 && !in_array($p2['rev'], $outgoing_revs)) {
$against = $p2['rev'];
break;
} else if ($p1) {
$against = $p1['rev'];
} else {
// This is the case where you have a new repository and the entire
// thing is outgoing; Mercurial literally accepts "--rev null" as
// meaning "diff against the empty state".
$against = 'null';
break;
}
}
if ($against == 'null') {
$this->setBaseCommitExplanation(
"this is a new repository (all changes are outgoing).");
} else {
$this->setBaseCommitExplanation(
"it is the first commit reachable from the working copy state ".
"which is not outgoing.");
}
$this->setRelativeCommit($against);
}
return $this->relativeCommit;
}
public function getLocalCommitInformation() {
if ($this->localCommitInfo === null) {
list($info) = $this->execxLocal(
"log --template '%C' --rev %s --branch %s --",
"{node}\1{rev}\1{author}\1{date|rfc822date}\1".
"{branch}\1{tag}\1{parents}\1{desc}\2",
'(ancestors(.) - ancestors('.$this->getRelativeCommit().'))',
$this->getBranchName());
$logs = array_filter(explode("\2", $info));
$last_node = null;
$futures = array();
$commits = array();
foreach ($logs as $log) {
list($node, $rev, $author, $date, $branch, $tag, $parents, $desc) =
explode("\1", $log);
// NOTE: If a commit has only one parent, {parents} returns empty.
// If it has two parents, {parents} returns revs and short hashes, not
// full hashes. Try to avoid making calls to "hg parents" because it's
// relatively expensive.
$commit_parents = null;
if (!$parents) {
if ($last_node) {
$commit_parents = array($last_node);
}
}
if (!$commit_parents) {
// We didn't get a cheap hit on previous commit, so do the full-cost
// "hg parents" call. We can run these in parallel, at least.
$futures[$node] = $this->execFutureLocal(
"parents --template='{node}\\n' --rev %s",
$node);
}
$commits[$node] = array(
'author' => $author,
'time' => strtotime($date),
'branch' => $branch,
'tag' => $tag,
'commit' => $node,
'rev' => $node, // TODO: Remove eventually.
'local' => $rev,
'parents' => $commit_parents,
'summary' => head(explode("\n", $desc)),
'message' => $desc,
);
$last_node = $node;
}
foreach (Futures($futures)->limit(4) as $node => $future) {
list($parents) = $future->resolvex();
$parents = array_filter(explode("\n", $parents));
$commits[$node]['parents'] = $parents;
}
// Put commits in newest-first order, to be consistent with Git and the
// expected order of "hg log" and "git log" under normal circumstances.
// The order of ancestors() is oldest-first.
$commits = array_reverse($commits);
$this->localCommitInfo = $commits;
}
return $this->localCommitInfo;
}
public function getBlame($path) {
list($stdout) = $this->execxLocal(
'annotate -u -v -c --rev %s -- %s',
$this->getRelativeCommit(),
$path);
$blame = array();
foreach (explode("\n", trim($stdout)) as $line) {
if (!strlen($line)) {
continue;
}
$matches = null;
$ok = preg_match('/^\s*([^:]+?) [a-f0-9]{12}: (.*)$/', $line, $matches);
if (!$ok) {
throw new Exception("Unable to parse Mercurial blame line: {$line}");
}
$revision = $matches[2];
$author = trim($matches[1]);
$blame[] = array($author, $revision);
}
return $blame;
}
public function getWorkingCopyStatus() {
if (!isset($this->status)) {
// A reviewable revision spans multiple local commits in Mercurial, but
// there is no way to get file change status across multiple commits, so
// just take the entire diff and parse it to figure out what's changed.
$diff = $this->getFullMercurialDiff();
if (!$diff) {
$this->status = array();
return $this->status;
}
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
$status_map = array();
foreach ($changes as $change) {
$flags = 0;
switch ($change->getType()) {
case ArcanistDiffChangeType::TYPE_ADD:
case ArcanistDiffChangeType::TYPE_MOVE_HERE:
case ArcanistDiffChangeType::TYPE_COPY_HERE:
$flags |= self::FLAG_ADDED;
break;
case ArcanistDiffChangeType::TYPE_CHANGE:
case ArcanistDiffChangeType::TYPE_COPY_AWAY: // Check for changes?
$flags |= self::FLAG_MODIFIED;
break;
case ArcanistDiffChangeType::TYPE_DELETE:
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
case ArcanistDiffChangeType::TYPE_MULTICOPY:
$flags |= self::FLAG_DELETED;
break;
}
$status_map[$change->getCurrentPath()] = $flags;
}
list($stdout) = $this->execxLocal('status');
$working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
foreach ($working_status as $path => $status) {
if ($status & ArcanistRepositoryAPI::FLAG_UNTRACKED) {
// If the file is untracked, don't mark it uncommitted.
continue;
}
$status |= self::FLAG_UNCOMMITTED;
if (!empty($status_map[$path])) {
$status_map[$path] |= $status;
} else {
$status_map[$path] = $status;
}
}
$this->status = $status_map;
}
return $this->status;
}
private function getDiffOptions() {
$options = array(
'--git',
'-U'.$this->getDiffLinesOfContext(),
);
return implode(' ', $options);
}
public function getRawDiffText($path) {
$options = $this->getDiffOptions();
// NOTE: In Mercurial, "--rev x" means "diff between x and the working
// copy state", while "--rev x..." means "diff between x and the working
// copy commit" (i.e., from 'x' to '.'). The latter excludes any dirty
// changes in the working copy.
$range = $this->getRelativeCommit();
if (!$this->includeDirectoryStateInDiffs) {
$range .= '...';
}
list($stdout) = $this->execxLocal(
'diff %C --rev %s -- %s',
$options,
$range,
$path);
return $stdout;
}
public function getFullMercurialDiff() {
return $this->getRawDiffText('');
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getRelativeCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision(
$path,
$this->getWorkingCopyRevision());
}
private function getFileDataAtRevision($path, $revision) {
list($err, $stdout) = $this->execManualLocal(
'cat --rev %s -- %s',
$revision,
$path);
if ($err) {
// Assume this is "no file at revision", i.e. a deleted or added file.
return null;
} else {
return $stdout;
}
}
public function getWorkingCopyRevision() {
return '.';
}
public function isHistoryDefaultImmutable() {
return true;
}
public function supportsAmend() {
list($err, $stdout) = $this->execManualLocal('help commit');
if ($err) {
return false;
} else {
return (strpos($stdout, "amend") !== false);
}
}
public function supportsRelativeLocalCommits() {
return true;
}
public function setDefaultBaseCommit() {
$this->setRelativeCommit('.^');
return $this;
}
public function hasLocalCommit($commit) {
try {
$this->getCanonicalRevisionName($commit);
return true;
} catch (Exception $ex) {
return false;
}
}
public function getCommitMessage($commit) {
list($message) = $this->execxLocal(
'log --template={desc} --rev %s',
$commit);
return $message;
}
public function parseRelativeLocalCommit(array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException("Specify only one commit.");
}
$this->setBaseCommitExplanation("you explicitly specified it.");
// This does the "hg id" call we need to normalize/validate the revision
// identifier.
$this->setRelativeCommit(reset($argv));
}
public function getAllLocalChanges() {
$diff = $this->getFullMercurialDiff();
if (!strlen(trim($diff))) {
return array();
}
$parser = new ArcanistDiffParser();
return $parser->parseDiff($diff);
}
public function supportsLocalBranchMerge() {
return true;
}
public function performLocalBranchMerge($branch, $message) {
if ($branch) {
$err = phutil_passthru(
'(cd %s && HGPLAIN=1 hg merge --rev %s && hg commit -m %s)',
$this->getPath(),
$branch,
$message);
} else {
$err = phutil_passthru(
'(cd %s && HGPLAIN=1 hg merge && hg commit -m %s)',
$this->getPath(),
$message);
}
if ($err) {
throw new ArcanistUsageException("Merge failed!");
}
}
public function getFinalizedRevisionMessage() {
return "You may now push this commit upstream, as appropriate (e.g. with ".
"'hg push' or by printing and faxing it).";
}
public function getCommitMessageLog() {
list($stdout) = $this->execxLocal(
"log --template '{node}\\2{desc}\\1' --rev %s --branch %s --",
'ancestors(.) - ancestors('.$this->getRelativeCommit().')',
$this->getBranchName());
$map = array();
$logs = explode("\1", trim($stdout));
foreach (array_filter($logs) as $log) {
list($node, $desc) = explode("\2", $log);
$map[$node] = $desc;
}
return array_reverse($map);
}
public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query) {
$messages = $this->getCommitMessageLog();
$parser = new ArcanistDiffParser();
// First, try to find revisions by explicit revision IDs in commit messages.
$reason_map = array();
$revision_ids = array();
foreach ($messages as $node_id => $message) {
$object = ArcanistDifferentialCommitMessage::newFromRawCorpus($message);
if ($object->getRevisionID()) {
$revision_ids[] = $object->getRevisionID();
$reason_map[$object->getRevisionID()] = $node_id;
}
}
if ($revision_ids) {
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'ids' => $revision_ids,
));
foreach ($results as $key => $result) {
$hash = substr($reason_map[$result['id']], 0, 16);
$results[$key]['why'] =
"Commit message for '{$hash}' has explicit 'Differential Revision'.";
}
return $results;
}
// Try to find revisions by hash.
$hashes = array();
foreach ($this->getLocalCommitInformation() as $commit) {
$hashes[] = array('hgcm', $commit['commit']);
}
if ($hashes) {
// NOTE: In the case of "arc diff . --uncommitted" in a Mercurial working
// copy with dirty changes, there may be no local commits.
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'commitHashes' => $hashes,
));
foreach ($results as $key => $hash) {
$results[$key]['why'] =
"A mercurial commit hash in the commit range is already attached ".
"to the Differential revision.";
}
return $results;
}
return array();
}
public function updateWorkingCopy() {
$this->execxLocal('up');
}
public function amendCommit($message) {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$this->execxLocal(
'commit --amend -l %s',
$tmp_file);
}
public function setIncludeDirectoryStateInDiffs($include) {
$this->includeDirectoryStateInDiffs = $include;
return $this;
}
public function getCommitSummary($commit) {
if ($commit == 'null') {
return '(The Empty Void)';
}
list($summary) = $this->execxLocal(
'log --template {desc} --limit 1 --rev %s',
$commit);
$summary = head(explode("\n", $summary));
return trim($summary);
}
public function resolveBaseCommitRule($rule, $source) {
list($type, $name) = explode(':', $rule, 2);
switch ($type) {
case 'hg':
$matches = null;
if (preg_match('/^gca\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'log --template={node} --rev %s',
sprintf('ancestor(., %s)', $matches[1]));
if (!$err) {
$this->setBaseCommitExplanation(
"it is the greatest common ancestor of '{$matches[1]}' and ., as".
"specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return trim($merge_base);
}
} else {
list($err) = $this->execManualLocal(
'id -r %s',
$name);
if (!$err) {
$this->setBaseCommitExplanation(
"it is specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return $name;
}
}
break;
case 'arc':
switch ($name) {
case 'empty':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return 'null';
case 'outgoing':
list($err, $outgoing_base) = $this->execManualLocal(
'log --template={node} --rev %s',
'limit(reverse(ancestors(.) - outgoing()), 1)'
);
if (!$err) {
$this->setBaseCommitExplanation(
"it is the first ancestor of the working copy that is not ".
"outgoing, and it matched the rule {$rule} in your {$source} ".
"'base' configuration.");
return trim($outgoing_base);
}
case 'amended':
$text = $this->getCommitMessage('.');
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$text);
if ($message->getRevisionID()) {
$this->setBaseCommitExplanation(
"'.' has been amended with 'Differential Revision:', ".
"as specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
// NOTE: This should be safe because Mercurial doesn't support
// amend until 2.2.
return '.^';
}
break;
case 'bookmark':
$revset =
'limit('.
' sort('.
' (ancestors(.) and bookmark() - .) or'.
' (ancestors(.) - outgoing()), '.
' -rev),'.
'1)';
list($err, $bookmark_base) = $this->execManualLocal(
'log --template={node} --rev %s',
$revset);
if (!$err) {
$this->setBaseCommitExplanation(
"it is the first ancestor of . that either has a bookmark, or ".
"is already in the remote and it matched the rule {$rule} in ".
"your {$source} 'base' configuration");
return trim($bookmark_base);
}
}
break;
default:
return null;
}
return null;
}
public function getSubversionInfo() {
$info = array();
$base_path = null;
$revision = null;
list($err, $raw_info) = $this->execManualLocal('svn info');
if (!$err) {
foreach (explode("\n", trim($raw_info)) as $line) {
list($key, $value) = explode(': ', $line, 2);
switch ($key) {
case 'URL':
$info['base_path'] = $value;
$base_path = $value;
break;
case 'Repository UUID':
$info['uuid'] = $value;
break;
case 'Revision':
$revision = $value;
break;
default:
break;
}
}
if ($base_path && $revision) {
$info['base_revision'] = $base_path.'@'.$revision;
}
}
return $info;
}
public function getActiveBookmark() {
list($raw_output) = $this->execxLocal('bookmarks');
$raw_output = trim($raw_output);
if ($raw_output !== 'no bookmarks set') {
foreach (explode("\n", $raw_output) as $line) {
$line = trim($line);
if ('*' === $line[0]) {
return idx(explode(' ', $line, 3), 1);
}
}
}
return null;
}
}
diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php
index 4524ecd7..539ea7f0 100644
--- a/src/repository/api/ArcanistRepositoryAPI.php
+++ b/src/repository/api/ArcanistRepositoryAPI.php
@@ -1,442 +1,426 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Interfaces with the VCS in the working copy.
*
* @group workingcopy
*/
abstract class ArcanistRepositoryAPI {
const FLAG_MODIFIED = 1;
const FLAG_ADDED = 2;
const FLAG_DELETED = 4;
const FLAG_UNTRACKED = 8;
const FLAG_CONFLICT = 16;
const FLAG_MISSING = 32;
const FLAG_UNSTAGED = 64;
const FLAG_UNCOMMITTED = 128;
const FLAG_EXTERNALS = 256;
// Occurs in SVN when you replace a file with a directory without telling
// SVN about it.
const FLAG_OBSTRUCTED = 512;
// Occurs in SVN when an update was interrupted or failed, e.g. you ^C'd it.
const FLAG_INCOMPLETE = 1024;
protected $path;
protected $diffLinesOfContext = 0x7FFF;
private $baseCommitExplanation = '???';
private $workingCopyIdentity;
private $baseCommitArgumentRules;
abstract public function getSourceControlSystemName();
public function getDiffLinesOfContext() {
return $this->diffLinesOfContext;
}
public function setDiffLinesOfContext($lines) {
$this->diffLinesOfContext = $lines;
return $this;
}
public function getWorkingCopyIdentity() {
return $this->workingCopyIdentity;
}
public static function newAPIFromWorkingCopyIdentity(
ArcanistWorkingCopyIdentity $working_copy) {
$root = $working_copy->getProjectRoot();
if (!$root) {
throw new ArcanistUsageException(
"There is no readable '.arcconfig' file in the working directory or ".
"any parent directory. Create an '.arcconfig' file to configure arc.");
}
// check if we're in an svn working copy
list($err) = exec_manual('svn info');
if (!$err) {
$api = new ArcanistSubversionAPI($root);
$api->workingCopyIdentity = $working_copy;
return $api;
}
if (Filesystem::pathExists($root.'/.hg')) {
$api = new ArcanistMercurialAPI($root);
$api->workingCopyIdentity = $working_copy;
return $api;
}
$git_root = self::discoverGitBaseDirectory($root);
if ($git_root) {
if (!Filesystem::pathsAreEquivalent($root, $git_root)) {
throw new ArcanistUsageException(
"'.arcconfig' file is located at '{$root}', but working copy root ".
"is '{$git_root}'. Move '.arcconfig' file to the working copy root.");
}
$api = new ArcanistGitAPI($root);
$api->workingCopyIdentity = $working_copy;
return $api;
}
throw new ArcanistUsageException(
"The current working directory is not part of a working copy for a ".
"supported version control system (svn, git or mercurial).");
}
public function __construct($path) {
$this->path = $path;
}
public function getPath($to_file = null) {
if ($to_file !== null) {
return $this->path.DIRECTORY_SEPARATOR.
ltrim($to_file, DIRECTORY_SEPARATOR);
} else {
return $this->path.DIRECTORY_SEPARATOR;
}
}
public function getUntrackedChanges() {
return $this->getWorkingCopyFilesWithMask(self::FLAG_UNTRACKED);
}
public function getUnstagedChanges() {
return $this->getWorkingCopyFilesWithMask(self::FLAG_UNSTAGED);
}
public function getUncommittedChanges() {
return $this->getWorkingCopyFilesWithMask(self::FLAG_UNCOMMITTED);
}
public function getMergeConflicts() {
return $this->getWorkingCopyFilesWithMask(self::FLAG_CONFLICT);
}
public function getIncompleteChanges() {
return $this->getWorkingCopyFilesWithMask(self::FLAG_INCOMPLETE);
}
private function getWorkingCopyFilesWithMask($mask) {
$match = array();
foreach ($this->getWorkingCopyStatus() as $file => $flags) {
if ($flags & $mask) {
$match[] = $file;
}
}
return $match;
}
private static function discoverGitBaseDirectory($root) {
try {
// NOTE: This awkward construction is to make sure things work on Windows.
$future = new ExecFuture('git rev-parse --show-cdup');
$future->setCWD($root);
list($stdout) = $future->resolvex();
return Filesystem::resolvePath(rtrim($stdout, "\n"), $root);
} catch (CommandException $ex) {
if (preg_match('/^fatal: Not a git repository/', $ex->getStdErr())) {
return null;
}
throw $ex;
}
}
abstract public function getBlame($path);
abstract public function getWorkingCopyStatus();
abstract public function getRawDiffText($path);
abstract public function getOriginalFileData($path);
abstract public function getCurrentFileData($path);
abstract public function getLocalCommitInformation();
abstract public function getSourceControlBaseRevision();
abstract public function getCanonicalRevisionName($string);
abstract public function isHistoryDefaultImmutable();
abstract public function supportsAmend();
abstract public function supportsRelativeLocalCommits();
abstract public function getWorkingCopyRevision();
abstract public function updateWorkingCopy();
abstract public function getMetadataPath();
abstract public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query);
/**
* Set the base commit to a reasonable default value so that working copy
* status checks can do something meaningful and won't invoke configured
* 'base' rules.
*
* This is primarily useful for workflows which do not operate on commit
* ranges but need to verify the working copy is not dirty, like "amend",
* "upgrade" and "patch".
*
* @return this
*/
public function setDefaultBaseCommit() {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function amendCommit($message) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getAllBranches() {
// TODO: Implement for Mercurial/SVN and make abstract.
return array();
}
public function hasLocalCommit($commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getCommitMessage($commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function parseRelativeLocalCommit(array $argv) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getCommitSummary($commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getAllLocalChanges() {
throw new ArcanistCapabilityNotSupportedException($this);
}
abstract public function supportsLocalBranchMerge();
public function performLocalBranchMerge($branch, $message) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getFinalizedRevisionMessage() {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function execxLocal($pattern /* , ... */) {
$args = func_get_args();
return $this->buildLocalFuture($args)->resolvex();
}
public function execManualLocal($pattern /* , ... */) {
$args = func_get_args();
return $this->buildLocalFuture($args)->resolve();
}
public function execFutureLocal($pattern /* , ... */) {
$args = func_get_args();
return $this->buildLocalFuture($args);
}
abstract protected function buildLocalFuture(array $argv);
/* -( Scratch Files )------------------------------------------------------ */
/**
* Try to read a scratch file, if it exists and is readable.
*
* @param string Scratch file name.
* @return mixed String for file contents, or false for failure.
* @task scratch
*/
public function readScratchFile($path) {
$full_path = $this->getScratchFilePath($path);
if (!$full_path) {
return false;
}
if (!Filesystem::pathExists($full_path)) {
return false;
}
try {
$result = Filesystem::readFile($full_path);
} catch (FilesystemException $ex) {
return false;
}
return $result;
}
/**
* Try to write a scratch file, if there's somewhere to put it and we can
* write there.
*
* @param string Scratch file name to write.
* @param string Data to write.
* @return bool True on success, false on failure.
* @task scratch
*/
public function writeScratchFile($path, $data) {
$dir = $this->getScratchFilePath('');
if (!$dir) {
return false;
}
if (!Filesystem::pathExists($dir)) {
try {
Filesystem::createDirectory($dir);
} catch (Exception $ex) {
return false;
}
}
try {
Filesystem::writeFile($this->getScratchFilePath($path), $data);
} catch (FilesystemException $ex) {
return false;
}
return true;
}
/**
* Try to remove a scratch file.
*
* @param string Scratch file name to remove.
* @return bool True if the file was removed successfully.
* @task scratch
*/
public function removeScratchFile($path) {
$full_path = $this->getScratchFilePath($path);
if (!$full_path) {
return false;
}
try {
Filesystem::remove($full_path);
} catch (FilesystemException $ex) {
return false;
}
return true;
}
/**
* Get a human-readable description of the scratch file location.
*
* @param string Scratch file name.
* @return mixed String, or false on failure.
* @task scratch
*/
public function getReadableScratchFilePath($path) {
$full_path = $this->getScratchFilePath($path);
if ($full_path) {
return Filesystem::readablePath(
$full_path,
$this->getPath());
} else {
return false;
}
}
/**
* Get the path to a scratch file, if possible.
*
* @param string Scratch file name.
* @return mixed File path, or false on failure.
* @task scratch
*/
public function getScratchFilePath($path) {
$new_scratch_path = Filesystem::resolvePath(
'arc',
$this->getMetadataPath());
static $checked = false;
if (!$checked) {
$checked = true;
$old_scratch_path = $this->getPath('.arc');
// we only want to do the migration once
// unfortunately, people have checked in .arc directories which
// means that the old one may get recreated after we delete it
if (Filesystem::pathExists($old_scratch_path) &&
!Filesystem::pathExists($new_scratch_path)) {
Filesystem::createDirectory($new_scratch_path);
$existing_files = Filesystem::listDirectory($old_scratch_path, true);
foreach ($existing_files as $file) {
$new_path = Filesystem::resolvePath($file, $new_scratch_path);
$old_path = Filesystem::resolvePath($file, $old_scratch_path);
Filesystem::writeFile(
$new_path,
Filesystem::readFile($old_path));
}
Filesystem::remove($old_scratch_path);
}
}
return Filesystem::resolvePath($path, $new_scratch_path);
}
/* -( Base Commits )------------------------------------------------------- */
public function getBaseCommitExplanation() {
return $this->baseCommitExplanation;
}
public function setBaseCommitExplanation($explanation) {
$this->baseCommitExplanation = $explanation;
return $this;
}
public function resolveBaseCommitRule($rule, $source) {
return null;
}
public function setBaseCommitArgumentRules($base_commit_argument_rules) {
$this->baseCommitArgumentRules = $base_commit_argument_rules;
return $this;
}
public function getBaseCommitArgumentRules() {
return $this->baseCommitArgumentRules;
}
public function resolveBaseCommit() {
$working_copy = $this->getWorkingCopyIdentity();
$global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
$system_config = ArcanistBaseWorkflow::readSystemArcConfig();
$parser = new ArcanistBaseCommitParser($this);
$commit = $parser->resolveBaseCommit(
array(
'args' => $this->getBaseCommitArgumentRules(),
'local' => $working_copy->getLocalConfig('base', ''),
'project' => $working_copy->getConfig('base', ''),
'global' => idx($global_config, 'base', ''),
'system' => idx($system_config, 'base', ''),
));
return $commit;
}
}
diff --git a/src/repository/api/ArcanistSubversionAPI.php b/src/repository/api/ArcanistSubversionAPI.php
index ab1f680c..ab028586 100644
--- a/src/repository/api/ArcanistSubversionAPI.php
+++ b/src/repository/api/ArcanistSubversionAPI.php
@@ -1,602 +1,586 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Interfaces with Subversion working copies.
*
* @group workingcopy
*/
final class ArcanistSubversionAPI extends ArcanistRepositoryAPI {
protected $svnStatus;
protected $svnBaseRevisions;
protected $svnInfo = array();
protected $svnInfoRaw = array();
protected $svnDiffRaw = array();
private $svnBaseRevisionNumber;
public function getSourceControlSystemName() {
return 'svn';
}
public function getMetadataPath() {
static $svn_dir = null;
if ($svn_dir === null) {
// from svn 1.7, subversion keeps a single .svn directly under
// the working copy root. However, we allow .arcconfigs that
// aren't at the working copy root.
foreach (Filesystem::walkToRoot($this->getPath()) as $parent) {
$possible_svn_dir = Filesystem::resolvePath('.svn', $parent);
if (Filesystem::pathExists($possible_svn_dir)) {
$svn_dir = $possible_svn_dir;
break;
}
}
}
return $svn_dir;
}
protected function buildLocalFuture(array $argv) {
$argv[0] = 'svn '.$argv[0];
$future = newv('ExecFuture', $argv);
$future->setCWD($this->getPath());
return $future;
}
public function hasMergeConflicts() {
foreach ($this->getSVNStatus() as $path => $mask) {
if ($mask & self::FLAG_CONFLICT) {
return true;
}
}
return false;
}
public function getWorkingCopyStatus() {
return $this->getSVNStatus();
}
public function getSVNBaseRevisions() {
if ($this->svnBaseRevisions === null) {
$this->getSVNStatus();
}
return $this->svnBaseRevisions;
}
public function getSVNStatus($with_externals = false) {
if ($this->svnStatus === null) {
list($status) = $this->execxLocal('--xml status');
$xml = new SimpleXMLElement($status);
if (count($xml->target) != 1) {
throw new Exception("Expected exactly one XML status target.");
}
$externals = array();
$files = array();
$target = $xml->target[0];
$this->svnBaseRevisions = array();
foreach ($target->entry as $entry) {
$path = (string)$entry['path'];
$mask = 0;
$props = (string)($entry->{'wc-status'}[0]['props']);
$item = (string)($entry->{'wc-status'}[0]['item']);
$base = (string)($entry->{'wc-status'}[0]['revision']);
$this->svnBaseRevisions[$path] = $base;
switch ($props) {
case 'none':
case 'normal':
break;
case 'modified':
$mask |= self::FLAG_MODIFIED;
break;
default:
throw new Exception("Unrecognized property status '{$props}'.");
}
switch ($item) {
case 'normal':
break;
case 'external':
$mask |= self::FLAG_EXTERNALS;
$externals[] = $path;
break;
case 'unversioned':
$mask |= self::FLAG_UNTRACKED;
break;
case 'obstructed':
$mask |= self::FLAG_OBSTRUCTED;
break;
case 'missing':
$mask |= self::FLAG_MISSING;
break;
case 'added':
$mask |= self::FLAG_ADDED;
break;
case 'replaced':
// This is the result of "svn rm"-ing a file, putting another one
// in place of it, and then "svn add"-ing the new file. Just treat
// this as equivalent to "modified".
$mask |= self::FLAG_MODIFIED;
break;
case 'modified':
$mask |= self::FLAG_MODIFIED;
break;
case 'deleted':
$mask |= self::FLAG_DELETED;
break;
case 'conflicted':
$mask |= self::FLAG_CONFLICT;
break;
case 'incomplete':
$mask |= self::FLAG_INCOMPLETE;
break;
default:
throw new Exception("Unrecognized item status '{$item}'.");
}
// This is new in or around Subversion 1.6.
$tree_conflicts = (string)($entry->{'wc-status'}[0]['tree-conflicted']);
if ($tree_conflicts) {
$mask |= self::FLAG_CONFLICT;
}
$files[$path] = $mask;
}
foreach ($files as $path => $mask) {
foreach ($externals as $external) {
if (!strncmp($path, $external, strlen($external))) {
$files[$path] |= self::FLAG_EXTERNALS;
}
}
}
$this->svnStatus = $files;
}
$status = $this->svnStatus;
if (!$with_externals) {
foreach ($status as $path => $mask) {
if ($mask & ArcanistRepositoryAPI::FLAG_EXTERNALS) {
unset($status[$path]);
}
}
}
return $status;
}
public function getSVNProperty($path, $property) {
list($stdout) = execx(
'svn propget %s %s@',
$property,
$this->getPath($path));
return trim($stdout);
}
public function getSourceControlPath() {
return idx($this->getSVNInfo('/'), 'URL');
}
public function getSourceControlBaseRevision() {
$info = $this->getSVNInfo('/');
return $info['URL'].'@'.$this->getSVNBaseRevisionNumber();
}
public function getCanonicalRevisionName($string) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getSVNBaseRevisionNumber() {
if ($this->svnBaseRevisionNumber) {
return $this->svnBaseRevisionNumber;
}
$info = $this->getSVNInfo('/');
return $info['Revision'];
}
public function overrideSVNBaseRevisionNumber($effective_base_revision) {
$this->svnBaseRevisionNumber = $effective_base_revision;
return $this;
}
public function getBranchName() {
return 'svn';
}
public function buildInfoFuture($path) {
if ($path == '/') {
// When the root of a working copy is referenced by a symlink and you
// execute 'svn info' on that symlink, svn fails. This is a longstanding
// bug in svn:
//
// See http://subversion.tigris.org/issues/show_bug.cgi?id=2305
//
// To reproduce, do:
//
// $ ln -s working_copy working_link
// $ svn info working_copy # ok
// $ svn info working_link # fails
//
// Work around this by cd-ing into the directory before executing
// 'svn info'.
return $this->buildLocalFuture(array('info .'));
} else {
// Note: here and elsewhere we need to append "@" to the path because if
// a file has a literal "@" in it, everything after that will be
// interpreted as a revision. By appending "@" with no argument, SVN
// parses it properly.
return $this->buildLocalFuture(array('info %s@', $this->getPath($path)));
}
}
public function buildDiffFuture($path) {
// The "--depth empty" flag prevents us from picking up changes in
// children when we run 'diff' against a directory. Specifically, when a
// user has added or modified some directory "example/", we want to return
// ONLY changes to that directory when given it as a path. If we run
// without "--depth empty", svn will give us changes to the directory
// itself (such as property changes) and also give us changes to any
// files within the directory (basically, implicit recursion). We don't
// want that, so prevent recursive diffing.
$root = phutil_get_library_root('arcanist');
if (phutil_is_windows()) {
// TODO: Provide a binary_safe_diff script for Windows.
// TODO: Provide a diff command which can take lines of context somehow.
return $this->buildLocalFuture(
array(
'diff --depth empty %s',
$path,
));
} else {
$diff_bin = $root.'/../scripts/repository/binary_safe_diff.sh';
$diff_cmd = Filesystem::resolvePath($diff_bin);
return $this->buildLocalFuture(
array(
'diff --depth empty --diff-cmd %s -x -U%d %s',
$diff_cmd,
$this->getDiffLinesOfContext(),
$path,
));
}
}
public function primeSVNInfoResult($path, $result) {
$this->svnInfoRaw[$path] = $result;
return $this;
}
public function primeSVNDiffResult($path, $result) {
$this->svnDiffRaw[$path] = $result;
return $this;
}
public function getSVNInfo($path) {
if (empty($this->svnInfo[$path])) {
if (empty($this->svnInfoRaw[$path])) {
$this->svnInfoRaw[$path] = $this->buildInfoFuture($path)->resolve();
}
list($err, $stdout) = $this->svnInfoRaw[$path];
if ($err) {
throw new Exception(
"Error #{$err} executing svn info against '{$path}'.");
}
// TODO: Hack for Windows.
$stdout = str_replace("\r\n", "\n", $stdout);
$patterns = array(
'/^(URL): (\S+)$/m',
'/^(Revision): (\d+)$/m',
'/^(Last Changed Author): (\S+)$/m',
'/^(Last Changed Rev): (\d+)$/m',
'/^(Last Changed Date): (.+) \(.+\)$/m',
'/^(Copied From URL): (\S+)$/m',
'/^(Copied From Rev): (\d+)$/m',
'/^(Repository UUID): (\S+)$/m',
'/^(Node Kind): (\S+)$/m',
);
$result = array();
foreach ($patterns as $pattern) {
$matches = null;
if (preg_match($pattern, $stdout, $matches)) {
$result[$matches[1]] = $matches[2];
}
}
if (isset($result['Last Changed Date'])) {
$result['Last Changed Date'] = strtotime($result['Last Changed Date']);
}
if (empty($result)) {
throw new Exception('Unable to parse SVN info.');
}
$this->svnInfo[$path] = $result;
}
return $this->svnInfo[$path];
}
public function getRawDiffText($path) {
$status = $this->getSVNStatus();
if (!isset($status[$path])) {
return null;
}
$status = $status[$path];
// Build meaningful diff text for "svn copy" operations.
if ($status & ArcanistRepositoryAPI::FLAG_ADDED) {
$info = $this->getSVNInfo($path);
if (!empty($info['Copied From URL'])) {
return $this->buildSyntheticAdditionDiff(
$path,
$info['Copied From URL'],
$info['Copied From Rev']);
}
}
// If we run "diff" on a binary file which doesn't have the "svn:mime-type"
// of "application/octet-stream", `diff' will explode in a rain of
// unhelpful hellfire as it tries to build a textual diff of the two
// files. We just fix this inline since it's pretty unambiguous.
// TODO: Move this to configuration?
$matches = null;
if (preg_match('/\.(gif|png|jpe?g|swf|pdf|ico)$/i', $path, $matches)) {
$mime = $this->getSVNProperty($path, 'svn:mime-type');
if ($mime != 'application/octet-stream') {
execx(
'svn propset svn:mime-type application/octet-stream %s',
$this->getPath($path));
}
}
if (empty($this->svnDiffRaw[$path])) {
$this->svnDiffRaw[$path] = $this->buildDiffFuture($path)->resolve();
}
list($err, $stdout, $stderr) = $this->svnDiffRaw[$path];
// Note: GNU Diff returns 2 when SVN hands it binary files to diff and they
// differ. This is not an error; it is documented behavior. But SVN isn't
// happy about it. SVN will exit with code 1 and return the string below.
if ($err != 0 && $stderr !== "svn: 'diff' returned 2\n") {
throw new Exception(
"svn diff returned unexpected error code: $err\n".
"stdout: $stdout\n".
"stderr: $stderr");
}
if ($err == 0 && empty($stdout)) {
// If there are no changes, 'diff' exits with no output, but that means
// we can not distinguish between empty and unmodified files. Build a
// synthetic "diff" without any changes in it.
return $this->buildSyntheticUnchangedDiff($path);
}
return $stdout;
}
protected function buildSyntheticAdditionDiff($path, $source, $rev) {
$type = $this->getSVNProperty($path, 'svn:mime-type');
if ($type == 'application/octet-stream') {
return <<<EODIFF
Index: {$path}
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
EODIFF;
}
if (is_dir($this->getPath($path))) {
return null;
}
$data = Filesystem::readFile($this->getPath($path));
list($orig) = execx('svn cat %s@%s', $source, $rev);
$src = new TempFile();
$dst = new TempFile();
Filesystem::writeFile($src, $orig);
Filesystem::writeFile($dst, $data);
list($err, $diff) = exec_manual(
'diff -L a/%s -L b/%s -U%d %s %s',
str_replace($this->getSourceControlPath().'/', '', $source),
$path,
$this->getDiffLinesOfContext(),
$src,
$dst);
if ($err == 1) { // 1 means there are differences.
return <<<EODIFF
Index: {$path}
===================================================================
{$diff}
EODIFF;
} else {
return $this->buildSyntheticUnchangedDiff($path);
}
}
protected function buildSyntheticUnchangedDiff($path) {
$full_path = $this->getPath($path);
if (is_dir($full_path)) {
return null;
}
if (!file_exists($full_path)) {
return null;
}
$data = Filesystem::readFile($full_path);
$lines = explode("\n", $data);
$len = count($lines);
foreach ($lines as $key => $line) {
$lines[$key] = ' '.$line;
}
$lines = implode("\n", $lines);
return <<<EODIFF
Index: {$path}
===================================================================
--- {$path} (synthetic)
+++ {$path} (synthetic)
@@ -1,{$len} +1,{$len} @@
{$lines}
EODIFF;
}
public function getBlame($path) {
$blame = array();
list($stdout) = $this->execxLocal('blame %s', $path);
$stdout = trim($stdout);
if (!strlen($stdout)) {
// Empty file.
return $blame;
}
foreach (explode("\n", $stdout) as $line) {
$m = array();
if (!preg_match('/^\s*(\d+)\s+(\S+)/', $line, $m)) {
throw new Exception("Bad blame? `{$line}'");
}
$revision = $m[1];
$author = $m[2];
$blame[] = array($author, $revision);
}
return $blame;
}
public function getOriginalFileData($path) {
// SVN issues warnings for nonexistent paths, directories, etc., but still
// returns no error code. However, for new paths in the working copy it
// fails. Assume that failure means the original file does not exist.
list($err, $stdout) = $this->execManualLocal('cat %s@', $path);
if ($err) {
return null;
}
return $stdout;
}
public function getCurrentFileData($path) {
$full_path = $this->getPath($path);
if (Filesystem::pathExists($full_path)) {
return Filesystem::readFile($full_path);
}
return null;
}
public function getRepositorySVNUUID() {
$info = $this->getSVNInfo('/');
return $info['Repository UUID'];
}
public function getLocalCommitInformation() {
return null;
}
public function isHistoryDefaultImmutable() {
return true;
}
public function supportsAmend() {
return false;
}
public function supportsRelativeLocalCommits() {
return false;
}
public function hasLocalCommit($commit) {
return false;
}
public function getWorkingCopyRevision() {
return $this->getSourceControlBaseRevision();
}
public function supportsLocalBranchMerge() {
return false;
}
public function getFinalizedRevisionMessage() {
// In other VCSes we give push instructions here, but it never makes sense
// in SVN.
return "Done.";
}
public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query) {
// We don't have much to go on in SVN, look for revisions that came from
// this directory and belong to the same project.
$project = $this->getWorkingCopyIdentity()->getProjectID();
if (!$project) {
return array();
}
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'arcanistProjects' => array($project),
));
foreach ($results as $key => $result) {
if ($result['sourcePath'] != $this->getPath()) {
unset($results[$key]);
}
}
foreach ($results as $key => $result) {
$results[$key]['why'] =
"Matching arcanist project name and working copy directory path.";
}
return $results;
}
public function updateWorkingCopy() {
$this->execxLocal('up');
}
}
diff --git a/src/repository/hookapi/ArcanistHookAPI.php b/src/repository/hookapi/ArcanistHookAPI.php
index 3696dc66..7c461a31 100644
--- a/src/repository/hookapi/ArcanistHookAPI.php
+++ b/src/repository/hookapi/ArcanistHookAPI.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* API while running in the context of a commit hook
*/
abstract class ArcanistHookAPI {
abstract public function getCurrentFileData($path);
abstract public function getUpstreamFileData($path);
}
diff --git a/src/repository/hookapi/ArcanistSubversionHookAPI.php b/src/repository/hookapi/ArcanistSubversionHookAPI.php
index da5d337f..acefff2b 100644
--- a/src/repository/hookapi/ArcanistSubversionHookAPI.php
+++ b/src/repository/hookapi/ArcanistSubversionHookAPI.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Interfaces with Subversion while running as a commit hook.
*/
final class ArcanistSubversionHookAPI extends ArcanistHookAPI {
protected $root;
protected $transaction;
protected $repository;
public function __construct($root, $transaction, $repository) {
$this->root = $root;
$this->transaction = $transaction;
$this->repository = $repository;
}
public function getCurrentFileData($path) {
list($err, $file) = exec_manual(
'svnlook cat --transaction %s %s %s',
$this->transaction,
$this->repository,
$path);
return ($err? null : $file);
}
public function getUpstreamFileData($path) {
list($err, $file) = exec_manual(
'svnlook cat %s %s',
$this->repository,
$this->root."/$path");
return ($err ? null : $file);
}
}
diff --git a/src/repository/parser/ArcanistMercurialParser.php b/src/repository/parser/ArcanistMercurialParser.php
index 7847bd9f..814a95b7 100644
--- a/src/repository/parser/ArcanistMercurialParser.php
+++ b/src/repository/parser/ArcanistMercurialParser.php
@@ -1,230 +1,214 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Parses output from various "hg" commands into structured data. This class
* provides low-level APIs for reading "hg" output.
*
* @task parse Parsing "hg" Output
* @group workingcopy
*/
final class ArcanistMercurialParser {
/* -( Parsing "hg" Output )------------------------------------------------ */
/**
* Parse the output of "hg status". This provides detailed information, you
* can get less detailed information with @{method:parseMercurialStatus}. In
* particular, this will parse copy sources as per "hg status -C".
*
* @param string The stdout from running an "hg status" command.
* @return dict Map of paths to status dictionaries.
* @task parse
*/
public static function parseMercurialStatusDetails($stdout) {
$result = array();
$stdout = trim($stdout);
if (!strlen($stdout)) {
return $result;
}
$last_path = null;
$lines = explode("\n", $stdout);
foreach ($lines as $line) {
$flags = 0;
if ($line[1] !== ' ') {
throw new Exception("Unparsable Mercurial status line '{$line}'.");
}
$code = $line[0];
$path = substr($line, 2);
switch ($code) {
case 'A':
$flags |= ArcanistRepositoryAPI::FLAG_ADDED;
break;
case 'R':
$flags |= ArcanistRepositoryAPI::FLAG_DELETED;
break;
case 'M':
$flags |= ArcanistRepositoryAPI::FLAG_MODIFIED;
break;
case 'C':
// This is "clean" and included only for completeness, these files
// have not been changed.
break;
case '!':
$flags |= ArcanistRepositoryAPI::FLAG_MISSING;
break;
case '?':
$flags |= ArcanistRepositoryAPI::FLAG_UNTRACKED;
break;
case 'I':
// This is "ignored" and included only for completeness.
break;
case ' ':
// This shows the source of a file move, so update the last file we
// parsed to set its source.
if ($last_path === null) {
throw new Exception(
"Unexpected copy source in hg status, '{$line}'.");
}
$result[$last_path]['from'] = $path;
continue 2;
default:
throw new Exception("Unknown Mercurial status '{$code}'.");
}
$result[$path] = array(
'flags' => $flags,
'from' => null,
);
$last_path = $path;
}
return $result;
}
/**
* Parse the output of "hg status". This provides only basic information, you
* can get more detailed information by invoking
* @{method:parseMercurialStatusDetails}.
*
* @param string The stdout from running an "hg status" command.
* @return dict Map of paths to ArcanistRepositoryAPI status flags.
* @task parse
*/
public static function parseMercurialStatus($stdout) {
$result = self::parseMercurialStatusDetails($stdout);
return ipull($result, 'flags');
}
/**
* Parse the output of "hg log". This also parses "hg outgoing", "hg parents",
* and other similar commands. This assumes "--style default".
*
* @param string The stdout from running an "hg log" command.
* @return list List of dictionaries with commit information.
* @task parse
*/
public static function parseMercurialLog($stdout) {
$result = array();
$stdout = trim($stdout);
if (!strlen($stdout)) {
return $result;
}
$chunks = explode("\n\n", $stdout);
foreach ($chunks as $chunk) {
$commit = array();
$lines = explode("\n", $chunk);
foreach ($lines as $line) {
if (preg_match('/^(comparing with|searching for changes)/', $line)) {
// These are sent to stdout when you run "hg outgoing" although the
// format is otherwise identical to "hg log".
continue;
}
list($name, $value) = explode(':', $line, 2);
$value = trim($value);
switch ($name) {
case 'user':
$commit['user'] = $value;
break;
case 'date':
$commit['date'] = strtotime($value);
break;
case 'summary':
$commit['summary'] = $value;
break;
case 'changeset':
list($local, $rev) = explode(':', $value, 2);
$commit['local'] = $local;
$commit['rev'] = $rev;
break;
case 'parent':
if (empty($commit['parents'])) {
$commit['parents'] = array();
}
list($local, $rev) = explode(':', $value, 2);
$commit['parents'][] = array(
'local' => $local,
'rev' => $rev,
);
break;
case 'branch':
$commit['branch'] = $value;
break;
case 'tag':
$commit['tag'] = $value;
break;
case 'bookmark':
$commit['bookmark'] = $value;
break;
default:
throw new Exception("Unknown Mercurial log field '{$name}'!");
}
}
$result[] = $commit;
}
return $result;
}
/**
* Parse the output of "hg branches".
*
* @param string The stdout from running an "hg branches" command.
* @return list A list of dictionaries with branch information.
* @task parse
*/
public static function parseMercurialBranches($stdout) {
$lines = explode("\n", trim($stdout));
$branches = array();
foreach ($lines as $line) {
$matches = null;
// Output of "hg branches" normally looks like:
//
// default 15101:a21ccf4412d5
//
// ...but may also have human-readable cues like:
//
// stable 15095:ec222a29bdf0 (inactive)
//
// See the unit tests for more examples.
$regexp = '/^(\S+(?:\s+\S+)*)\s+(\d+):([a-f0-9]+)(\s+\\(inactive\\))?$/';
if (!preg_match($regexp, $line, $matches)) {
throw new Exception("Failed to parse 'hg branches' output: {$line}");
}
$branches[$matches[1]] = array(
'local' => $matches[2],
'rev' => $matches[3],
);
}
return $branches;
}
}
diff --git a/src/repository/parser/__tests__/ArcanistMercurialParserTestCase.php b/src/repository/parser/__tests__/ArcanistMercurialParserTestCase.php
index e54237cc..418dbfb5 100644
--- a/src/repository/parser/__tests__/ArcanistMercurialParserTestCase.php
+++ b/src/repository/parser/__tests__/ArcanistMercurialParserTestCase.php
@@ -1,100 +1,84 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
final class ArcanistMercurialParserTestCase extends ArcanistTestCase {
public function testParseAll() {
$root = dirname(__FILE__).'/mercurial/';
foreach (Filesystem::listDirectory($root, $hidden = false) as $file) {
$this->parseData(
basename($file),
Filesystem::readFile($root.'/'.$file));
}
}
private function parseData($name, $data) {
switch ($name) {
case 'branches-basic.txt':
$output = ArcanistMercurialParser::parseMercurialBranches($data);
$this->assertEqual(
array('default', 'stable'),
array_keys($output));
$this->assertEqual(
array('a21ccf4412d5', 'ec222a29bdf0'),
array_values(ipull($output, 'rev')));
break;
case 'branches-with-spaces.txt':
$output = ArcanistMercurialParser::parseMercurialBranches($data);
$this->assertEqual(
array(
'm m m m m 2:ffffffffffff (inactive)',
'xxx yyy zzz',
'default',
"'",
),
array_keys($output));
$this->assertEqual(
array('0b9d8290c4e0', '78963faacfc7', '5db03c5500c6', 'ffffffffffff'),
array_values(ipull($output, 'rev')));
break;
case 'log-basic.txt':
$output = ArcanistMercurialParser::parseMercurialLog($data);
$this->assertEqual(
3,
count($output));
$this->assertEqual(
array('a21ccf4412d5', 'a051f8a6a7cc', 'b1f49efeab65'),
array_values(ipull($output, 'rev')));
break;
case 'log-empty.txt':
// Empty logs (e.g., "hg parents" for a root revision) should parse
// correctly.
$output = ArcanistMercurialParser::parseMercurialLog($data);
$this->assertEqual(
array(),
$output);
break;
case 'status-basic.txt':
$output = ArcanistMercurialParser::parseMercurialStatus($data);
$this->assertEqual(
4,
count($output));
$this->assertEqual(
array('changed', 'added', 'removed', 'untracked'),
array_keys($output));
break;
case 'status-moves.txt':
$output = ArcanistMercurialParser::parseMercurialStatusDetails($data);
$this->assertEqual(
'move_source',
$output['moved_file']['from']);
$this->assertEqual(
null,
$output['changed_file']['from']);
$this->assertEqual(
'copy_source',
$output['copied_file']['from']);
$this->assertEqual(
null,
idx($output, 'copy_source'));
break;
default:
throw new Exception("No test information for test data '{$name}'!");
}
}
}
diff --git a/src/unit/ArcanistUnitTestResult.php b/src/unit/ArcanistUnitTestResult.php
index 3b43f60a..7e6efc29 100644
--- a/src/unit/ArcanistUnitTestResult.php
+++ b/src/unit/ArcanistUnitTestResult.php
@@ -1,144 +1,128 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Represents the outcome of running a unit test.
*
* @group unit
*/
final class ArcanistUnitTestResult {
const RESULT_PASS = 'pass';
const RESULT_FAIL = 'fail';
const RESULT_SKIP = 'skip';
const RESULT_BROKEN = 'broken';
const RESULT_UNSOUND = 'unsound';
const RESULT_POSTPONED = 'postponed';
private $namespace;
private $name;
private $link;
private $result;
private $duration;
private $userData;
private $extraData;
private $coverage;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setLink($link) {
$this->link = $link;
return $this;
}
public function getLink() {
return $this->link;
}
public function setResult($result) {
$this->result = $result;
return $this;
}
public function getResult() {
return $this->result;
}
public function getConsoleFormattedResult() {
static $status_codes = array(
self::RESULT_PASS => '<bg:green>** PASS **</bg>',
self::RESULT_FAIL => '<bg:red>** FAIL **</bg>',
self::RESULT_SKIP => '<bg:yellow>** SKIP **</bg>',
self::RESULT_BROKEN => '<bg:red>** BROKEN **</bg>',
self::RESULT_UNSOUND => '<bg:yellow>** UNSOUND **</bg>',
self::RESULT_POSTPONED => '<bg:yellow>** POSTPONED **</bg>',
);
return phutil_console_format($status_codes[$this->result]);
}
public function setDuration($duration) {
$this->duration = $duration;
return $this;
}
public function getDuration() {
return $this->duration;
}
public function setUserData($user_data) {
$this->userData = $user_data;
return $this;
}
public function getUserData() {
return $this->userData;
}
/**
* "extra data" allows an implementation to store additional
* key/value metadata along with the result of the test run.
*/
public function setExtraData(array $extra_data = null) {
$this->extraData = $extra_data;
return $this;
}
public function getExtraData() {
return $this->extraData;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
public function getCoverage() {
return $this->coverage;
}
/**
* Merge several coverage reports into a comprehensive coverage report.
*
* @param list List of coverage report strings.
* @return string Cumulative coverage report.
*/
public static function mergeCoverage(array $coverage) {
if (empty($coverage)) {
return null;
}
$base = reset($coverage);
foreach ($coverage as $more_coverage) {
$len = min(strlen($base), strlen($more_coverage));
for ($ii = 0; $ii < $len; $ii++) {
if ($more_coverage[$ii] == 'C') {
$base[$ii] = 'C';
}
}
}
return $base;
}
}
diff --git a/src/unit/engine/ArcanistBaseUnitTestEngine.php b/src/unit/engine/ArcanistBaseUnitTestEngine.php
index 1e673430..12416796 100644
--- a/src/unit/engine/ArcanistBaseUnitTestEngine.php
+++ b/src/unit/engine/ArcanistBaseUnitTestEngine.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2012 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 unit test execution.
*
* @group unit
*/
abstract class ArcanistBaseUnitTestEngine {
private $workingCopy;
private $paths;
private $arguments = array();
protected $diffID;
private $enableAsyncTests;
private $enableCoverage;
final public function __construct() {
}
final public function setWorkingCopy(
ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
final public function getWorkingCopy() {
return $this->workingCopy;
}
final public function setPaths(array $paths) {
$this->paths = $paths;
return $this;
}
final public function getPaths() {
return $this->paths;
}
final public function setArguments(array $arguments) {
$this->arguments = $arguments;
return $this;
}
final public function getArgument($key, $default = null) {
return idx($this->arguments, $key, $default);
}
final public function setEnableAsyncTests($enable_async_tests) {
$this->enableAsyncTests = $enable_async_tests;
return $this;
}
final public function getEnableAsyncTests() {
return $this->enableAsyncTests;
}
final public function setEnableCoverage($enable_coverage) {
$this->enableCoverage = $enable_coverage;
return $this;
}
final public function getEnableCoverage() {
return $this->enableCoverage;
}
abstract public function run();
/**
* Modify the return value of this function in the child class, if
* you do not need to echo the test results after all the tests have
* been run. This is the case for example when the child class
* prints the tests results while the tests are running.
*/
public function shouldEchoTestResults() {
return true;
}
}
diff --git a/src/unit/engine/NoseTestEngine.php b/src/unit/engine/NoseTestEngine.php
index 9a99207b..fd508e10 100644
--- a/src/unit/engine/NoseTestEngine.php
+++ b/src/unit/engine/NoseTestEngine.php
@@ -1,257 +1,241 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Very basic 'nose' unit test engine wrapper.
*
* Requires nose 1.1.3 for code coverage.
*
* @group unitrun
*/
final class NoseTestEngine extends ArcanistBaseUnitTestEngine {
public function run() {
$paths = $this->getPaths();
$affected_tests = array();
foreach ($paths as $path) {
$absolute_path = Filesystem::resolvePath($path);
if (is_dir($absolute_path)) {
$absolute_test_path = Filesystem::resolvePath("tests/".$path);
if (is_readable($absolute_test_path)) {
$affected_tests[] = $absolute_test_path;
}
}
if (is_readable($absolute_path)) {
$filename = basename($path);
$directory = dirname($path);
// assumes directory layout: tests/<package>/test_<module>.py
$relative_test_path = "tests/".$directory."/test_".$filename;
$absolute_test_path = Filesystem::resolvePath($relative_test_path);
if (is_readable($absolute_test_path)) {
$affected_tests[] = $absolute_test_path;
}
}
}
if (empty($affected_tests)) {
return array();
}
$futures = array();
$tmpfiles = array();
foreach ($affected_tests as $test_path) {
$xunit_tmp = new TempFile();
$cover_tmp = new TempFile();
$future = $this->buildTestFuture($test_path,
$xunit_tmp,
$cover_tmp);
$futures[$test_path] = $future;
$tmpfiles[$test_path] = array(
'xunit' => $xunit_tmp,
'cover' => $cover_tmp,
);
}
$results = array();
foreach (Futures($futures)->limit(4) as $test_path => $future) {
try {
list($stdout, $stderr) = $future->resolvex();
} catch(CommandException $exc) {
if ($exc->getError() > 1) {
// 'nose' returns 1 when tests are failing/broken.
throw $exc;
}
}
$xunit_tmp = $tmpfiles[$test_path]['xunit'];
$cover_tmp = $tmpfiles[$test_path]['cover'];
$results[] = $this->parseTestResults($test_path,
$xunit_tmp,
$cover_tmp);
}
return array_mergev($results);
}
public function buildTestFuture($path, $xunit_tmp, $cover_tmp) {
$cmd_line = csprintf("nosetests --with-xunit --xunit-file=%s",
$xunit_tmp);
if ($this->getEnableCoverage() !== false) {
$cmd_line .= csprintf(" --with-coverage --cover-xml " .
"--cover-xml-file=%s",
$cover_tmp);
}
return new ExecFuture("%C %s", $cmd_line, $path);
}
public function parseTestResults($path, $xunit_tmp, $cover_tmp) {
// xunit xsd: https://gist.github.com/959290
$xunit_dom = new DOMDocument();
$xunit_dom->loadXML(Filesystem::readFile($xunit_tmp));
// coverage is for all testcases in the executed $path
$coverage = array();
if ($this->getEnableCoverage() !== false) {
$coverage = $this->readCoverage($cover_tmp);
}
$results = array();
$testcases = $xunit_dom->getElementsByTagName("testcase");
foreach ($testcases as $testcase) {
$classname = $testcase->getAttribute("classname");
$name = $testcase->getAttribute("name");
$time = $testcase->getAttribute("time");
$status = ArcanistUnitTestResult::RESULT_PASS;
$user_data = "";
// A skipped test is a test which was ignored using framework
// mechanizms (e.g. @skip decorator)
$skipped = $testcase->getElementsByTagName("skipped");
if ($skipped->length > 0) {
$status = ArcanistUnitTestResult::RESULT_SKIP;
$messages = array();
for ($ii = 0; $ii < $skipped->length; $ii++) {
$messages[] = trim($skipped->item($ii)->nodeValue, " \n");
}
$user_data .= implode("\n", $messages);
}
// Failure is a test which the code has explicitly failed by using
// the mechanizms for that purpose. e.g., via an assertEquals
$failures = $testcase->getElementsByTagName("failure");
if ($failures->length > 0) {
$status = ArcanistUnitTestResult::RESULT_FAIL;
$messages = array();
for ($ii = 0; $ii < $failures->length; $ii++) {
$messages[] = trim($failures->item($ii)->nodeValue, " \n");
}
$user_data .= implode("\n", $messages)."\n";
}
// An errored test is one that had an unanticipated problem. e.g., an
// unchecked throwable, or a problem with an implementation of the
// test.
$errors = $testcase->getElementsByTagName("error");
if ($errors->length > 0) {
$status = ArcanistUnitTestResult::RESULT_BROKEN;
$messages = array();
for ($ii = 0; $ii < $errors->length; $ii++) {
$messages[] = trim($errors->item($ii)->nodeValue, " \n");
}
$user_data .= implode("\n", $messages)."\n";
}
$result = new ArcanistUnitTestResult();
$result->setName($classname.".".$name);
$result->setResult($status);
$result->setDuration($time);
$result->setCoverage($coverage);
$result->setUserData($user_data);
$results[] = $result;
}
return $results;
}
public function readCoverage($path) {
$coverage_dom = new DOMDocument();
$coverage_dom->loadXML(Filesystem::readFile($path));
$reports = array();
$classes = $coverage_dom->getElementsByTagName("class");
foreach ($classes as $class) {
// filename is actually python module path with ".py" at the end,
// e.g.: tornado.web.py
$relative_path = explode(".", $class->getAttribute("filename"));
array_pop($relative_path);
$relative_path = implode("/", $relative_path);
// first we check if the path is a directory (a Python package), if it is
// set relative and absolute paths to have __init__.py at the end.
$absolute_path = Filesystem::resolvePath($relative_path);
if (is_dir($absolute_path)) {
$relative_path .= "/__init__.py";
$absolute_path .= "/__init__.py";
}
// then we check if the path with ".py" at the end is file (a Python
// submodule), if it is - set relative and absolute paths to have
// ".py" at the end.
if (is_file($absolute_path.".py")) {
$relative_path .= ".py";
$absolute_path .= ".py";
}
if (!file_exists($absolute_path)) {
continue;
}
// get total line count in file
$line_count = count(file($absolute_path));
$coverage = "";
$start_line = 1;
$lines = $class->getElementsByTagName("line");
for ($ii = 0; $ii < $lines->length; $ii++) {
$line = $lines->item($ii);
$next_line = intval($line->getAttribute("number"));
for ($start_line; $start_line < $next_line; $start_line++) {
$coverage .= "N";
}
if (intval($line->getAttribute("hits")) == 0) {
$coverage .= "U";
}
else if (intval($line->getAttribute("hits")) > 0) {
$coverage .= "C";
}
$start_line++;
}
if ($start_line < $line_count) {
foreach (range($start_line, $line_count) as $line_num) {
$coverage .= "N";
}
}
$reports[$relative_path] = $coverage;
}
return $reports;
}
}
diff --git a/src/unit/engine/PhpunitTestEngine.php b/src/unit/engine/PhpunitTestEngine.php
index d20e0b41..5ed4fc5b 100644
--- a/src/unit/engine/PhpunitTestEngine.php
+++ b/src/unit/engine/PhpunitTestEngine.php
@@ -1,426 +1,410 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* PHPUnit wrapper
*
* To use, set unit_engine in .arcconfig, or use --engine flag
* with arc unit. Currently supports only class & test files
* (no directory support).
* To use custom phpunit configuration, set phpunit_config in
* .arcconfig (e.g. app/phpunit.xml.dist).
*
* @group unitrun
*/
final class PhpunitTestEngine extends ArcanistBaseUnitTestEngine {
private $configFile;
private $affectedTests;
private $projectRoot;
public function run() {
$this->projectRoot = $this->getWorkingCopy()->getProjectRoot();
$this->affectedTests = array();
foreach ($this->getPaths() as $path) {
$path = Filesystem::resolvePath($path, $this->projectRoot);
// TODO: add support for directories
// Users can call phpunit on the directory themselves
if (is_dir($path)) {
continue;
}
// Not sure if it would make sense to go further if
// it is not a .php file
if (substr($path, -4) != '.php') {
continue;
}
if (substr($path, -8) == 'Test.php') {
// Looks like a valid test file name.
$this->affectedTests[$path] = $path;
continue;
}
if ($test = $this->findTestFile($path)) {
$this->affectedTests[$path] = $test;
}
}
if (empty($this->affectedTests)) {
throw new ArcanistNoEffectException('No tests to run.');
}
$this->prepareConfigFile();
$futures = array();
$tmpfiles = array();
foreach ($this->affectedTests as $class_path => $test_path) {
$json_tmp = new TempFile();
$clover_tmp = null;
$clover = null;
if ($this->getEnableCoverage() !== false) {
$clover_tmp = new TempFile();
$clover = csprintf('--coverage-clover %s', $clover_tmp);
}
$config = $this->configFile ? csprintf('-c %s', $this->configFile) : null;
$futures[$test_path] = new ExecFuture('phpunit %C --log-json %s %C %s',
$config, $json_tmp, $clover, $test_path);
$tmpfiles[$test_path] = array(
'json' => $json_tmp,
'clover' => $clover_tmp,
);
}
$results = array();
foreach (Futures($futures)->limit(4) as $test => $future) {
list($err, $stdout, $stderr) = $future->resolve();
$results[] = $this->parseTestResults($test_path,
$tmpfiles[$test]['json'],
$tmpfiles[$test]['clover']);
}
return array_mergev($results);
}
/**
* We need this non-sense to make json generated by phpunit
* valid.
*
* @param string $json_tmp Path to JSON report
*
* @return array JSON decoded array
*/
private function getJsonReport($json_tmp) {
$json = Filesystem::readFile($json_tmp);
if (empty($json)) {
throw new Exception('JSON report file is empty, '
. 'it probably means that phpunit failed to run tests. '
. 'Try running arc unit with --trace option and then run '
. 'generated phpunit command yourself, you might get the '
. 'answer.'
);
}
$json = preg_replace('/}{\s*"/', '},{"', $json);
$json = '[' . $json . ']';
$json = json_decode($json);
if (!is_array($json)) {
throw new Exception('JSON could not be decoded');
}
return $json;
}
/**
* Parse test results from phpunit json report
*
* @param string $path Path to test
* @param string $json_path Path to phpunit json report
* @param string $clover_tmp Path to phpunit clover report
*
* @return array
*/
private function parseTestResults($path, $json_tmp, $clover_tmp) {
$test_results = Filesystem::readFile($json_tmp);
$report = $this->getJsonReport($json_tmp);
// coverage is for all testcases in the executed $path
$coverage = array();
if ($this->getEnableCoverage() !== false) {
$coverage = $this->readCoverage($clover_tmp);
}
$results = array();
foreach ($report as $event) {
if ('test' != $event->event) {
continue;
}
$status = ArcanistUnitTestResult::RESULT_PASS;
$user_data = '';
if ('fail' == $event->status) {
$status = ArcanistUnitTestResult::RESULT_FAIL;
$user_data .= $event->message . "\n";
foreach ($event->trace as $trace) {
$user_data .= sprintf("\n%s:%s", $trace->file, $trace->line);
}
} else if ('error' == $event->status) {
if ('Skipped Test' == $event->message) {
$status = ArcanistUnitTestResult::RESULT_SKIP;
$user_data .= $event->message;
} else if ('Incomplete Test' == $event->message) {
$status = ArcanistUnitTestResult::RESULT_SKIP;
$user_data .= $event->message;
} else {
$status = ArcanistUnitTestResult::RESULT_BROKEN;
$user_data .= $event->message;
foreach ($event->trace as $trace) {
$user_data .= sprintf("\n%s:%s", $trace->file, $trace->line);
}
}
}
$name = preg_replace('/ \(.*\)/', '', $event->test);
$result = new ArcanistUnitTestResult();
$result->setName($name);
$result->setResult($status);
$result->setDuration($event->time);
$result->setCoverage($coverage);
$result->setUserData($user_data);
$results[] = $result;
}
return $results;
}
/**
* Red the coverage from phpunit generated clover report
*
* @param string $path Path to report
*
* @return array
*/
private function readCoverage($path) {
$test_results = Filesystem::readFile($path);
if (empty($test_results)) {
throw new Exception('Clover coverage XML report file is empty, '
. 'it probably means that phpunit failed to run tests. '
. 'Try running arc unit with --trace option and then run '
. 'generated phpunit command yourself, you might get the '
. 'answer.'
);
}
$coverage_dom = new DOMDocument();
$coverage_dom->loadXML($test_results);
$reports = array();
$files = $coverage_dom->getElementsByTagName('file');
foreach ($files as $file) {
$class_path = $file->getAttribute('name');
if (empty($this->affectedTests[$class_path])) {
continue;
}
$test_path = $this->affectedTests[$file->getAttribute('name')];
// get total line count in file
$line_count = count(file($class_path));
$coverage = '';
$start_line = 1;
$lines = $file->getElementsByTagName('line');
for ($ii = 0; $ii < $lines->length; $ii++) {
$line = $lines->item($ii);
for (; $start_line < $line->getAttribute('num'); $start_line++) {
$coverage .= 'N';
}
if ($line->getAttribute('type') != 'stmt') {
$coverage .= 'N';
} else {
if ((int) $line->getAttribute('count') == 0) {
$coverage .= 'U';
}
else if ((int) $line->getAttribute('count') > 0) {
$coverage .= 'C';
}
}
$start_line++;
}
for (; $start_line <= $line_count; $start_line++) {
$coverage .= 'N';
}
$len = strlen($this->projectRoot . DIRECTORY_SEPARATOR);
$class_path = substr($class_path, $len);
$reports[$class_path] = $coverage;
}
return $reports;
}
/**
* Search for test cases for a given file in a large number of "reasonable"
* locations. See @{method:getSearchLocationsForTests} for specifics.
*
* TODO: Add support for finding tests in testsuite folders from
* phpunit.xml configuration.
*
* @param string PHP file to locate test cases for.
* @return string|null Path to test cases, or null.
*/
private function findTestFile($path) {
$root = $this->projectRoot;
$path = Filesystem::resolvePath($path, $root);
$file = basename($path);
$possible_files = array(
$file,
substr($file, 0, -4).'Test.php',
);
$search = self::getSearchLocationsForTests($path);
foreach ($search as $search_path) {
foreach ($possible_files as $possible_file) {
$full_path = $search_path.$possible_file;
if (!Filesystem::pathExists($full_path)) {
// If the file doesn't exist, it's clearly a miss.
continue;
}
if (!Filesystem::isDescendant($full_path, $root)) {
// Don't look above the project root.
continue;
}
if (Filesystem::resolvePath($full_path) == $path) {
// Don't return the original file.
continue;
}
return $full_path;
}
}
return null;
}
/**
* Get places to look for PHP Unit tests that cover a given file. For some
* file "/a/b/c/X.php", we look in the same directory:
*
* /a/b/c/
*
* We then look in all parent directories for a directory named "tests/"
* (or "Tests/"):
*
* /a/b/c/tests/
* /a/b/tests/
* /a/tests/
* /tests/
*
* We also try to replace each directory component with "tests/":
*
* /a/b/tests/
* /a/tests/c/
* /tests/b/c/
*
* We also try to add "tests/" at each directory level:
*
* /a/b/c/tests/
* /a/b/tests/c/
* /a/tests/b/c/
* /tests/a/b/c/
*
* This finds tests with a layout like:
*
* docs/
* src/
* tests/
*
* ...or similar. This list will be further pruned by the caller; it is
* intentionally filesystem-agnostic to be unit testable.
*
* @param string PHP file to locate test cases for.
* @return list<string> List of directories to search for tests in.
*/
public static function getSearchLocationsForTests($path) {
$file = basename($path);
$dir = dirname($path);
$test_dir_names = array('tests', 'Tests');
$try_directories = array();
// Try in the current directory.
$try_directories[] = array($dir);
// Try in a tests/ directory anywhere in the ancestry.
foreach (Filesystem::walkToRoot($dir) as $parent_dir) {
if ($parent_dir == '/') {
// We'll restore this later.
$parent_dir = '';
}
foreach ($test_dir_names as $test_dir_name) {
$try_directories[] = array($parent_dir, $test_dir_name);
}
}
// Try replacing each directory component with 'tests/'.
$parts = trim($dir, DIRECTORY_SEPARATOR);
$parts = explode(DIRECTORY_SEPARATOR, $parts);
foreach (array_reverse(array_keys($parts)) as $key) {
foreach ($test_dir_names as $test_dir_name) {
$try = $parts;
$try[$key] = $test_dir_name;
array_unshift($try, '');
$try_directories[] = $try;
}
}
// Try adding 'tests/' at each level.
foreach (array_reverse(array_keys($parts)) as $key) {
foreach ($test_dir_names as $test_dir_name) {
$try = $parts;
$try[$key] = $test_dir_name.DIRECTORY_SEPARATOR.$try[$key];
array_unshift($try, '');
$try_directories[] = $try;
}
}
$results = array();
foreach ($try_directories as $parts) {
$results[implode(DIRECTORY_SEPARATOR, $parts).DIRECTORY_SEPARATOR] = true;
}
return array_keys($results);
}
/**
* Tries to find and update phpunit configuration file
* based on phpunit_config option in .arcconfig
*/
private function prepareConfigFile() {
$project_root = $this->projectRoot . DIRECTORY_SEPARATOR;
if ($config = $this->getWorkingCopy()->getConfig('phpunit_config')) {
if (Filesystem::pathExists($project_root . $config)) {
$this->configFile = $project_root . $config;
} else {
throw new Exception('PHPUnit configuration file was not ' .
'found in ' . $project_root . $config);
}
}
}
}
diff --git a/src/unit/engine/PhutilUnitTestEngine.php b/src/unit/engine/PhutilUnitTestEngine.php
index 290c2ddf..8e02772c 100644
--- a/src/unit/engine/PhutilUnitTestEngine.php
+++ b/src/unit/engine/PhutilUnitTestEngine.php
@@ -1,138 +1,122 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Very basic unit test engine which runs libphutil tests.
*
* @group unitrun
*/
final class PhutilUnitTestEngine extends ArcanistBaseUnitTestEngine {
public function run() {
$bootloader = PhutilBootloader::getInstance();
$project_root = $this->getWorkingCopy()->getProjectRoot();
$look_here = array();
foreach ($this->getPaths() as $path) {
$library_root = phutil_get_library_root_for_path($path);
if (!$library_root) {
continue;
}
$library_name = phutil_get_library_name_for_root($library_root);
if (!$library_name) {
throw new Exception(
"Attempting to run unit tests on a libphutil library which has not ".
"been loaded, at:\n\n".
" {$library_root}\n\n".
"This probably means one of two things:\n\n".
" - You may need to add this library to .arcconfig.\n".
" - You may be running tests on a copy of libphutil or arcanist\n".
" using a different copy of libphutil or arcanist. This\n".
" operation is not supported.");
}
$path = Filesystem::resolvePath($path, $project_root);
if (!is_dir($path)) {
$path = dirname($path);
}
if ($path == $library_root) {
continue;
}
if (!Filesystem::isDescendant($path, $library_root)) {
// We have encountered some kind of symlink maze -- for instance, $path
// is some symlink living outside the library that links into some file
// inside the library. Just ignore these cases, since the affected file
// does not actually lie within the library.
continue;
}
$library_path = Filesystem::readablePath($path, $library_root);
do {
$look_here[$library_name.':'.$library_path] = array(
'library' => $library_name,
'path' => $library_path,
);
$library_path = dirname($library_path);
} while ($library_path != '.');
}
// Look for any class that extends ArcanistPhutilTestCase inside a
// __tests__ directory in any parent directory of every affected file.
//
// The idea is that "infrastructure/__tests__/" tests defines general tests
// for all of "infrastructure/", and those tests run for any change in
// "infrastructure/". However, "infrastructure/concrete/rebar/__tests__/"
// defines more specific tests that run only when rebar/ (or some
// subdirectory) changes.
$run_tests = array();
foreach ($look_here as $path_info) {
$library = $path_info['library'];
$path = $path_info['path'];
$symbols = id(new PhutilSymbolLoader())
->setType('class')
->setLibrary($library)
->setPathPrefix($path.'/__tests__/')
->setAncestorClass('ArcanistPhutilTestCase')
->setConcreteOnly(true)
->selectAndLoadSymbols();
foreach ($symbols as $symbol) {
$run_tests[$symbol['name']] = true;
}
}
$run_tests = array_keys($run_tests);
if (!$run_tests) {
throw new ArcanistNoEffectException("No tests to run.");
}
$enable_coverage = $this->getEnableCoverage();
if ($enable_coverage !== false) {
if (!function_exists('xdebug_start_code_coverage')) {
if ($enable_coverage === true) {
throw new ArcanistUsageException(
"You specified --coverage but xdebug is not available, so ".
"coverage can not be enabled for PhutilUnitTestEngine.");
}
} else {
$enable_coverage = true;
}
}
$results = array();
foreach ($run_tests as $test_class) {
$test_case = newv($test_class, array());
$test_case->setEnableCoverage($enable_coverage);
$test_case->setProjectRoot($project_root);
$test_case->setPaths($this->getPaths());
$results[] = $test_case->run();
}
$results = array_mergev($results);
return $results;
}
}
diff --git a/src/unit/engine/__tests__/PHPUnitTestEngineTestCase.php b/src/unit/engine/__tests__/PHPUnitTestEngineTestCase.php
index 882122a8..2136a5f6 100644
--- a/src/unit/engine/__tests__/PHPUnitTestEngineTestCase.php
+++ b/src/unit/engine/__tests__/PHPUnitTestEngineTestCase.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Tests for @{class:PHPUnitTestEngine}.
*
* @group testcase
*/
final class PHPUnitTestEngineTestCase extends ArcanistTestCase {
public function testSearchLocations() {
$path = '/path/to/some/file/X.php';
$this->assertEqual(
array(
'/path/to/some/file/',
'/path/to/some/file/tests/',
'/path/to/some/file/Tests/',
'/path/to/some/tests/',
'/path/to/some/Tests/',
'/path/to/tests/',
'/path/to/Tests/',
'/path/tests/',
'/path/Tests/',
'/tests/',
'/Tests/',
'/path/to/tests/file/',
'/path/to/Tests/file/',
'/path/tests/some/file/',
'/path/Tests/some/file/',
'/tests/to/some/file/',
'/Tests/to/some/file/',
'/path/to/some/tests/file/',
'/path/to/some/Tests/file/',
'/path/to/tests/some/file/',
'/path/to/Tests/some/file/',
'/path/tests/to/some/file/',
'/path/Tests/to/some/file/',
'/tests/path/to/some/file/',
'/Tests/path/to/some/file/',
),
PhpunitTestEngine::getSearchLocationsForTests($path));
}
}
diff --git a/src/unit/engine/__tests__/PhutilUnitTestEngineTestCase.php b/src/unit/engine/__tests__/PhutilUnitTestEngineTestCase.php
index eb5bc58b..6ace3af4 100644
--- a/src/unit/engine/__tests__/PhutilUnitTestEngineTestCase.php
+++ b/src/unit/engine/__tests__/PhutilUnitTestEngineTestCase.php
@@ -1,130 +1,114 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Very meta test for @{class:PhutilUnitTestEngine}.
*
* @group testcase
*/
final class PhutilUnitTestEngineTestCase extends ArcanistTestCase {
static $allTestsCounter = 0;
static $oneTestCounter = 0;
static $distinctWillRunTests = array();
static $distinctDidRunTests = array();
protected function willRunTests() {
self::$allTestsCounter++;
}
protected function didRunTests() {
$this->assertEqual(
1,
self::$allTestsCounter,
'Expect willRunTests() has been called once.');
self::$allTestsCounter--;
$actual_test_count = 4;
$this->assertEqual(
$actual_test_count,
count(self::$distinctWillRunTests),
'Expect willRunOneTest() was called once for each test.');
$this->assertEqual(
$actual_test_count,
count(self::$distinctDidRunTests),
'Expect didRunOneTest() was called once for each test.');
$this->assertEqual(
self::$distinctWillRunTests,
self::$distinctDidRunTests,
'Expect same tests had pre- and post-run callbacks invoked.');
}
public function __destruct() {
if (self::$allTestsCounter !== 0) {
throw new Exception(
"didRunTests() was not called correctly after tests completed!");
}
}
protected function willRunOneTest($test) {
self::$distinctWillRunTests[$test] = true;
self::$oneTestCounter++;
}
protected function didRunOneTest($test) {
$this->assertEqual(
1,
self::$oneTestCounter,
'Expect willRunOneTest depth to be one.');
self::$distinctDidRunTests[$test] = true;
self::$oneTestCounter--;
}
public function testPass() {
$this->assertEqual(1, 1, 'This test is expected to pass.');
}
public function testFailSkip() {
$failed = 0;
$skipped = 0;
$test_case = new ArcanistPhutilTestCaseTestCase();
foreach ($test_case->run() as $result) {
if ($result->getResult() == ArcanistUnitTestResult::RESULT_FAIL) {
$failed++;
} else if ($result->getResult() == ArcanistUnitTestResult::RESULT_SKIP) {
$skipped++;
} else {
$this->assertFailure('These tests should either fail or skip.');
}
}
$this->assertEqual(1, $failed, 'One test was expected to fail.');
$this->assertEqual(1, $skipped, 'One test was expected to skip.');
}
public function testTryTestCases() {
$this->tryTestCases(
array(
true,
false,
),
array(
true,
false,
),
array($this, 'throwIfFalsey'));
}
public function testTryTestMap() {
$this->tryTestCaseMap(
array(
1 => true,
0 => false,
),
array($this, 'throwIfFalsey'));
}
protected function throwIfFalsey($input) {
if (!$input) {
throw new Exception("This is a negative test case!");
}
}
}
diff --git a/src/unit/engine/phutil/ArcanistPhutilTestCase.php b/src/unit/engine/phutil/ArcanistPhutilTestCase.php
index fed0f5ee..e109c65a 100644
--- a/src/unit/engine/phutil/ArcanistPhutilTestCase.php
+++ b/src/unit/engine/phutil/ArcanistPhutilTestCase.php
@@ -1,550 +1,534 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Base test case for the very simple libphutil test framework.
*
* @task assert Making Test Assertions
* @task exceptions Exception Handling
* @task hook Hooks for Setup and Teardown
* @task internal Internals
*
* @group unitrun
*/
abstract class ArcanistPhutilTestCase {
private $assertions = 0;
private $runningTest;
private $testStartTime;
private $results = array();
private $enableCoverage;
private $coverage = array();
private $projectRoot;
private $paths;
/* -( Making Test Assertions )--------------------------------------------- */
/**
* Assert that two values are equal. The test fails if they are not.
*
* NOTE: This method uses PHP's strict equality test operator ("===") to
* compare values. This means values and types must be equal, key order must
* be identical in arrays, and objects must be referentially identical.
*
* @param wild The theoretically expected value, generated by careful
* reasoning about the properties of the system.
* @param wild The empirically derived value, generated by executing the
* test.
* @param string A human-readable description of what these values represent,
* and particularly of what a discrepancy means.
*
* @return void
* @task assert
*/
final protected function assertEqual($expect, $result, $message = null) {
if ($expect === $result) {
$this->assertions++;
return;
}
$expect = PhutilReadableSerializer::printableValue($expect);
$result = PhutilReadableSerializer::printableValue($result);
$where = debug_backtrace();
$where = array_shift($where);
$line = idx($where, 'line');
$file = basename(idx($where, 'file'));
$output = "Assertion failed at line {$line} in {$file}";
if ($message) {
$output .= ": {$message}";
}
$output .= "\n";
if (strpos($expect, "\n") === false && strpos($result, "\n") === false) {
$output .= "Expected: {$expect}\n";
$output .= "Actual: {$result}";
} else {
$output .= "Expected vs Actual Output Diff\n";
$output .= ArcanistDiffUtils::renderDifferences(
$expect,
$result,
$lines = 0xFFFF);
}
$this->failTest($output);
throw new ArcanistPhutilTestTerminatedException($output);
}
/**
* Assert an unconditional failure. This is just a convenience method that
* better indicates intent than using dummy values with assertEqual(). This
* causes test failure.
*
* @param string Human-readable description of the reason for test failure.
* @return void
* @task assert
*/
final protected function assertFailure($message) {
$this->failTest($message);
throw new ArcanistPhutilTestTerminatedException($message);
}
/**
* End this test by asserting that the test should be skipped for some
* reason.
*
* @param string Reason for skipping this test.
* @return void
* @task assert
*/
final protected function assertSkipped($message) {
$this->skipTest($message);
throw new ArcanistPhutilTestSkippedException($message);
}
/* -( Exception Handling )------------------------------------------------- */
/**
* This simplest way to assert exceptions are thrown.
*
* @param exception The expected exception.
* @param callable The thing which throws the exception.
*
* @return void
* @task exceptions
*/
final protected function assertException($expected_exception_class,
$callable) {
$this->tryTestCases(
array('assertException' => array()),
array(false),
$callable,
$expected_exception_class
);
}
/**
* Straightforward method for writing unit tests which check if some block of
* code throws an exception. For example, this allows you to test the
* exception behavior of ##is_a_fruit()## on various inputs:
*
* public function testFruit() {
* $this->tryTestCases(
* array(
* 'apple is a fruit' => new Apple(),
* 'rock is not a fruit' => new Rock(),
* ),
* array(
* true,
* false,
* ),
* array($this, 'tryIsAFruit'),
* 'NotAFruitException');
* }
*
* protected function tryIsAFruit($input) {
* is_a_fruit($input);
* }
*
* @param map Map of test case labels to test case inputs.
* @param list List of expected results, true to indicate that the case
* is expected to succeed and false to indicate that the case
* is expected to throw.
* @param callable Callback to invoke for each test case.
* @param string Optional exception class to catch, defaults to
* 'Exception'.
* @return void
* @task exceptions
*/
final protected function tryTestCases(
array $inputs,
array $expect,
$callable,
$exception_class = 'Exception') {
if (count($inputs) !== count($expect)) {
$this->assertFailure(
"Input and expectations must have the same number of values.");
}
$labels = array_keys($inputs);
$inputs = array_values($inputs);
$expecting = array_values($expect);
foreach ($inputs as $idx => $input) {
$expect = $expecting[$idx];
$label = $labels[$idx];
$caught = null;
try {
call_user_func($callable, $input);
} catch (Exception $ex) {
if ($ex instanceof ArcanistPhutilTestTerminatedException) {
throw $ex;
}
if (!($ex instanceof $exception_class)) {
throw $ex;
}
$caught = $ex;
}
$actual = !($caught instanceof Exception);
if ($expect === $actual) {
if ($expect) {
$message = "Test case '{$label}' did not throw, as expected.";
} else {
$message = "Test case '{$label}' threw, as expected.";
}
} else {
if ($expect) {
$message = "Test case '{$label}' was expected to succeed, but it ".
"raised an exception of class ".get_class($ex)." with ".
"message: ".$ex->getMessage();
} else {
$message = "Test case '{$label}' was expected to raise an ".
"exception, but it did not throw anything.";
}
}
$this->assertEqual($expect, $actual, $message);
}
}
/**
* Convenience wrapper around @{method:tryTestCases} for cases where your
* inputs are scalar. For example:
*
* public function testFruit() {
* $this->tryTestCaseMap(
* array(
* 'apple' => true,
* 'rock' => false,
* ),
* array($this, 'tryIsAFruit'),
* 'NotAFruitException');
* }
*
* protected function tryIsAFruit($input) {
* is_a_fruit($input);
* }
*
* For cases where your inputs are not scalar, use @{method:tryTestCases}.
*
* @param map Map of scalar test inputs to expected success (true
* expects success, false expects an exception).
* @param callable Callback to invoke for each test case.
* @param string Optional exception class to catch, defaults to
* 'Exception'.
* @return void
* @task exceptions
*/
final protected function tryTestCaseMap(
array $map,
$callable,
$exception_class = 'Exception') {
return $this->tryTestCases(
array_combine(array_keys($map), array_keys($map)),
array_values($map),
$callable,
$exception_class);
}
/* -( Hooks for Setup and Teardown )--------------------------------------- */
/**
* This hook is invoked once, before any tests in this class are run. It
* gives you an opportunity to perform setup steps for the entire class.
*
* @return void
* @task hook
*/
protected function willRunTests() {
return;
}
/**
* This hook is invoked once, after any tests in this class are run. It gives
* you an opportunity to perform teardown steps for the entire class.
*
* @return void
* @task hook
*/
protected function didRunTests() {
return;
}
/**
* This hook is invoked once per test, before the test method is invoked.
*
* @param string Method name of the test which will be invoked.
* @return void
* @task hook
*/
protected function willRunOneTest($test_method_name) {
return;
}
/**
* This hook is invoked once per test, after the test method is invoked.
*
* @param string Method name of the test which was invoked.
* @return void
* @task hook
*/
protected function didRunOneTest($test_method_name) {
return;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Construct a new test case. This method is ##final##, use willRunTests() to
* provide test-wide setup logic.
*
* @task internal
*/
final public function __construct() {
}
/**
* Mark the currently-running test as a failure.
*
* @param string Human-readable description of problems.
* @return void
*
* @task internal
*/
final private function failTest($reason) {
$this->resultTest(ArcanistUnitTestResult::RESULT_FAIL, $reason);
}
/**
* This was a triumph. I'm making a note here: HUGE SUCCESS.
*
* @param string Human-readable overstatement of satisfaction.
* @return void
*
* @task internal
*/
final private function passTest($reason) {
$this->resultTest(ArcanistUnitTestResult::RESULT_PASS, $reason);
}
/**
* Mark the current running test as skipped.
*
* @param string Description for why this test was skipped.
* @return void
* @task internal
*/
final private function skipTest($reason) {
$this->resultTest(ArcanistUnitTestResult::RESULT_SKIP, $reason);
}
final private function resultTest($test_result, $reason) {
$coverage = $this->endCoverage();
$result = new ArcanistUnitTestResult();
$result->setCoverage($coverage);
$result->setName($this->runningTest);
$result->setLink($this->getLink($this->runningTest));
$result->setResult($test_result);
$result->setDuration(microtime(true) - $this->testStartTime);
$result->setUserData($reason);
$this->results[] = $result;
}
/**
* Execute the tests in this test case. You should not call this directly;
* use @{class:PhutilUnitTestEngine} to orchestrate test execution.
*
* @return void
* @task internal
*/
final public function run() {
$this->results = array();
$reflection = new ReflectionClass($this);
$methods = $reflection->getMethods();
// Try to ensure that poorly-written tests which depend on execution order
// (and are thus not properly isolated) will fail.
shuffle($methods);
$this->willRunTests();
foreach ($methods as $method) {
$name = $method->getName();
if (preg_match('/^test/', $name)) {
$this->runningTest = $name;
$this->assertions = 0;
$this->testStartTime = microtime(true);
try {
$this->willRunOneTest($name);
$this->beginCoverage();
$exceptions = array();
try {
call_user_func_array(
array($this, $name),
array());
$this->passTest(pht('%d assertion(s) passed.', $this->assertions));
} catch (Exception $ex) {
$exceptions['Execution'] = $ex;
}
try {
$this->didRunOneTest($name);
} catch (Exception $ex) {
$exceptions['Shutdown'] = $ex;
}
if ($exceptions) {
if (count($exceptions) == 1) {
throw head($exceptions);
} else {
throw new PhutilAggregateException(
"Multiple exceptions were raised during test execution.",
$exceptions);
}
}
} catch (ArcanistPhutilTestTerminatedException $ex) {
// Continue with the next test.
} catch (ArcanistPhutilTestSkippedException $ex) {
// Continue with the next test.
} catch (Exception $ex) {
$ex_class = get_class($ex);
$ex_message = $ex->getMessage();
$ex_trace = $ex->getTraceAsString();
$message = "EXCEPTION ({$ex_class}): {$ex_message}\n{$ex_trace}";
$this->failTest($message);
}
}
}
$this->didRunTests();
return $this->results;
}
final public function setEnableCoverage($enable_coverage) {
$this->enableCoverage = $enable_coverage;
return $this;
}
/**
* @phutil-external-symbol function xdebug_start_code_coverage
*/
final private function beginCoverage() {
if (!$this->enableCoverage) {
return;
}
$this->assertCoverageAvailable();
xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
}
/**
* @phutil-external-symbol function xdebug_get_code_coverage
* @phutil-external-symbol function xdebug_stop_code_coverage
*/
final private function endCoverage() {
if (!$this->enableCoverage) {
return;
}
$result = xdebug_get_code_coverage();
xdebug_stop_code_coverage($cleanup = false);
$coverage = array();
foreach ($result as $file => $report) {
if (strncmp($file, $this->projectRoot, strlen($this->projectRoot))) {
continue;
}
$max = max(array_keys($report));
$str = '';
for ($ii = 1; $ii <= $max; $ii++) {
$c = idx($report, $ii);
if ($c === -1) {
$str .= 'U'; // Un-covered.
} else if ($c === -2) {
// TODO: This indicates "unreachable", but it flags the closing braces
// of functions which end in "return", which is super ridiculous. Just
// ignore it for now.
$str .= 'N'; // Not executable.
} else if ($c === 1) {
$str .= 'C'; // Covered.
} else {
$str .= 'N'; // Not executable.
}
}
$coverage[substr($file, strlen($this->projectRoot) + 1)] = $str;
}
// Only keep coverage information for files modified by the change.
$coverage = array_select_keys($coverage, $this->paths);
return $coverage;
}
final private function assertCoverageAvailable() {
if (!function_exists('xdebug_start_code_coverage')) {
throw new Exception(
"You've enabled code coverage but XDebug is not installed.");
}
}
final public function setProjectRoot($project_root) {
$this->projectRoot = $project_root;
return $this;
}
final public function setPaths(array $paths) {
$this->paths = $paths;
return $this;
}
protected function getLink($method) {
return null;
}
}
diff --git a/src/unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php b/src/unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php
index 70a32b8a..79cdb82e 100644
--- a/src/unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php
+++ b/src/unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Test for @{class:PhutilUnitTestEngineTestCase}.
*
* @group testcase
*/
final class ArcanistPhutilTestCaseTestCase extends ArcanistPhutilTestCase {
public function testFail() {
$this->assertFailure('This test is expected to fail.');
}
public function testSkip() {
$this->assertSkipped('This test is expected to skip.');
}
}
diff --git a/src/unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php b/src/unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php
index a9cf91a6..24e48175 100644
--- a/src/unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php
+++ b/src/unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown to skip test execution.
*
* @group unitrun
*/
final class ArcanistPhutilTestSkippedException extends Exception {}
diff --git a/src/unit/engine/phutil/testcase/ArcanistPhutilTestTerminatedException.php b/src/unit/engine/phutil/testcase/ArcanistPhutilTestTerminatedException.php
index 5d0c76e7..11d8b526 100644
--- a/src/unit/engine/phutil/testcase/ArcanistPhutilTestTerminatedException.php
+++ b/src/unit/engine/phutil/testcase/ArcanistPhutilTestTerminatedException.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Thrown to prematurely end test execution.
*
* @group unitrun
*/
final class ArcanistPhutilTestTerminatedException extends Exception {}
diff --git a/src/workflow/ArcanistAliasWorkflow.php b/src/workflow/ArcanistAliasWorkflow.php
index a91428f9..7b6362a5 100644
--- a/src/workflow/ArcanistAliasWorkflow.php
+++ b/src/workflow/ArcanistAliasWorkflow.php
@@ -1,187 +1,171 @@
<?php
-/*
- * Copyright 2012 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 aliases for commands with options.
*
* @group workflow
*/
final class ArcanistAliasWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'alias';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**alias**
**alias** __command__
**alias** __command__ __target__ -- [__options__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Create an alias from __command__ to __target__ (optionally, with
__options__). For example:
arc alias fpatch patch -- --force
...will create a new 'arc' command, 'arc fpatch', which invokes
'arc patch --force ...' when run. NOTE: use "--" before specifying
options!
If you start an alias with "!", the remainder of the alias will be
invoked as a shell command. For example, if you want to implement
'arc ls', you can do so like this:
arc alias ls '!ls'
You can now run "arc ls" and it will behave like "ls". Of course, this
example is silly and would make your life worse.
You can not overwrite builtins, including 'alias' itself. The builtin
will always execute, even if it was added after your alias.
To remove an alias, run:
arc alias fpatch
Without any arguments, 'arc alias' will list aliases.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'argv',
);
}
public static function getAliases($working_copy) {
$working_copy_config_aliases = $working_copy->getConfig('aliases');
if (!$working_copy_config_aliases) {
$working_copy_config_aliases = array();
}
$user_config_aliases =
idx(self::readUserConfigurationFile(), 'aliases', array());
return $user_config_aliases + $working_copy_config_aliases;
}
private function writeAliases(array $aliases) {
$config = self::readUserConfigurationFile();
$config['aliases'] = $aliases;
self::writeUserConfigurationFile($config);
}
public function run() {
// We might not be in a working directory, so we don't want to require a
// working copy identity here.
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(getcwd());
$aliases = self::getAliases($working_copy);
$argv = $this->getArgument('argv');
if (count($argv) == 0) {
if ($aliases) {
foreach ($aliases as $alias => $binding) {
echo phutil_console_format(
"**%s** %s\n",
$alias,
implode(' ' , $binding));
}
} else {
echo "You haven't defined any aliases yet.\n";
}
} else if (count($argv) == 1) {
if (empty($aliases[$argv[0]])) {
echo "No alias '{$argv[0]}' to remove.\n";
} else {
echo phutil_console_format(
"'**arc %s**' is currently aliased to '**arc %s**'.",
$argv[0],
implode(' ', $aliases[$argv[0]]));
$ok = phutil_console_confirm('Delete this alias?');
if ($ok) {
$was = implode(' ', $aliases[$argv[0]]);
unset($aliases[$argv[0]]);
$this->writeAliases($aliases);
echo "Unaliased '{$argv[0]}' (was '{$was}').\n";
} else {
throw new ArcanistUserAbortException();
}
}
} else {
$arc_config = $this->getArcanistConfiguration();
if ($arc_config->buildWorkflow($argv[0])) {
throw new ArcanistUsageException(
"You can not create an alias for '{$argv[0]}' because it is a ".
"builtin command. 'arc alias' can only create new commands.");
}
$aliases[$argv[0]] = array_slice($argv, 1);
echo phutil_console_format(
"Aliased '**arc %s**' to '**arc %s**'.\n",
$argv[0],
implode(' ', $aliases[$argv[0]]));
$this->writeAliases($aliases);
}
return 0;
}
public static function isShellCommandAlias($command) {
return preg_match('/^!/', $command);
}
public static function resolveAliases(
$command,
ArcanistConfiguration $config,
array $argv,
ArcanistWorkingCopyIdentity $working_copy) {
$aliases = ArcanistAliasWorkflow::getAliases($working_copy);
if (!isset($aliases[$command])) {
return array(null, $argv);
}
$new_command = head($aliases[$command]);
if (self::isShellCommandAlias($new_command)) {
return array($new_command, $argv);
}
$workflow = $config->buildWorkflow($new_command);
if (!$workflow) {
return array(null, $argv);
}
$alias_argv = array_slice($aliases[$command], 1);
foreach (array_reverse($alias_argv) as $alias_arg) {
if (!in_array($alias_arg, $argv)) {
array_unshift($argv, $alias_arg);
}
}
return array($new_command, $argv);
}
}
diff --git a/src/workflow/ArcanistAmendWorkflow.php b/src/workflow/ArcanistAmendWorkflow.php
index 947a8be9..0aed0d22 100644
--- a/src/workflow/ArcanistAmendWorkflow.php
+++ b/src/workflow/ArcanistAmendWorkflow.php
@@ -1,207 +1,191 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Updates git commit messages after a revision is "Accepted".
*
* @group workflow
*/
final class ArcanistAmendWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'amend';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**amend** [--revision __revision_id__] [--show]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, hg
Amend the working copy after a revision has been accepted, so commits
can be marked 'committed' and pushed upstream.
Supported in Mercurial 2.2 and newer.
EOTEXT
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function getArguments() {
return array(
'show' => array(
'help' =>
"Show the amended commit message, without modifying the working copy."
),
'revision' => array(
'param' => 'revision_id',
'help' =>
"Amend a specific revision. If you do not specify a revision, ".
"arc will look in the commit message at HEAD.",
),
);
}
public function run() {
$is_show = $this->getArgument('show');
$repository_api = $this->getRepositoryAPI();
if (!$is_show) {
if (!$repository_api->supportsAmend()) {
throw new ArcanistUsageException(
"You may only run 'arc amend' in a git or hg (version ".
"2.2 or newer) working copy.");
}
if ($this->isHistoryImmutable()) {
throw new ArcanistUsageException(
"This project is marked as adhering to a conservative history ".
"mutability doctrine (having an immutable local history), which ".
"precludes amending commit messages. You can use 'arc merge' to ".
"merge feature branches instead.");
}
if ($repository_api->getUncommittedChanges()) {
throw new ArcanistUsageException(
"You have uncommitted changes in this branch. Stage and commit (or ".
"revert) them before proceeding.");
}
}
$revision_id = null;
if ($this->getArgument('revision')) {
$revision_id = $this->normalizeRevisionID($this->getArgument('revision'));
}
if ($repository_api->supportsRelativeLocalCommits()) {
$repository_api->setDefaultBaseCommit();
}
$in_working_copy = $repository_api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
array(
'authors' => array($this->getUserPHID()),
'status' => 'status-any',
));
$in_working_copy = ipull($in_working_copy, null, 'id');
if (!$revision_id) {
if (count($in_working_copy) == 0) {
throw new ArcanistUsageException(
"No revision specified with '--revision', and no revisions found ".
"in the working copy. Use '--revision <id>' to specify which ".
"revision you want to amend.");
} else if (count($in_working_copy) > 1) {
$message = "More than one revision was found in the working copy:\n".
$this->renderRevisionList($in_working_copy)."\n".
"Use '--revision <id>' to specify which revision you want to ".
"amend.";
throw new ArcanistUsageException($message);
} else {
$revision_id = key($in_working_copy);
}
}
$conduit = $this->getConduit();
try {
$message = $conduit->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $revision_id,
'edit' => false,
)
);
} catch (ConduitClientException $ex) {
if (strpos($ex->getMessage(), 'ERR_NOT_FOUND') === false) {
throw $ex;
} else {
throw new ArcanistUsageException(
"Revision D{$revision_id} does not exist."
);
}
}
$revision = $conduit->callMethodSynchronous(
'differential.query',
array(
'ids' => array($revision_id),
));
if (empty($revision)) {
throw new Exception(
"Failed to lookup information for 'D{$revision_id}'!");
}
$revision = head($revision);
$revision_title = $revision['title'];
if (!$is_show) {
if ($revision_id && empty($in_working_copy[$revision_id])) {
$ok = phutil_console_confirm(
"The revision 'D{$revision_id}' does not appear to be in the ".
"working copy. Are you sure you want to amend HEAD with the ".
"commit message for 'D{$revision_id}: {$revision_title}'?");
if (!$ok) {
throw new ArcanistUserAbortException();
}
}
}
if ($is_show) {
echo $message."\n";
} else {
echo phutil_console_format(
"Amending commit message to reflect revision **%s**.\n",
"D{$revision_id}: {$revision_title}");
$repository_api->amendCommit($message);
$mark_workflow = $this->buildChildWorkflow(
'close-revision',
array(
'--finalize',
$revision_id,
));
$mark_workflow->run();
}
return 0;
}
protected function getSupportedRevisionControlSystems() {
return array('git', 'hg');
}
}
diff --git a/src/workflow/ArcanistAnoidWorkflow.php b/src/workflow/ArcanistAnoidWorkflow.php
index cdc3d25c..4a114ea0 100644
--- a/src/workflow/ArcanistAnoidWorkflow.php
+++ b/src/workflow/ArcanistAnoidWorkflow.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* @group workflow
*/
final class ArcanistAnoidWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'anoid';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**anoid**
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
There's only one way to find out...
EOTEXT
);
}
public function run() {
phutil_passthru(
dirname(phutil_get_library_root('arcanist')) . '/scripts/breakout.py'
);
}
}
diff --git a/src/workflow/ArcanistBaseWorkflow.php b/src/workflow/ArcanistBaseWorkflow.php
index 7eb05219..97d93c93 100644
--- a/src/workflow/ArcanistBaseWorkflow.php
+++ b/src/workflow/ArcanistBaseWorkflow.php
@@ -1,1356 +1,1340 @@
<?php
-/*
- * Copyright 2012 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.
*
* = Scratch Files =
*
* Arcanist workflows can read and write 'scratch files', which are temporary
* files stored in the project that persist across commands. They can be useful
* if you want to save some state, or keep a copy of a long message the user
* entered if something goes wrong.
*
*
* @task conduit Conduit
* @task scratch Scratch Files
* @group workflow
* @stable
*/
abstract class ArcanistBaseWorkflow {
private $conduit;
private $conduitURI;
private $conduitCredentials;
private $conduitAuthenticated;
private $forcedConduitVersion;
private $conduitTimeout;
private $userPHID;
private $userName;
private $repositoryAPI;
private $workingCopy;
private $arguments;
private $passedArguments;
private $command;
private $repositoryEncoding;
private $arcanistConfiguration;
private $parentWorkflow;
private $workingDirectory;
private $changeCache = array();
public function __construct() {
}
abstract public function run();
/**
* Return the command used to invoke this workflow from the command like,
* e.g. "help" for @{class:ArcanistHelpWorkflow}.
*
* @return string The command a user types to invoke this workflow.
*/
abstract public function getWorkflowName();
/**
* Return console formatted string with all command synopses.
*
* @return string 6-space indented list of available command synopses.
*/
abstract public function getCommandSynopses();
/**
* Return console formatted string with command help printed in `arc help`.
*
* @return string 10-space indented help to use the command.
*/
abstract public function getCommandHelp();
/* -( 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;
}
/**
* Returns the URI the conduit connection within the workflow uses.
*
* @return string
* @task conduit
*/
final public function getConduitURI() {
return $this->conduitURI;
}
/**
* 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);
if ($this->conduitTimeout) {
$this->conduit->setTimeout($this->conduitTimeout);
}
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->isConduitAuthenticated()) {
throw new Exception(
"You may not set new credentials after authenticating conduit.");
}
$this->conduitCredentials = $credentials;
return $this;
}
/**
* Force arc to identify with a specific Conduit version during the
* protocol handshake. This is primarily useful for development (especially
* for sending diffs which bump the client Conduit version), since the client
* still actually speaks the builtin version of the protocol.
*
* Controlled by the --conduit-version flag.
*
* @param int Version the client should pretend to be.
* @return this
* @task conduit
*/
public function forceConduitVersion($version) {
$this->forcedConduitVersion = $version;
return $this;
}
/**
* Get the protocol version the client should identify with.
*
* @return int Version the client should claim to be.
* @task conduit
*/
public function getConduitVersion() {
return nonempty($this->forcedConduitVersion, 5);
}
/**
* Override the default timeout for Conduit.
*
* Controlled by the --conduit-timeout flag.
*
* @param float Timeout, in seconds.
* @return this
* @task conduit
*/
public function setConduitTimeout($timeout) {
$this->conduitTimeout = $timeout;
if ($this->conduit) {
$this->conduit->setConduitTimeout($timeout);
}
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->isConduitAuthenticated()) {
return $this;
}
$this->establishConduit();
$credentials = $this->conduitCredentials;
try {
if (!$credentials) {
throw new Exception(
"Set conduit credentials with setConduitCredentials() before ".
"authenticating conduit!");
}
if (empty($credentials['user'])) {
throw new ConduitClientException('ERR-INVALID-USER',
'Empty user in credentials.');
}
if (empty($credentials['certificate'])) {
throw new ConduitClientException('ERR-NO-CERTIFICATE',
'Empty certificate in credentials.');
}
$description = idx($credentials, 'description', '');
$user = $credentials['user'];
$certificate = $credentials['certificate'];
$connection = $this->getConduit()->callMethodSynchronous(
'conduit.connect',
array(
'client' => 'arc',
'clientVersion' => $this->getConduitVersion(),
'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 if ($ex->getErrorCode() == 'NEW-ARC-VERSION') {
// Cleverly disguise this as being AWESOME!!!
echo phutil_console_format("**New Version Available!**\n\n");
echo phutil_console_wrap($ex->getMessage());
echo "\n\n";
echo "In most cases, arc can be upgraded automatically.\n";
$ok = phutil_console_confirm(
"Upgrade arc now?",
$default_no = false);
if (!$ok) {
throw $ex;
}
$root = dirname(phutil_get_library_root('arcanist'));
chdir($root);
$err = phutil_passthru('%s upgrade', $root.'/bin/arc');
if (!$err) {
echo "\nTry running your arc command again.\n";
}
exit(1);
} else {
throw $ex;
}
}
$this->userName = $user;
$this->userPHID = $connection['userPHID'];
$this->conduitAuthenticated = true;
return $this;
}
/**
* @return bool True if conduit is authenticated, false otherwise.
* @task conduit
*/
final protected function isConduitAuthenticated() {
return (bool)$this->conduitAuthenticated;
}
/**
* 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 requiresWorkingCopy() {
return false;
}
public function desiresWorkingCopy() {
return false;
}
public function requiresRepositoryAPI() {
return false;
}
public function desiresRepositoryAPI() {
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) {
return idx($this->arguments, $key, $default);
}
public function getPassedArguments() {
return $this->passedArguments;
}
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) {
$this->passedArguments = $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;
}
}
foreach ($spec as $long => $options) {
if (!empty($options['repeat'])) {
$dict[$long] = array();
}
}
$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.");
}
if (!empty($options['repeat'])) {
$dict[$arg_key][] = $args[$ii + 1];
} else {
$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()) {
// Exempt ".arc/" scratch files from this warning so that things work
// a little more smoothly if no one has gotten around to adding .arc to
// the ignore list.
foreach ($untracked as $key => $path) {
if (preg_match('@\.arc/@', $path)) {
unset($untracked[$key]);
}
}
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 ArcanistUncommittedChangesException(
"You have uncommitted changes in this working copy. Commit (or ".
"revert) them before proceeding.\n\n".
$working_copy_desc.
" Uncommitted changes in working copy\n".
" ".implode("\n ", $uncommitted)."\n");
}
}
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);
$bundle->setProjectID($diff['projectName']);
$bundle->setBaseRevision($diff['sourceControlBaseRevision']);
$bundle->setRevisionID($diff['revisionID']);
return $bundle;
}
/**
* Return a list of lines changed by the current diff, or ##null## if the
* change list is meaningless (for example, because the path is a directory
* or binary file).
*
* @param string Path within the repository.
* @param string Change selection mode (see ArcanistDiffHunk).
* @return list|null List of changed line numbers, or null to indicate that
* the path is not a line-oriented text file.
*/
protected function getChangedLines($path, $mode) {
$repository_api = $this->getRepositoryAPI();
$full_path = $repository_api->getPath($path);
if (is_dir($full_path)) {
return null;
}
if (!file_exists($full_path)) {
return null;
}
$change = $this->getChange($path);
if ($change->getFileType() !== ArcanistDiffChangeType::FILE_TEXT) {
return null;
}
$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 preg_replace('/^D/i', '', $revision_id);
}
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 getSystemArcConfigLocation() {
if (phutil_is_windows()) {
// this is a horrible place to put this, but there doesn't seem to be a
// non-horrible place on Windows
return Filesystem::resolvePath(
'Phabricator/Arcanist/config',
getenv('PROGRAMFILES'));
} else {
return '/etc/arcconfig';
}
}
public static function readSystemArcConfig() {
$system_config = array();
$system_config_path = self::getSystemArcConfigLocation();
if (Filesystem::pathExists($system_config_path)) {
$file = Filesystem::readFile($system_config_path);
if ($file) {
$system_config = json_decode($file, true);
}
}
return $system_config;
}
public static function getUserConfigurationFileLocation() {
if (phutil_is_windows()) {
return getenv('APPDATA').'/.arcrc';
} else {
return getenv('HOME').'/.arcrc';
}
}
public static function readUserConfigurationFile() {
$user_config = array();
$user_config_path = self::getUserConfigurationFileLocation();
if (Filesystem::pathExists($user_config_path)) {
if (!phutil_is_windows()) {
$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);
// Drop the stat cache so we don't read the old permissions if
// we end up here again. If we don't do this, we may prompt the user
// to fix permissions multiple times.
clearstatcache();
}
}
$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;
}
public static function writeUserConfigurationFile($config) {
$json_encoder = new PhutilJSON();
$json = $json_encoder->encodeFormatted($config);
$path = self::getUserConfigurationFileLocation();
Filesystem::writeFile($path, $json);
if (!phutil_is_windows()) {
execx('chmod 600 %s', $path);
}
}
public static function readGlobalArcConfig() {
return idx(self::readUserConfigurationFile(), 'config', array());
}
public static function writeGlobalArcConfig(array $options) {
$config = self::readUserConfigurationFile();
$config['config'] = $options;
self::writeUserConfigurationFile($config);
}
public function readLocalArcConfig() {
$local = array();
$file = $this->readScratchFile('config');
if ($file) {
$local = json_decode($file, true);
}
return $local;
}
public function writeLocalArcConfig(array $config) {
$json_encoder = new PhutilJSON();
$json = $json_encoder->encodeFormatted($config);
$this->writeScratchFile('config', $json);
return $this;
}
/**
* 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) {
fwrite(STDERR, $msg);
}
protected function isHistoryImmutable() {
$repository_api = $this->getRepositoryAPI();
$working_copy = $this->getWorkingCopy();
$config = $working_copy->getConfigFromAnySource('history.immutable');
if ($config !== null) {
return $config;
}
return $repository_api->isHistoryDefaultImmutable();
}
/**
* Workflows like 'lint' and 'unit' operate on a list of working copy paths.
* The user can either specify the paths explicitly ("a.js b.php"), or by
* specfifying a revision ("--rev a3f10f1f") to select all paths modified
* since that revision, or by omitting both and letting arc choose the
* default relative revision.
*
* This method takes the user's selections and returns the paths that the
* workflow should act upon.
*
* @param list List of explicitly provided paths.
* @param string|null Revision name, if provided.
* @param mask Mask of ArcanistRepositoryAPI flags to exclude.
* Defaults to ArcanistRepositoryAPI::FLAG_UNTRACKED.
* @return list List of paths the workflow should act on.
*/
protected function selectPathsForWorkflow(
array $paths,
$rev,
$omit_mask = null) {
if ($omit_mask === null) {
$omit_mask = ArcanistRepositoryAPI::FLAG_UNTRACKED;
}
if ($paths) {
$working_copy = $this->getWorkingCopy();
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;
}
} else {
$repository_api = $this->getRepositoryAPI();
if ($rev) {
$repository_api->parseRelativeLocalCommit(array($rev));
}
$paths = $repository_api->getWorkingCopyStatus();
foreach ($paths as $path => $flags) {
if ($flags & $omit_mask) {
unset($paths[$path]);
}
}
$paths = array_keys($paths);
}
return array_values($paths);
}
protected function renderRevisionList(array $revisions) {
$list = array();
foreach ($revisions as $revision) {
$list[] = ' - D'.$revision['id'].': '.$revision['title']."\n";
}
return implode('', $list);
}
/* -( Scratch Files )------------------------------------------------------ */
/**
* Try to read a scratch file, if it exists and is readable.
*
* @param string Scratch file name.
* @return mixed String for file contents, or false for failure.
* @task scratch
*/
protected function readScratchFile($path) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->readScratchFile($path);
}
/**
* Try to read a scratch JSON file, if it exists and is readable.
*
* @param string Scratch file name.
* @return array Empty array for failure.
* @task scratch
*/
protected function readScratchJSONFile($path) {
$file = $this->readScratchFile($path);
if (!$file) {
return array();
}
return json_decode($file, true);
}
/**
* Try to write a scratch file, if there's somewhere to put it and we can
* write there.
*
* @param string Scratch file name to write.
* @param string Data to write.
* @return bool True on success, false on failure.
* @task scratch
*/
protected function writeScratchFile($path, $data) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->writeScratchFile($path, $data);
}
/**
* Try to write a scratch JSON file, if there's somewhere to put it and we can
* write there.
*
* @param string Scratch file name to write.
* @param array Data to write.
* @return bool True on success, false on failure.
* @task scratch
*/
protected function writeScratchJSONFile($path, array $data) {
return $this->writeScratchFile($path, json_encode($data));
}
/**
* Try to remove a scratch file.
*
* @param string Scratch file name to remove.
* @return bool True if the file was removed successfully.
* @task scratch
*/
protected function removeScratchFile($path) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->removeScratchFile($path);
}
/**
* Get a human-readable description of the scratch file location.
*
* @param string Scratch file name.
* @return mixed String, or false on failure.
* @task scratch
*/
protected function getReadableScratchFilePath($path) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->getReadableScratchFilePath($path);
}
/**
* Get the path to a scratch file, if possible.
*
* @param string Scratch file name.
* @return mixed File path, or false on failure.
* @task scratch
*/
protected function getScratchFilePath($path) {
if (!$this->repositoryAPI) {
return false;
}
return $this->getRepositoryAPI()->getScratchFilePath($path);
}
protected function getRepositoryEncoding() {
if ($this->repositoryEncoding) {
return $this->repositoryEncoding;
}
$default = 'UTF-8';
$project_id = $this->getWorkingCopy()->getProjectID();
if (!$project_id) {
return $default;
}
$project_info = $this->getConduit()->callMethodSynchronous(
'arcanist.projectinfo',
array(
'name' => $project_id,
));
$this->repositoryEncoding = nonempty($project_info['encoding'], $default);
return $this->repositoryEncoding;
}
protected function newInteractiveEditor($text) {
$editor = new PhutilInteractiveEditor($text);
$preferred = $this->getWorkingCopy()->getConfigFromAnySource('editor');
if ($preferred) {
$editor->setPreferredEditor($preferred);
}
return $editor;
}
protected function newDiffParser() {
$parser = new ArcanistDiffParser();
if ($this->repositoryAPI) {
$parser->setRepositoryAPI($this->getRepositoryAPI());
}
$parser->setWriteDiffOnFailure(true);
return $parser;
}
protected function dispatchEvent($type, array $data) {
$data += array(
'workflow' => $this,
);
$event = new PhutilEvent($type, $data);
PhutilEventEngine::dispatchEvent($event);
return $event;
}
}
diff --git a/src/workflow/ArcanistBranchWorkflow.php b/src/workflow/ArcanistBranchWorkflow.php
index 717c55a7..dbdf9a85 100644
--- a/src/workflow/ArcanistBranchWorkflow.php
+++ b/src/workflow/ArcanistBranchWorkflow.php
@@ -1,272 +1,256 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Displays user's git branches
*
* @group workflow
*/
final class ArcanistBranchWorkflow extends ArcanistBaseWorkflow {
private $branches;
public function getWorkflowName() {
return 'branch';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**branch** [__options__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git
A wrapper on 'git branch'. It pulls data from Differential and
displays the revision status next to the branch name.
By default, branches are sorted chronologically. You can sort them
by status instead with __--by-status__.
By default, branches that are "Closed" or "Abandoned" are not
displayed. You can show them with __--view-all__.
EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function getArguments() {
return array(
'view-all' => array(
'help' => 'Include closed and abandoned revisions',
),
'by-status' => array(
'help' => 'Sort branches by status instead of time.',
),
);
}
public function run() {
$repository_api = $this->getRepositoryAPI();
if (!($repository_api instanceof ArcanistGitAPI)) {
throw new ArcanistUsageException(
'arc branch is only supported under git.');
}
$branches = $repository_api->getAllBranches();
if (!$branches) {
throw new ArcanistUsageException('No branches in this working copy.');
}
$branches = $this->loadCommitInfo($branches, $repository_api);
$revisions = $this->loadRevisions($branches);
$this->printBranches($branches, $revisions);
return 0;
}
private function loadCommitInfo(
array $branches,
ArcanistRepositoryAPI $repository_api) {
$futures = array();
foreach ($branches as $branch) {
// NOTE: "-s" is an option deep in git's diff argument parser that doesn't
// seem to have much documentation and has no long form. It suppresses any
// diff output.
$futures[$branch['name']] = $repository_api->execFutureLocal(
'show -s --format=%C %s --',
'%H%x01%ct%x01%T%x01%s%x01%b',
$branch['name']);
}
$branches = ipull($branches, null, 'name');
foreach (Futures($futures) as $name => $future) {
list($info) = $future->resolvex();
list($hash, $epoch, $tree, $desc, $text) = explode("\1", trim($info), 5);
$branch = $branches[$name];
$branch['hash'] = $hash;
$branch['desc'] = $desc;
try {
$text = $desc."\n".$text;
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
$id = $message->getRevisionID();
$branch += array(
'epoch' => (int)$epoch,
'tree' => $tree,
'revisionID' => $id,
);
} catch (ArcanistUsageException $ex) {
// In case of invalid commit message which fails the parsing,
// do nothing.
}
$branches[$name] = $branch;
}
return $branches;
}
private function loadRevisions(array $branches) {
$ids = array();
$hashes = array();
foreach ($branches as $branch) {
if ($branch['revisionID']) {
$ids[] = $branch['revisionID'];
}
$hashes[] = array('gtcm', $branch['hash']);
$hashes[] = array('gttr', $branch['tree']);
}
$calls = array();
if ($ids) {
$calls[] = $this->getConduit()->callMethod(
'differential.query',
array(
'ids' => $ids,
));
}
if ($hashes) {
$calls[] = $this->getConduit()->callMethod(
'differential.query',
array(
'commitHashes' => $hashes,
));
}
$results = array();
foreach (Futures($calls) as $call) {
$results[] = $call->resolve();
}
return array_mergev($results);
}
private function printBranches(array $branches, array $revisions) {
$revisions = ipull($revisions, null, 'id');
static $color_map = array(
'Closed' => 'cyan',
'Needs Review' => 'magenta',
'Needs Revision' => 'red',
'Accepted' => 'green',
'No Revision' => 'blue',
'Abandoned' => 'default',
);
static $ssort_map = array(
'Closed' => 1,
'No Revision' => 2,
'Needs Review' => 3,
'Needs Revision' => 4,
'Accepted' => 5,
);
$out = array();
foreach ($branches as $branch) {
$revision = idx($revisions, idx($branch, 'revisionID'));
// If we haven't identified a revision by ID, try to identify it by hash.
if (!$revision) {
foreach ($revisions as $rev) {
$hashes = idx($rev, 'hashes', array());
foreach ($hashes as $hash) {
if (($hash[0] == 'gtcm' && $hash[1] == $branch['hash']) ||
($hash[0] == 'gttr' && $hash[1] == $branch['tree'])) {
$revision = $rev;
break;
}
}
}
}
if ($revision) {
$desc = 'D'.$revision['id'].': '.$revision['title'];
$status = $revision['statusName'];
} else {
$desc = $branch['desc'];
$status = 'No Revision';
}
if (!$this->getArgument('view-all') && !$branch['current']) {
if ($status == 'Closed' || $status == 'Abandoned') {
continue;
}
}
$epoch = $branch['epoch'];
$color = idx($color_map, $status, 'default');
$ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch);
$out[] = array(
'name' => $branch['name'],
'current' => $branch['current'],
'status' => $status,
'desc' => $desc,
'color' => $color,
'esort' => $epoch,
'ssort' => $ssort,
);
}
$len_name = max(array_map('strlen', ipull($out, 'name'))) + 2;
$len_status = max(array_map('strlen', ipull($out, 'status'))) + 2;
if ($this->getArgument('by-status')) {
$out = isort($out, 'ssort');
} else {
$out = isort($out, 'esort');
}
$console = PhutilConsole::getConsole();
foreach ($out as $line) {
$color = $line['color'];
$console->writeOut(
"%s **%s** <fg:{$color}>%s</fg> %s\n",
$line['current'] ? '* ' : ' ',
str_pad($line['name'], $len_name),
str_pad($line['status'], $len_status),
$line['desc']);
}
}
}
diff --git a/src/workflow/ArcanistCallConduitWorkflow.php b/src/workflow/ArcanistCallConduitWorkflow.php
index 76c617c6..e21929c5 100644
--- a/src/workflow/ArcanistCallConduitWorkflow.php
+++ b/src/workflow/ArcanistCallConduitWorkflow.php
@@ -1,108 +1,92 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Provides command-line access to the Conduit API.
*
* @group workflow
*/
final class ArcanistCallConduitWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'call-conduit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**call-conduit** __method__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: http, https
Allows you to make a raw Conduit method call:
- Run this command from a working directory.
- Call parameters are REQUIRED and read as a JSON blob from stdin.
- Results are written to stdout as a JSON blob.
This workflow is primarily useful for writing scripts which integrate
with Phabricator. Examples:
$ echo '{}' | arc call-conduit conduit.ping
$ echo '{"phid":"PHID-FILE-xxxx"}' | arc call-conduit file.download
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'method',
);
}
public function shouldShellComplete() {
return false;
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function run() {
$method = $this->getArgument('method', array());
if (count($method) !== 1) {
throw new ArcanistUsageException(
"Provide exactly one Conduit method name.");
}
$method = reset($method);
$params = @file_get_contents('php://stdin');
$params = json_decode($params, true);
if (!is_array($params)) {
throw new ArcanistUsageException(
"Provide method parameters on stdin as a JSON blob.");
}
$error = null;
$error_message = null;
try {
$result = $this->getConduit()->callMethodSynchronous(
$method,
$params);
} catch (ConduitClientException $ex) {
$error = $ex->getErrorCode();
$error_message = $ex->getMessage();
$result = null;
}
echo json_encode(array(
'error' => $error,
'errorMessage' => $error_message,
'response' => $result,
))."\n";
return 0;
}
}
diff --git a/src/workflow/ArcanistCloseRevisionWorkflow.php b/src/workflow/ArcanistCloseRevisionWorkflow.php
index 23936a6d..995ce4ce 100644
--- a/src/workflow/ArcanistCloseRevisionWorkflow.php
+++ b/src/workflow/ArcanistCloseRevisionWorkflow.php
@@ -1,181 +1,165 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Explicitly closes Differential revisions.
*
* @group workflow
*/
final class ArcanistCloseRevisionWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'close-revision';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**close-revision** [__options__] __revision__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, hg, svn
Close a revision which has been committed (svn) or pushed (git, hg).
You should not normally need to do this: arc commit (svn), arc amend
(git), arc land (git), or repository tracking on the master remote
repository should do it for you. However, if these mechanisms have
failed for some reason you can use this command to manually change a
revision status from "Accepted" to "Closed".
EOTEXT
);
}
public function getArguments() {
return array(
'finalize' => array(
'help' =>
"Close only if the repository is untracked and the revision is ".
"accepted. Continue even if the close can't happen. This is a soft ".
"version of 'close-revision' used by other workflows.",
),
'quiet' => array(
'help' => 'Do not print a success message.',
),
'*' => 'revision',
);
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
// NOTE: Technically we only use this to generate the right message at
// the end, and you can even get the wrong message (e.g., if you run
// "arc close-revision D123" from a git repository, but D123 is an SVN
// revision). We could be smarter about this, but it's just display fluff.
return true;
}
public function run() {
$is_finalize = $this->getArgument('finalize');
$conduit = $this->getConduit();
$revision_list = $this->getArgument('revision', array());
if (!$revision_list) {
throw new ArcanistUsageException(
"close-revision requires a revision number.");
}
if (count($revision_list) != 1) {
throw new ArcanistUsageException(
"close-revision requires exactly one revision.");
}
$revision_id = reset($revision_list);
$revision_id = $this->normalizeRevisionID($revision_id);
$revision = null;
try {
$revision = $conduit->callMethodSynchronous(
'differential.getrevision',
array(
'revision_id' => $revision_id,
)
);
} catch (Exception $ex) {
if (!$is_finalize) {
throw new ArcanistUsageException(
"Revision D{$revision_id} does not exist."
);
}
}
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
if (!$is_finalize && $revision['status'] != $status_accepted) {
throw new ArcanistUsageException(
"Revision D{$revision_id} can not be closed. You can only close ".
"revisions which have been 'accepted'.");
}
if ($revision) {
if (!$is_finalize && $revision['authorPHID'] != $this->getUserPHID()) {
$prompt = "You are not the author of revision D{$revision_id}, ".
'are you sure you want to close it?';
if (!phutil_console_confirm($prompt)) {
throw new ArcanistUserAbortException();
}
}
$actually_close = true;
if ($is_finalize) {
$project_id = $this->getWorkingCopy()->getProjectID();
if ($project_id) {
$project_info = $conduit->callMethodSynchronous(
'arcanist.projectinfo',
array(
'name' => $project_id,
));
if ($project_info['tracked'] ||
$revision['status'] != $status_accepted) {
$actually_close = false;
}
}
}
if ($actually_close) {
$revision_name = $revision['title'];
echo "Closing revision D{$revision_id} '{$revision_name}'...\n";
$conduit->callMethodSynchronous(
'differential.close',
array(
'revisionID' => $revision_id,
));
}
}
$status = $revision['status'];
if ($status == $status_accepted || $status == $status_closed) {
// If this has already been attached to commits, don't show the
// "you can push this commit" message since we know it's been pushed
// already.
$is_finalized = empty($revision['commits']);
} else {
$is_finalized = false;
}
if (!$this->getArgument('quiet')) {
if ($is_finalized) {
$message = $this->getRepositoryAPI()->getFinalizedRevisionMessage();
echo phutil_console_wrap($message)."\n";
} else {
echo "Done.\n";
}
}
return 0;
}
}
diff --git a/src/workflow/ArcanistCloseWorkflow.php b/src/workflow/ArcanistCloseWorkflow.php
index ab02c28e..b9b10bf6 100644
--- a/src/workflow/ArcanistCloseWorkflow.php
+++ b/src/workflow/ArcanistCloseWorkflow.php
@@ -1,143 +1,127 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Close a task
*
* @group workflow
*/
final class ArcanistCloseWorkflow extends ArcanistBaseWorkflow {
private $tasks;
private $statusOptions = array(
"resolved" => 1,
"wontfix" => 2,
"invalid" => 3,
"duplicate" => 4,
"spite" => 5,
"open" => 0
);
public function getWorkflowName() {
return 'close';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**close** __task_id__ [__options__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Close a task.
EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function requiresRepositoryAPI() {
return false;
}
public function requiresAuthentication() {
return true;
}
public function getArguments() {
$options = array_keys($this->statusOptions);
$last = array_pop($options);
return array(
'*' => 'task_id',
'message' => array(
'short' => 'm',
'param' => 'comment',
'help' => "Provide a comment with your status change.",
),
'status' => array(
'param' => 'status',
'short' => 's',
'help' => "New status. Valid options are ".
implode(', ', $options).", or {$last}. Default is resolved.\n"
),
);
}
public function run() {
$ids = $this->getArgument('task_id');
$message = $this->getArgument('message');
$status = strtolower($this->getArgument('status'));
if (!isset($status) || $status == '') {
$status = head_key($this->statusOptions);
}
if (isset($this->statusOptions[$status])) {
$status = $this->statusOptions[$status];
} else {
$options = array_keys($this->statusOptions);
$last = array_pop($options);
echo "Invalid status {$status}, valid options are ".
implode(', ', $options).", or {$last}.\n";
return;
}
foreach ($ids as $id) {
if (!preg_match("/^T?\d+$/", $id)) {
echo "Invalid Task ID: {$id}.\n";
return 1;
}
$id = ltrim($id, 'T');
$result = $this->closeTask($id, $status, $message);
$status_options = array_flip($this->statusOptions);
$current_status = $status_options[$status];
if ($result) {
echo "T{$id}'s status is now set to {$current_status}.\n";
} else {
echo "T{$id} is already set to {$current_status}.\n";
}
}
return 0;
}
private function closeTask($task_id, $status = 1, $comment = "") {
$conduit = $this->getConduit();
$info = $conduit->callMethodSynchronous(
'maniphest.info',
array(
'task_id' => $task_id
));
if ($info['status'] == $status) {
return false;
}
return $conduit->callMethodSynchronous(
'maniphest.update',
array(
'id' => $task_id,
'status' => $status,
'comments' => $comment
));
}
}
diff --git a/src/workflow/ArcanistCommitWorkflow.php b/src/workflow/ArcanistCommitWorkflow.php
index aaceec5c..2802e7ac 100644
--- a/src/workflow/ArcanistCommitWorkflow.php
+++ b/src/workflow/ArcanistCommitWorkflow.php
@@ -1,363 +1,347 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Executes "svn commit" once a revision has been "Accepted".
*
* @group workflow
*/
final class ArcanistCommitWorkflow extends ArcanistBaseWorkflow {
private $revisionID;
public function getWorkflowName() {
return 'commit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**commit** [--revision __revision_id__] [--show]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: svn
Commit a revision which has been accepted by a reviewer.
EOTEXT
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function getRevisionID() {
return $this->revisionID;
}
public function getArguments() {
return array(
'show' => array(
'help' =>
"Show the command which would be issued, but do not actually ".
"commit anything."
),
'revision' => array(
'param' => 'revision_id',
'help' =>
"Commit a specific revision. If you do not specify a revision, ".
"arc will look for committable revisions.",
)
);
}
public function run() {
$repository_api = $this->getRepositoryAPI();
if (!($repository_api instanceof ArcanistSubversionAPI)) {
throw new ArcanistUsageException(
"'arc commit' is only supported under svn.");
}
$revision_id = $this->normalizeRevisionID($this->getArgument('revision'));
if (!$revision_id) {
$revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
array(
'authors' => array($this->getUserPHID()),
'status' => 'status-accepted',
));
if (count($revisions) == 0) {
throw new ArcanistUsageException(
"Unable to identify the revision in the working copy. Use ".
"'--revision <revision_id>' to select a revision.");
} else if (count($revisions) > 1) {
throw new ArcanistUsageException(
"More than one revision exists in the working copy:\n\n".
$this->renderRevisionList($revisions)."\n".
"Use '--revision <revision_id>' to select a revision.");
}
} else {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => array($revision_id),
));
if (count($revisions) == 0) {
throw new ArcanistUsageException(
"Revision 'D{$revision_id}' does not exist.");
}
}
$revision = head($revisions);
$this->revisionID = $revision['id'];
$revision_id = $revision['id'];
$is_show = $this->getArgument('show');
if (!$is_show) {
$this->runSanityChecks($revision);
}
$message = $this->getConduit()->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $revision_id,
'edit' => false,
));
$event = $this->dispatchEvent(
ArcanistEventType::TYPE_COMMIT_WILLCOMMITSVN,
array(
'message' => $message,
));
$message = $event->getValue('message');
if ($is_show) {
echo $message."\n";
return 0;
}
$revision_title = $revision['title'];
echo "Committing 'D{$revision_id}: {$revision_title}'...\n";
$files = $this->getCommitFileList($revision);
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$command = $this->getSVNCommitCommand();
chdir($repository_api->getPath());
$err = phutil_passthru(
$command,
$files,
$tmp_file
);
if ($err) {
throw new Exception("Executing 'svn commit' failed!");
}
$mark_workflow = $this->buildChildWorkflow(
'close-revision',
array(
'--finalize',
$revision_id,
));
$mark_workflow->run();
return $err;
}
protected function getCommitFileList(array $revision) {
$repository_api = $this->getRepositoryAPI();
$revision_id = $revision['id'];
$commit_paths = $this->getConduit()->callMethodSynchronous(
'differential.getcommitpaths',
array(
'revision_id' => $revision_id,
));
$dir_paths = array();
foreach ($commit_paths as $path) {
$path = dirname($path);
while ($path != '.') {
$dir_paths[$path] = true;
$path = dirname($path);
}
}
$commit_paths = array_fill_keys($commit_paths, true);
$status = $repository_api->getSVNStatus();
$modified_but_not_included = array();
foreach ($status as $path => $mask) {
if (!empty($dir_paths[$path])) {
$commit_paths[$path] = true;
}
if (!empty($commit_paths[$path])) {
continue;
}
foreach ($commit_paths as $will_commit => $ignored) {
if (Filesystem::isDescendant($path, $will_commit)) {
throw new ArcanistUsageException(
"This commit includes the directory '{$will_commit}', but ".
"it contains a modified path ('{$path}') which is NOT included ".
"in the commit. Subversion can not handle this operation and ".
"will commit the path anyway. You need to sort out the working ".
"copy changes to '{$path}' before you may proceed with the ".
"commit.");
}
}
$modified_but_not_included[] = $path;
}
if ($modified_but_not_included) {
$prefix = pht(
'Locally modified path(s) are not included in this revision:',
count($modified_but_not_included));
$prompt = pht(
'They will NOT be committed. Commit this revision anyway?',
count($modified_but_not_included));
$this->promptFileWarning($prefix, $prompt, $modified_but_not_included);
}
$do_not_exist = array();
foreach ($commit_paths as $path => $ignored) {
$disk_path = $repository_api->getPath($path);
if (file_exists($disk_path)) {
continue;
}
if (is_link($disk_path)) {
continue;
}
if (idx($status, $path) & ArcanistRepositoryAPI::FLAG_DELETED) {
continue;
}
$do_not_exist[] = $path;
unset($commit_paths[$path]);
}
if ($do_not_exist) {
$prefix = pht(
'Revision includes changes to path(s) that do not exist:',
count($do_not_exist));
$prompt = "Commit this revision anyway?";
$this->promptFileWarning($prefix, $prompt, $do_not_exist);
}
$files = array();
foreach ($commit_paths as $file => $ignored) {
$files[] = $file.'@'; // make SVN accept commits like foo@2x.png
}
if (empty($files)) {
throw new ArcanistUsageException(
"There is nothing left to commit. None of the modified paths exist.");
}
return $files;
}
protected function promptFileWarning($prefix, $prompt, array $paths) {
echo $prefix."\n\n";
foreach ($paths as $path) {
echo " ".$path."\n";
}
if (!phutil_console_confirm($prompt)) {
throw new ArcanistUserAbortException();
}
}
protected function getSupportedRevisionControlSystems() {
return array('svn');
}
/**
* On some systems, we need to specify "en_US.UTF-8" instead of "en_US.utf8",
* and SVN spews some bewildering warnings if we don't:
*
* svn: warning: cannot set LC_CTYPE locale
* svn: warning: environment variable LANG is en_US.utf8
* svn: warning: please check that your locale name is correct
*
* For example, it happens on epriestley's Mac (10.6.7) with
* Subversion 1.6.15.
*/
private function getSVNLangEnvVar() {
$locale = 'en_US.utf8';
try {
list($locales) = execx('locale -a');
$locales = explode("\n", trim($locales));
$locales = array_fill_keys($locales, true);
if (isset($locales['en_US.UTF-8'])) {
$locale = 'en_US.UTF-8';
}
} catch (Exception $ex) {
// Ignore.
}
return $locale;
}
private function getSVNCommitCommand() {
$command = 'svn commit %Ls --encoding utf-8 -F %s';
// make sure to specify LANG on non-windows systems to surpress any fancy
// warnings; see @{method:getSVNLangEnvVar}.
if (!phutil_is_windows()) {
$command = 'LANG='.$this->getSVNLangEnvVar().' '.$command;
}
return $command;
}
private function runSanityChecks(array $revision) {
$repository_api = $this->getRepositoryAPI();
$revision_id = $revision['id'];
$revision_title = $revision['title'];
$confirm = array();
if ($revision['status'] != ArcanistDifferentialRevisionStatus::ACCEPTED) {
$confirm[] =
"Revision 'D{$revision_id}: {$revision_title}' has not been accepted. ".
"Commit this revision anyway?";
}
if ($revision['authorPHID'] != $this->getUserPHID()) {
$confirm[] =
"You are not the author of 'D{$revision_id}: {$revision_title}'. ".
"Commit this revision anyway?";
}
$revision_source = idx($revision, 'sourcePath');
$current_source = $repository_api->getPath();
if ($revision_source != $current_source) {
$confirm[] =
"Revision 'D{$revision_id}: {$revision_title}' was generated from ".
"'{$revision_source}', but current working copy root is ".
"'{$current_source}'. Commit this revision anyway?";
}
foreach ($confirm as $thing) {
if (!phutil_console_confirm($thing)) {
throw new ArcanistUserAbortException();
}
}
}
}
diff --git a/src/workflow/ArcanistCoverWorkflow.php b/src/workflow/ArcanistCoverWorkflow.php
index 8768e59b..3ed4170e 100644
--- a/src/workflow/ArcanistCoverWorkflow.php
+++ b/src/workflow/ArcanistCoverWorkflow.php
@@ -1,190 +1,174 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Covers your professional reputation by blaming changes to locate reviewers.
*
* @group workflow
*/
final class ArcanistCoverWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'cover';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**cover** [--rev __revision__] [__path__ ...]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: svn, git, hg
Cover your... professional reputation. Show blame for the lines you
changed in your working copy (svn) or since some commit (hg, git).
This will take a minute because blame takes a minute, especially under
SVN.
EOTEXT
);
}
public function getArguments() {
return array(
'rev' => array(
'param' => 'revision',
'help' => 'Cover changes since a specific revision.',
'supports' => array(
'git',
'hg',
),
'nosupport' => array(
'svn' => "cover does not currently support --rev in svn.",
),
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresConduit() {
return false;
}
public function requiresAuthentication() {
return false;
}
public function requiresRepositoryAPI() {
return true;
}
public function run() {
$repository_api = $this->getRepositoryAPI();
$in_paths = $this->getArgument('paths');
$in_rev = $this->getArgument('rev');
if ($in_rev) {
// Although selectPathsForWorkflow() may set this, we want to set it
// explicitly so we blame against the correct relative commit.
$repository_api->parseRelativeLocalCommit(array($in_rev));
}
$paths = $this->selectPathsForWorkflow(
$in_paths,
$in_rev,
ArcanistRepositoryAPI::FLAG_UNTRACKED |
ArcanistRepositoryAPI::FLAG_ADDED);
if (!$paths) {
throw new ArcanistNoEffectException(
"You're covered, you didn't change anything.");
}
$covers = array();
foreach ($paths as $path) {
if (is_dir($repository_api->getPath($path))) {
continue;
}
$lines = $this->getChangedLines($path, 'cover');
if (!$lines) {
continue;
}
$blame = $repository_api->getBlame($path);
foreach ($lines as $line) {
list($author, $revision) = idx($blame, $line, array(null, null));
if (!$author) {
continue;
}
if (!isset($covers[$author])) {
$covers[$author] = array();
}
if (!isset($covers[$author][$path])) {
$covers[$author][$path] = array(
'lines' => array(),
'revisions' => array(),
);
}
$covers[$author][$path]['lines'][] = $line;
$covers[$author][$path]['revisions'][] = $revision;
}
}
if (count($covers)) {
foreach ($covers as $author => $files) {
echo phutil_console_format(
"**%s**\n",
$author);
foreach ($files as $file => $info) {
$line_noun = pht('line(s)', count($info['lines']));
$lines = $this->readableSequenceFromLineNumbers($info['lines']);
echo " {$file}: {$line_noun} {$lines}\n";
}
}
} else {
echo "You're covered, your changes didn't touch anyone else's code.\n";
}
return 0;
}
private function readableSequenceFromLineNumbers(array $array) {
$sequence = array();
$last = null;
$seq = null;
$array = array_unique(array_map('intval', $array));
sort($array);
foreach ($array as $element) {
if ($seq !== null && $element == ($seq + 1)) {
$seq++;
continue;
}
if ($seq === null) {
$last = $element;
$seq = $element;
continue;
}
if ($seq > $last) {
$sequence[] = $last.'-'.$seq;
} else {
$sequence[] = $last;
}
$last = $element;
$seq = $element;
}
if ($last !== null && $seq > $last) {
$sequence[] = $last.'-'.$seq;
} else if ($last !== null) {
$sequence[] = $element;
}
return implode(', ', $sequence);
}
}
diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php
index 8b6bbfd3..08163d3f 100644
--- a/src/workflow/ArcanistDiffWorkflow.php
+++ b/src/workflow/ArcanistDiffWorkflow.php
@@ -1,2420 +1,2404 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Sends changes from your working copy to Differential for code review.
*
* @task lintunit Lint and Unit Tests
* @task message Commit and Update Messages
* @task diffspec Diff Specification
* @task diffprop Diff Properties
*
* @group workflow
*/
final class ArcanistDiffWorkflow extends ArcanistBaseWorkflow {
private $console;
private $hasWarnedExternals = false;
private $unresolvedLint;
private $excuses = array('lint' => null, 'unit' => null);
private $testResults;
private $diffID;
private $revisionID;
private $postponedLinters;
private $haveUncommittedChanges = false;
private $diffPropertyFutures = array();
private $commitMessageFromRevision;
public function getWorkflowName() {
return 'diff';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**diff** [__paths__] (svn)
**diff** [__commit__] (git, hg)
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
Generate a Differential diff or revision from local changes.
Under git, you can specify a commit (like __HEAD^^^__ or __master__)
and Differential will generate a diff against the merge base of that
commit and HEAD.
Under svn, you can choose to include only some of the modified files
in the working copy in the diff by specifying their paths. If you
omit paths, all changes are included in the diff.
EOTEXT
);
}
public function requiresWorkingCopy() {
return !$this->isRawDiffSource();
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
if (!$this->isRawDiffSource()) {
return true;
}
if ($this->getArgument('use-commit-message')) {
return true;
}
return false;
}
public function getDiffID() {
return $this->diffID;
}
public function getArguments() {
$arguments = array(
'message' => array(
'short' => 'm',
'param' => 'message',
'help' =>
"When updating a revision, use the specified message instead of ".
"prompting.",
),
'message-file' => array(
'short' => 'F',
'param' => 'file',
'paramtype' => 'file',
'help' => 'When creating a revision, read revision information '.
'from this file.',
),
'use-commit-message' => array(
'supports' => array(
'git',
// TODO: Support mercurial.
),
'short' => 'C',
'param' => 'commit',
'help' => 'Read revision information from a specific commit.',
'conflicts' => array(
'only' => null,
'preview' => null,
'update' => null,
),
),
'edit' => array(
'supports' => array(
'git',
),
'nosupport' => array(
'svn' => 'Edit revisions via the web interface when using SVN.',
),
'help' =>
"When updating a revision under git, edit revision information ".
"before updating.",
),
'raw' => array(
'help' =>
"Read diff from stdin, not from the working copy. This disables ".
"many Arcanist/Phabricator features which depend on having access ".
"to the working copy.",
'conflicts' => array(
'less-context' => null,
'apply-patches' => '--raw disables lint.',
'never-apply-patches' => '--raw disables lint.',
'lintall' => '--raw disables lint.',
'create' => '--raw and --create both need stdin. '.
'Use --raw-command.',
'edit' => '--raw and --edit both need stdin. '.
'Use --raw-command.',
'raw-command' => null,
),
),
'raw-command' => array(
'param' => 'command',
'help' =>
"Generate diff by executing a specified command, not from the ".
"working copy. This disables many Arcanist/Phabricator features ".
"which depend on having access to the working copy.",
'conflicts' => array(
'less-context' => null,
'apply-patches' => '--raw-command disables lint.',
'never-apply-patches' => '--raw-command disables lint.',
'lintall' => '--raw-command disables lint.',
),
),
'create' => array(
'help' => "Always create a new revision.",
'conflicts' => array(
'edit' => '--create can not be used with --edit.',
'only' => '--create can not be used with --only.',
'preview' => '--create can not be used with --preview.',
'update' => '--create can not be used with --update.',
),
),
'update' => array(
'param' => 'revision_id',
'help' => "Always update a specific revision.",
),
'nounit' => array(
'help' =>
"Do not run unit tests.",
),
'nolint' => array(
'help' =>
"Do not run lint.",
'conflicts' => array(
'lintall' => '--nolint suppresses lint.',
'apply-patches' => '--nolint suppresses lint.',
'never-apply-patches' => '--nolint suppresses lint.',
),
),
'only' => array(
'help' =>
"Only generate a diff, without running lint, unit tests, or other ".
"auxiliary steps. See also --preview.",
'conflicts' => array(
'preview' => null,
'message' => '--only does not affect revisions.',
'edit' => '--only does not affect revisions.',
'lintall' => '--only suppresses lint.',
'apply-patches' => '--only suppresses lint.',
'never-apply-patches' => '--only suppresses lint.',
),
),
'preview' => array(
'help' =>
"Instead of creating or updating a revision, only create a diff, ".
"which you may later attach to a revision. This still runs lint ".
"unit tests. See also --only.",
'conflicts' => array(
'only' => null,
'edit' => '--preview does affect revisions.',
'message' => '--preview does not update any revision.',
),
),
'encoding' => array(
'param' => 'encoding',
'help' =>
"Attempt to convert non UTF-8 hunks into specified encoding.",
),
'allow-untracked' => array(
'help' =>
"Skip checks for untracked files in the working copy.",
),
'excuse' => array(
'param' => 'excuse',
'help' => 'Provide a prepared in advance excuse for any lints/tests'.
' shall they fail.',
),
'less-context' => array(
'help' =>
"Normally, files are diffed with full context: the entire file is ".
"sent to Differential so reviewers can 'show more' and see it. If ".
"you are making changes to very large files with tens of thousands ".
"of lines, this may not work well. With this flag, a diff will ".
"be created that has only a few lines of context.",
),
'lintall' => array(
'help' =>
"Raise all lint warnings, not just those on lines you changed.",
'passthru' => array(
'lint' => true,
),
),
'apply-patches' => array(
'help' =>
'Apply patches suggested by lint to the working copy without '.
'prompting.',
'conflicts' => array(
'never-apply-patches' => true,
),
'passthru' => array(
'lint' => true,
),
),
'never-apply-patches' => array(
'help' => 'Never apply patches suggested by lint.',
'conflicts' => array(
'apply-patches' => true,
),
'passthru' => array(
'lint' => true,
),
),
'amend-all' => array(
'help' =>
'When linting git repositories, amend HEAD with all patches '.
'suggested by lint without prompting.',
'passthru' => array(
'lint' => true,
),
),
'amend-autofixes' => array(
'help' =>
'When linting git repositories, amend HEAD with autofix '.
'patches suggested by lint without prompting.',
'passthru' => array(
'lint' => true,
),
),
'json' => array(
'help' =>
'Emit machine-readable JSON. EXPERIMENTAL! Probably does not work!',
),
'no-amend' => array(
'help' => 'Never amend commits in the working copy.',
),
'uncommitted' => array(
'help' => 'Suppress warning about uncommitted changes.',
'supports' => array(
'hg',
),
),
'verbatim' => array(
'help' => 'When creating a revision, try to use the working copy '.
'commit message verbatim, without prompting to edit it. '.
'When updating a revision, update some fields from the '.
'local commit message.',
'supports' => array(
'hg',
'git',
),
'conflicts' => array(
'use-commit-message' => true,
'update' => true,
'only' => true,
'preview' => true,
'raw' => true,
'raw-command' => true,
'message-file' => true,
),
),
'reviewers' => array(
'param' => 'usernames',
'help' => 'When creating a revision, add reviewers.',
'conflicts' => array(
'only' => true,
'preview' => true,
'update' => true,
),
),
'cc' => array(
'param' => 'usernames',
'help' => 'When creating a revision, add CCs.',
'conflicts' => array(
'only' => true,
'preview' => true,
'update' => true,
),
),
'skip-binaries' => array(
'help' => 'Do not upload binaries (like images).',
),
'ignore-unsound-tests' => array(
'help' => 'Ignore unsound test failures without prompting.',
),
'base' => array(
'param' => 'rules',
'help' => 'Additional rules for determining base revision.',
'nosupport' => array(
'svn' => 'Subversion does not use base commits.',
),
'supports' => array('git', 'hg'),
),
'no-diff' => array(
'help' => 'Only run lint and unit tests. Intended for internal use.',
),
'background' => array(
'param' => 'bool',
'help' =>
'Run lint and unit tests on background. '.
'"0" to disable, "1" to enable (default).',
),
'*' => 'paths',
);
if (phutil_is_windows()) {
unset($arguments['background']);
}
return $arguments;
}
public function isRawDiffSource() {
return $this->getArgument('raw') || $this->getArgument('raw-command');
}
public function run() {
$this->console = PhutilConsole::getConsole();
$this->runRepositoryAPISetup();
if ($this->getArgument('no-diff')) {
$this->removeScratchFile('diff-result.json');
$data = $this->runLintUnit();
$this->writeScratchJSONFile('diff-result.json', $data);
return 0;
}
$this->runDiffSetupBasics();
$background = $this->getArgument('background', true);
if ($this->isRawDiffSource() || phutil_is_windows()) {
$background = false;
}
if ($background) {
$argv = $this->getPassedArguments();
if (!PhutilConsoleFormatter::getDisableANSI()) {
array_unshift($argv, '--ansi');
}
$lint_unit = new ExecFuture(
'php %s --recon diff --no-diff %Ls',
phutil_get_library_root('arcanist').'/../scripts/arcanist.php',
$argv);
$lint_unit->write('', true);
$lint_unit->start();
}
$commit_message = $this->buildCommitMessage();
$this->dispatchEvent(
ArcanistEventType::TYPE_DIFF_DIDBUILDMESSAGE,
array());
if (!$this->shouldOnlyCreateDiff()) {
$revision = $this->buildRevisionFromCommitMessage($commit_message);
}
if ($background) {
$server = new PhutilConsoleServer();
$server->addExecFutureClient($lint_unit);
$server->setHandler(array($this, 'handleServerMessage'));
$server->run();
list($err) = $lint_unit->resolve();
$data = $this->readScratchJSONFile('diff-result.json');
if ($err || !$data) {
return 1;
}
} else {
$server = $this->console->getServer();
$server->setHandler(array($this, 'handleServerMessage'));
$data = $this->runLintUnit();
}
$lint_result = $data['lintResult'];
$this->unresolvedLint = $data['unresolvedLint'];
$this->postponedLinters = $data['postponedLinters'];
$unit_result = $data['unitResult'];
$this->testResults = $data['testResults'];
if ($this->getArgument('nolint')) {
$this->excuses['lint'] = $this->getSkipExcuse(
'Provide explanation for skipping lint or press Enter to abort:',
'lint-excuses');
}
if ($this->getArgument('nounit')) {
$this->excuses['unit'] = $this->getSkipExcuse(
'Provide explanation for skipping unit tests or press Enter to abort:',
'unit-excuses');
}
$changes = $this->generateChanges();
if (!$changes) {
throw new ArcanistUsageException(
"There are no changes to generate a diff from!");
}
$diff_spec = array(
'changes' => mpull($changes, 'toDictionary'),
'lintStatus' => $this->getLintStatus($lint_result),
'unitStatus' => $this->getUnitStatus($unit_result),
) + $this->buildDiffSpecification();
$conduit = $this->getConduit();
$diff_info = $conduit->callMethodSynchronous(
'differential.creatediff',
$diff_spec);
$this->diffID = $diff_info['diffid'];
$event = $this->dispatchEvent(
ArcanistEventType::TYPE_DIFF_WASCREATED,
array(
'diffID' => $diff_info['diffid'],
'lintResult' => $lint_result,
'unitResult' => $unit_result,
));
$this->updateLintDiffProperty();
$this->updateUnitDiffProperty();
$this->updateLocalDiffProperty();
$this->resolveDiffPropertyUpdates();
$output_json = $this->getArgument('json');
if ($this->shouldOnlyCreateDiff()) {
if (!$output_json) {
echo phutil_console_format(
"Created a new Differential diff:\n".
" **Diff URI:** __%s__\n\n",
$diff_info['uri']);
} else {
$human = ob_get_clean();
echo json_encode(array(
'diffURI' => $diff_info['uri'],
'diffID' => $this->getDiffID(),
'human' => $human,
))."\n";
ob_start();
}
} else {
$revision['diffid'] = $this->getDiffID();
if ($commit_message->getRevisionID()) {
$future = $conduit->callMethod(
'differential.updaterevision',
$revision);
$result = $future->resolve();
foreach (array('edit-messages.json', 'update-messages.json') as $file) {
$messages = $this->readScratchJSONFile($file);
unset($messages[$revision['id']]);
$this->writeScratchJSONFile($file, $messages);
}
echo "Updated an existing Differential revision:\n";
} else {
$revision['user'] = $this->getUserPHID();
$revision = $this->dispatchWillCreateRevisionEvent($revision);
$result = $conduit->callMethodSynchronous(
'differential.createrevision',
$revision);
$revised_message = $conduit->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $result['revisionid'],
));
if ($this->shouldAmend()) {
$repository_api = $this->getRepositoryAPI();
if ($repository_api->supportsAmend()) {
echo "Updating commit message...\n";
$repository_api->amendCommit($revised_message);
} else {
echo "Commit message was not amended. Amending commit message is ".
"only supported in git and hg (version 2.2 or newer)";
}
}
echo "Created a new Differential revision:\n";
}
$uri = $result['uri'];
echo phutil_console_format(
" **Revision URI:** __%s__\n\n",
$uri);
}
echo "Included changes:\n";
foreach ($changes as $change) {
echo ' '.$change->renderTextSummary()."\n";
}
if ($output_json) {
ob_get_clean();
}
$this->removeScratchFile('create-message');
return 0;
}
private function runRepositoryAPISetup() {
if (!$this->requiresRepositoryAPI()) {
return;
}
$repository_api = $this->getRepositoryAPI();
if ($this->getArgument('less-context')) {
$repository_api->setDiffLinesOfContext(3);
}
$repository_api->setBaseCommitArgumentRules(
$this->getArgument('base', ''));
if ($repository_api->supportsRelativeLocalCommits()) {
// Parse the relative commit as soon as we can, to avoid generating
// caches we need to drop later and expensive discovery operations
// (particularly in Mercurial).
$relative = $this->getArgument('paths');
if ($relative) {
$repository_api->parseRelativeLocalCommit($relative);
}
}
}
private function runDiffSetupBasics() {
$output_json = $this->getArgument('json');
if ($output_json) {
// TODO: We should move this to a higher-level and put an indirection
// layer between echoing stuff and stdout.
ob_start();
}
if ($this->requiresWorkingCopy()) {
try {
$this->requireCleanWorkingCopy();
} catch (ArcanistUncommittedChangesException $ex) {
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistMercurialAPI) {
// Some Mercurial users prefer to use it like SVN, where they don't
// commit changes before sending them for review. This would be a
// pretty bad workflow in Git, but Mercurial users are significantly
// more expert at change management.
$use_dirty_changes = false;
if ($this->getArgument('uncommitted')) {
// OK.
} else {
$ok = phutil_console_confirm(
"You have uncommitted changes in your working copy. You can ".
"include them in the diff, or abort and deal with them. (Use ".
"'--uncommitted' to include them and skip this prompt.) ".
"Do you want to include uncommitted changes in the diff?");
if (!$ok) {
throw $ex;
}
}
$repository_api->setIncludeDirectoryStateInDiffs(true);
$this->haveUncommittedChanges = true;
} else {
throw $ex;
}
}
}
}
private function buildRevisionFromCommitMessage(
ArcanistDifferentialCommitMessage $message) {
$conduit = $this->getConduit();
$revision_id = $message->getRevisionID();
$revision = array(
'fields' => $message->getFields(),
);
if ($revision_id) {
// With '--verbatim', pass the (possibly modified) local fields. This
// allows the user to edit some fields (like "title" and "summary")
// locally without '--edit' and have changes automatically synchronized.
// Without '--verbatim', we do not update the revision to reflect local
// commit message changes.
if ($this->getArgument('verbatim')) {
$use_fields = $message->getFields();
} else {
$use_fields = array();
}
$should_edit = $this->getArgument('edit');
$edit_messages = $this->readScratchJSONFile('edit-messages.json');
$remote_corpus = idx($edit_messages, $revision_id);
if (!$should_edit || !$remote_corpus || $use_fields) {
if ($this->commitMessageFromRevision) {
$remote_corpus = $this->commitMessageFromRevision;
} else {
$remote_corpus = $conduit->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $revision_id,
'edit' => 'edit',
'fields' => $use_fields,
));
}
}
if ($should_edit) {
$edited = $this->newInteractiveEditor($remote_corpus)
->setName('differential-edit-revision-info')
->editInteractively();
if ($edited != $remote_corpus) {
$remote_corpus = $edited;
$edit_messages[$revision_id] = $remote_corpus;
$this->writeScratchJSONFile('edit-messages.json', $edit_messages);
}
}
if ($this->commitMessageFromRevision == $remote_corpus) {
$new_message = $message;
} else {
$new_message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$remote_corpus);
$new_message->pullDataFromConduit($conduit);
}
$revision['fields'] = $new_message->getFields();
$revision['id'] = $revision_id;
$this->revisionID = $revision_id;
$revision['message'] = $this->getArgument('message');
if (!strlen($revision['message'])) {
$update_messages = $this->readScratchJSONFile('update-messages.json');
$update_messages[$revision_id] = $this->getUpdateMessage(
$revision['fields'],
idx($update_messages, $revision_id));
$revision['message'] = ArcanistCommentRemover::removeComments(
$update_messages[$revision_id]);
if (!strlen(trim($revision['message']))) {
throw new ArcanistUserAbortException();
}
$this->writeScratchJSONFile('update-messages.json', $update_messages);
}
}
return $revision;
}
protected function shouldOnlyCreateDiff() {
if ($this->getArgument('create')) {
return false;
}
if ($this->getArgument('update')) {
return false;
}
if ($this->getArgument('use-commit-message')) {
return false;
}
if ($this->isRawDiffSource()) {
return true;
}
return $this->getArgument('preview') ||
$this->getArgument('only');
}
private function generateAffectedPaths() {
if ($this->isRawDiffSource()) {
return array();
}
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) {
$file_list = new FileList($this->getArgument('paths', array()));
$paths = $repository_api->getSVNStatus($externals = true);
foreach ($paths as $path => $mask) {
if (!$file_list->contains($repository_api->getPath($path), true)) {
unset($paths[$path]);
}
}
$warn_externals = array();
foreach ($paths as $path => $mask) {
$any_mod = ($mask & ArcanistRepositoryAPI::FLAG_ADDED) ||
($mask & ArcanistRepositoryAPI::FLAG_MODIFIED) ||
($mask & ArcanistRepositoryAPI::FLAG_DELETED);
if ($mask & ArcanistRepositoryAPI::FLAG_EXTERNALS) {
unset($paths[$path]);
if ($any_mod) {
$warn_externals[] = $path;
}
}
}
if ($warn_externals && !$this->hasWarnedExternals) {
echo phutil_console_format(
"The working copy includes changes to 'svn:externals' paths. These ".
"changes will not be included in the diff because SVN can not ".
"commit 'svn:externals' changes alongside normal changes.".
"\n\n".
"Modified 'svn:externals' files:".
"\n\n".
phutil_console_wrap(implode("\n", $warn_externals), 8));
$prompt = "Generate a diff (with just local changes) anyway?";
if (!phutil_console_confirm($prompt)) {
throw new ArcanistUserAbortException();
} else {
$this->hasWarnedExternals = true;
}
}
} else if ($repository_api->supportsRelativeLocalCommits()) {
$paths = $repository_api->getWorkingCopyStatus();
} else {
throw new Exception("Unknown VCS!");
}
foreach ($paths as $path => $mask) {
if ($mask & ArcanistRepositoryAPI::FLAG_UNTRACKED) {
unset($paths[$path]);
}
}
return $paths;
}
protected function generateChanges() {
$parser = $this->newDiffParser();
$is_raw = $this->isRawDiffSource();
if ($is_raw) {
if ($this->getArgument('raw')) {
fwrite(STDERR, "Reading diff from stdin...\n");
$raw_diff = file_get_contents('php://stdin');
} else if ($this->getArgument('raw-command')) {
list($raw_diff) = execx($this->getArgument('raw-command'));
} else {
throw new Exception("Unknown raw diff source.");
}
$changes = $parser->parseDiff($raw_diff);
foreach ($changes as $key => $change) {
// Remove "message" changes, e.g. from "git show".
if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
unset($changes[$key]);
}
}
return $changes;
}
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) {
$paths = $this->generateAffectedPaths();
$this->primeSubversionWorkingCopyData($paths);
// Check to make sure the user is diffing from a consistent base revision.
// This is mostly just an abuse sanity check because it's silly to do this
// and makes the code more difficult to effectively review, but it also
// affects patches and makes them nonportable.
$bases = $repository_api->getSVNBaseRevisions();
// Remove all files with baserev "0"; these files are new.
foreach ($bases as $path => $baserev) {
if ($bases[$path] <= 0) {
unset($bases[$path]);
}
}
if ($bases) {
$rev = reset($bases);
$revlist = array();
foreach ($bases as $path => $baserev) {
$revlist[] = " Revision {$baserev}, {$path}";
}
$revlist = implode("\n", $revlist);
foreach ($bases as $path => $baserev) {
if ($baserev !== $rev) {
throw new ArcanistUsageException(
"Base revisions of changed paths are mismatched. Update all ".
"paths to the same base revision before creating a diff: ".
"\n\n".
$revlist);
}
}
// If you have a change which affects several files, all of which are
// at a consistent base revision, treat that revision as the effective
// base revision. The use case here is that you made a change to some
// file, which updates it to HEAD, but want to be able to change it
// again without updating the entire working copy. This is a little
// sketchy but it arises in Facebook Ops workflows with config files and
// doesn't have any real material tradeoffs (e.g., these patches are
// perfectly applyable).
$repository_api->overrideSVNBaseRevisionNumber($rev);
}
$changes = $parser->parseSubversionDiff(
$repository_api,
$paths);
} else if ($repository_api instanceof ArcanistGitAPI) {
$diff = $repository_api->getFullGitDiff();
if (!strlen($diff)) {
throw new ArcanistUsageException(
"No changes found. (Did you specify the wrong commit range?)");
}
$changes = $parser->parseDiff($diff);
} else if ($repository_api instanceof ArcanistMercurialAPI) {
$diff = $repository_api->getFullMercurialDiff();
if (!strlen($diff)) {
throw new ArcanistUsageException(
"No changes found. (Did you specify the wrong commit range?)");
}
$changes = $parser->parseDiff($diff);
} else {
throw new Exception("Repository API is not supported.");
}
if (count($changes) > 250) {
$count = number_format(count($changes));
$link =
"http://www.phabricator.com/docs/phabricator/article/".
"Differential_User_Guide_Large_Changes.html";
$message =
"This diff has a very large number of changes ({$count}). ".
"Differential works best for changes which will receive detailed ".
"human review, and not as well for large automated changes or ".
"bulk checkins. See {$link} for information about reviewing big ".
"checkins. Continue anyway?";
if (!phutil_console_confirm($message)) {
throw new ArcanistUsageException(
"Aborted generation of gigantic diff.");
}
}
$limit = 1024 * 1024 * 4;
foreach ($changes as $change) {
$size = 0;
foreach ($change->getHunks() as $hunk) {
$size += strlen($hunk->getCorpus());
}
if ($size > $limit) {
$file_name = $change->getCurrentPath();
$change_size = number_format($size);
$byte_warning =
"Diff for '{$file_name}' with context is {$change_size} bytes in ".
"length. Generally, source changes should not be this large.";
if (!$this->getArgument('less-context')) {
$byte_warning .=
" If this file is a huge text file, try using the ".
"'--less-context' flag.";
}
if ($repository_api instanceof ArcanistSubversionAPI) {
throw new ArcanistUsageException(
"{$byte_warning} If the file is not a text file, mark it as ".
"binary with:".
"\n\n".
" $ svn propset svn:mime-type application/octet-stream <filename>".
"\n");
} else {
$confirm =
"{$byte_warning} If the file is not a text file, you can ".
"mark it 'binary'. Mark this file as 'binary' and continue?";
if (phutil_console_confirm($confirm)) {
$change->convertToBinaryChange();
} else {
throw new ArcanistUsageException(
"Aborted generation of gigantic diff.");
}
}
}
}
$try_encoding = nonempty($this->getArgument('encoding'), null);
$utf8_problems = array();
foreach ($changes as $change) {
foreach ($change->getHunks() as $hunk) {
$corpus = $hunk->getCorpus();
if (!phutil_is_utf8($corpus)) {
// If this corpus is heuristically binary, don't try to convert it.
// mb_check_encoding() and mb_convert_encoding() are both very very
// liberal about what they're willing to process.
$is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus);
if (!$is_binary) {
if (!$try_encoding) {
try {
$try_encoding = $this->getRepositoryEncoding();
} catch (ConduitClientException $e) {
if ($e->getErrorCode() == 'ERR-BAD-ARCANIST-PROJECT') {
echo phutil_console_wrap(
"Lookup of encoding in arcanist project failed\n".
$e->getMessage());
} else {
throw $e;
}
}
}
if ($try_encoding) {
$corpus = phutil_utf8_convert($corpus, 'UTF-8', $try_encoding);
$name = $change->getCurrentPath();
if (phutil_is_utf8($corpus)) {
$this->writeStatusMessage(
"Converted a '{$name}' hunk from '{$try_encoding}' ".
"to UTF-8.\n");
$hunk->setCorpus($corpus);
continue;
}
}
}
$utf8_problems[] = $change;
break;
}
}
}
// If there are non-binary files which aren't valid UTF-8, warn the user
// and treat them as binary changes. See D327 for discussion of why Arcanist
// has this behavior.
if ($utf8_problems) {
$utf8_warning =
pht(
"This diff includes file(s) which are not valid UTF-8 (they contain ".
"invalid byte sequences). You can either stop this workflow and ".
"fix these files, or continue. If you continue, these files will ".
"be marked as binary.",
count($utf8_problems))."\n\n".
"You can learn more about how Phabricator handles character encodings ".
"(and how to configure encoding settings and detect and correct ".
"encoding problems) by reading 'User Guide: UTF-8 and Character ".
"Encoding' in the Phabricator documentation.\n\n";
" ".pht('AFFECTED FILE(S)', count($utf8_problems))."\n";
$confirm = pht(
'Do you want to mark these files as binary and continue?',
count($utf8_problems));
echo phutil_console_format("**Invalid Content Encoding (Non-UTF8)**\n");
echo phutil_console_wrap($utf8_warning);
$file_list = mpull($utf8_problems, 'getCurrentPath');
$file_list = ' '.implode("\n ", $file_list);
echo $file_list;
if (!phutil_console_confirm($confirm, $default_no = false)) {
throw new ArcanistUsageException("Aborted workflow to fix UTF-8.");
} else {
foreach ($utf8_problems as $change) {
$change->convertToBinaryChange();
}
}
}
foreach ($changes as $change) {
if ($change->getFileType() != ArcanistDiffChangeType::FILE_BINARY) {
continue;
}
$path = $change->getCurrentPath();
$name = basename($path);
$old_file = $change->getOriginalFileData();
$old_dict = $this->uploadFile($old_file, $name, 'old binary');
if ($old_dict['guid']) {
$change->setMetadata('old:binary-phid', $old_dict['guid']);
}
$change->setMetadata('old:file:size', $old_dict['size']);
$change->setMetadata('old:file:mime-type', $old_dict['mime']);
$new_file = $change->getCurrentFileData();
$new_dict = $this->uploadFile($new_file, $name, 'new binary');
if ($new_dict['guid']) {
$change->setMetadata('new:binary-phid', $new_dict['guid']);
}
$change->setMetadata('new:file:size', $new_dict['size']);
$change->setMetadata('new:file:mime-type', $new_dict['mime']);
$mime_type = coalesce($new_dict['mime'], $old_dict['mime']);
if (preg_match('@^image/@', $mime_type)) {
$change->setFileType(ArcanistDiffChangeType::FILE_IMAGE);
}
}
return $changes;
}
private function uploadFile($data, $name, $desc) {
$result = array(
'guid' => null,
'mime' => null,
'size' => null
);
if ($this->getArgument('skip-binaries')) {
return $result;
}
$result['size'] = $size = strlen($data);
if (!$size) {
return $result;
}
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$mime_type = Filesystem::getMimeType($tmp);
$result['mime'] = $mime_type;
echo "Uploading {$desc} '{$name}' ({$mime_type}, {$size} bytes)...\n";
try {
$guid = $this->getConduit()->callMethodSynchronous(
'file.upload',
array(
'data_base64' => base64_encode($data),
'name' => $name,
));
$result['guid'] = $guid;
} catch (Exception $e) {
echo "Failed to upload {$desc} '{$name}'.\n";
if (!phutil_console_confirm('Continue?', $default_no = false)) {
throw new ArcanistUsageException(
'Aborted due to file upload failure. You can use --skip-binaries '.
'to skip binary uploads.');
}
}
return $result;
}
private function getGitParentLogInfo() {
$info = array(
'parent' => null,
'base_revision' => null,
'base_path' => null,
'uuid' => null,
);
$conduit = $this->getConduit();
$repository_api = $this->getRepositoryAPI();
$parser = $this->newDiffParser();
$history_messages = $repository_api->getGitHistoryLog();
if (!$history_messages) {
// This can occur on the initial commit.
return $info;
}
$history_messages = $parser->parseDiff($history_messages);
foreach ($history_messages as $key => $change) {
try {
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$change->getMetadata('message'));
if ($message->getRevisionID() && $info['parent'] === null) {
$info['parent'] = $message->getRevisionID();
}
if ($message->getGitSVNBaseRevision() &&
$info['base_revision'] === null) {
$info['base_revision'] = $message->getGitSVNBaseRevision();
$info['base_path'] = $message->getGitSVNBasePath();
}
if ($message->getGitSVNUUID()) {
$info['uuid'] = $message->getGitSVNUUID();
}
if ($info['parent'] && $info['base_revision']) {
break;
}
} catch (ArcanistDifferentialCommitMessageParserException $ex) {
// Ignore.
} catch (ArcanistUsageException $ex) {
// Ignore an invalid Differential Revision field in the parent commit
}
}
return $info;
}
protected function primeSubversionWorkingCopyData($paths) {
$repository_api = $this->getRepositoryAPI();
$futures = array();
$targets = array();
foreach ($paths as $path => $mask) {
$futures[] = $repository_api->buildDiffFuture($path);
$targets[] = array('command' => 'diff', 'path' => $path);
$futures[] = $repository_api->buildInfoFuture($path);
$targets[] = array('command' => 'info', 'path' => $path);
}
foreach (Futures($futures)->limit(8) as $key => $future) {
$target = $targets[$key];
if ($target['command'] == 'diff') {
$repository_api->primeSVNDiffResult(
$target['path'],
$future->resolve());
} else {
$repository_api->primeSVNInfoResult(
$target['path'],
$future->resolve());
}
}
}
private function shouldAmend() {
if ($this->haveUncommittedChanges) {
return false;
}
if ($this->isHistoryImmutable()) {
return false;
}
if ($this->getArgument('no-amend')) {
return false;
}
if ($this->isRawDiffSource()) {
return false;
}
return true;
}
/* -( Lint and Unit Tests )------------------------------------------------ */
/**
* @task lintunit
*/
private function runLintUnit() {
$lint_result = $this->runLint();
$unit_result = $this->runUnit();
return array(
'lintResult' => $lint_result,
'unresolvedLint' => $this->unresolvedLint,
'postponedLinters' => $this->postponedLinters,
'unitResult' => $unit_result,
'testResults' => $this->testResults,
);
}
/**
* @task lintunit
*/
private function runLint() {
if ($this->getArgument('nolint') ||
$this->getArgument('only') ||
$this->isRawDiffSource()) {
return ArcanistLintWorkflow::RESULT_SKIP;
}
$repository_api = $this->getRepositoryAPI();
$this->console->writeOut("Linting...\n");
try {
$argv = $this->getPassthruArgumentsAsArgv('lint');
if ($repository_api->supportsRelativeLocalCommits()) {
$argv[] = '--rev';
$argv[] = $repository_api->getRelativeCommit();
}
$lint_workflow = $this->buildChildWorkflow('lint', $argv);
if ($this->shouldAmend()) {
// TODO: We should offer to create a checkpoint commit.
$lint_workflow->setShouldAmendChanges(true);
}
$lint_result = $lint_workflow->run();
switch ($lint_result) {
case ArcanistLintWorkflow::RESULT_OKAY:
$this->console->writeOut(
"<bg:green>** LINT OKAY **</bg> No lint problems.\n");
break;
case ArcanistLintWorkflow::RESULT_WARNINGS:
$this->getErrorExcuse(
'lint',
"Lint issued unresolved warnings.",
'lint-excuses');
break;
case ArcanistLintWorkflow::RESULT_ERRORS:
$this->console->writeOut(
"<bg:red>** LINT ERRORS **</bg> Lint raised errors!\n");
$this->getErrorExcuse(
'lint',
"Lint issued unresolved errors!",
'lint-excuses');
break;
case ArcanistLintWorkflow::RESULT_POSTPONED:
$this->console->writeOut(
"<bg:yellow>** LINT POSTPONED **</bg> ".
"Lint results are postponed.\n");
break;
}
$this->unresolvedLint = array();
foreach ($lint_workflow->getUnresolvedMessages() as $message) {
$this->unresolvedLint[] = array(
'path' => $message->getPath(),
'line' => $message->getLine(),
'char' => $message->getChar(),
'code' => $message->getCode(),
'severity' => $message->getSeverity(),
'name' => $message->getName(),
'description' => $message->getDescription(),
);
}
$this->postponedLinters = $lint_workflow->getPostponedLinters();
return $lint_result;
} catch (ArcanistNoEngineException $ex) {
$this->console->writeOut("No lint engine configured for this project.\n");
} catch (ArcanistNoEffectException $ex) {
$this->console->writeOut("No paths to lint.\n");
}
return null;
}
/**
* @task lintunit
*/
private function runUnit() {
if ($this->getArgument('nounit') ||
$this->getArgument('only') ||
$this->isRawDiffSource()) {
return ArcanistUnitWorkflow::RESULT_SKIP;
}
$repository_api = $this->getRepositoryAPI();
$this->console->writeOut("Running unit tests...\n");
try {
$argv = $this->getPassthruArgumentsAsArgv('unit');
if ($repository_api->supportsRelativeLocalCommits()) {
$argv[] = '--rev';
$argv[] = $repository_api->getRelativeCommit();
}
$unit_workflow = $this->buildChildWorkflow('unit', $argv);
$unit_result = $unit_workflow->run();
switch ($unit_result) {
case ArcanistUnitWorkflow::RESULT_OKAY:
$this->console->writeOut(
"<bg:green>** UNIT OKAY **</bg> No unit test failures.\n");
break;
case ArcanistUnitWorkflow::RESULT_UNSOUND:
if ($this->getArgument('ignore-unsound-tests')) {
echo phutil_console_format(
"<bg:yellow>** UNIT UNSOUND **</bg> Unit testing raised errors, ".
"but all failing tests are unsound.\n");
} else {
$continue = $this->console->confirm(
"Unit test results included failures, but all failing tests ".
"are known to be unsound. Ignore unsound test failures?");
if (!$continue) {
throw new ArcanistUserAbortException();
}
}
break;
case ArcanistUnitWorkflow::RESULT_FAIL:
$this->console->writeOut(
"<bg:red>** UNIT ERRORS **</bg> Unit testing raised errors!\n");
$this->getErrorExcuse(
'unit',
"Unit test results include failures!",
'unit-excuses');
break;
}
$this->testResults = array();
foreach ($unit_workflow->getTestResults() as $test) {
$this->testResults[] = array(
'name' => $test->getName(),
'link' => $test->getLink(),
'result' => $test->getResult(),
'userdata' => $test->getUserData(),
'coverage' => $test->getCoverage(),
'extra' => $test->getExtraData(),
);
}
return $unit_result;
} catch (ArcanistNoEngineException $ex) {
$this->console->writeOut(
"No unit test engine is configured for this project.\n");
} catch (ArcanistNoEffectException $ex) {
$this->console->writeOut("No tests to run.\n");
}
return null;
}
public function getTestResults() {
return $this->testResults;
}
private function getSkipExcuse($prompt, $history) {
$excuse = $this->getArgument('excuse');
if ($excuse === null) {
$history = $this->getRepositoryAPI()->getScratchFilePath($history);
$excuse = phutil_console_prompt($prompt, $history);
if ($excuse == '') {
throw new ArcanistUserAbortException();
}
}
return $excuse;
}
private function getErrorExcuse($type, $prompt, $history) {
if ($this->getArgument('excuse')) {
$this->console->sendMessage(array(
'type' => $type,
'confirm' => $prompt." Ignore them?",
));
return;
}
$history = $this->getRepositoryAPI()->getScratchFilePath($history);
$prompt .= " Provide explanation to continue or press Enter to abort.";
$this->console->writeOut("\n\n%s", phutil_console_wrap($prompt));
$this->console->sendMessage(array(
'type' => $type,
'prompt' => "Explanation:",
'history' => $history,
));
}
public function handleServerMessage(PhutilConsoleMessage $message) {
$data = $message->getData();
$response = '';
if (isset($data['prompt'])) {
$response = phutil_console_prompt($data['prompt'], idx($data, 'history'));
} else if (phutil_console_confirm($data['confirm'])) {
$response = $this->getArgument('excuse');
}
if ($response == '') {
throw new ArcanistUserAbortException();
}
$this->excuses[$data['type']] = $response;
return null;
}
/* -( Commit and Update Messages )----------------------------------------- */
/**
* @task message
*/
private function buildCommitMessage() {
if ($this->getArgument('preview') || $this->getArgument('only')) {
return null;
}
$is_create = $this->getArgument('create');
$is_update = $this->getArgument('update');
$is_raw = $this->isRawDiffSource();
$is_message = $this->getArgument('use-commit-message');
$is_verbatim = $this->getArgument('verbatim');
if ($is_message) {
return $this->getCommitMessageFromCommit($is_message);
}
if ($is_verbatim) {
return $this->getCommitMessageFromUser();
}
if (!$is_raw && !$is_create && !$is_update) {
$repository_api = $this->getRepositoryAPI();
$revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
array(
'authors' => array($this->getUserPHID()),
'status' => 'status-open',
));
if (!$revisions) {
$is_create = true;
} else if (count($revisions) == 1) {
$revision = head($revisions);
$is_update = $revision['id'];
} else {
throw new ArcanistUsageException(
"There are several revisions which match the working copy:\n\n".
$this->renderRevisionList($revisions)."\n".
"Use '--update' to choose one, or '--create' to create a new ".
"revision.");
}
}
$message = null;
if ($is_create) {
$message_file = $this->getArgument('message-file');
if ($message_file) {
return $this->getCommitMessageFromFile($message_file);
} else {
return $this->getCommitMessageFromUser();
}
} else if ($is_update) {
$revision_id = $this->normalizeRevisionID($is_update);
if (!is_numeric($revision_id)) {
throw new ArcanistUsageException(
'Parameter to --update must be a Differential Revision number');
}
return $this->getCommitMessageFromRevision($revision_id);
} else {
// This is --raw without enough info to create a revision, so force just
// a diff.
return null;
}
}
/**
* @task message
*/
private function getCommitMessageFromCommit($commit) {
$text = $this->getRepositoryAPI()->getCommitMessage($commit);
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
$message->pullDataFromConduit($this->getConduit());
$this->validateCommitMessage($message);
return $message;
}
/**
* @task message
*/
private function getCommitMessageFromUser() {
$conduit = $this->getConduit();
$template = null;
if (!$this->getArgument('verbatim')) {
$saved = $this->readScratchFile('create-message');
if ($saved) {
$where = $this->getReadableScratchFilePath('create-message');
$preview = explode("\n", $saved);
$preview = array_shift($preview);
$preview = trim($preview);
$preview = phutil_utf8_shorten($preview, 64);
if ($preview) {
$preview = "Message begins:\n\n {$preview}\n\n";
} else {
$preview = null;
}
echo
"You have a saved revision message in '{$where}'.\n".
"{$preview}".
"You can use this message, or discard it.";
$use = phutil_console_confirm(
"Do you want to use this message?",
$default_no = false);
if ($use) {
$template = $saved;
} else {
$this->removeScratchFile('create-message');
}
}
}
$template_is_default = false;
$notes = array();
$included = array();
list($fields, $notes, $included_commits) = $this->getDefaultCreateFields();
if ($template) {
$fields = array();
$notes = array();
} else {
if (!$fields) {
$template_is_default = true;
}
if ($notes) {
$commit = head($this->getRepositoryAPI()->getLocalCommitInformation());
$template = $commit['message'];
} else {
$template = $conduit->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => null,
'edit' => 'create',
'fields' => $fields,
));
}
}
$included = array();
if ($included_commits) {
foreach ($included_commits as $commit) {
$included[] = ' '.$commit;
}
$in_branch = '';
if (!$this->isRawDiffSource()) {
$in_branch = ' in branch '.$this->getRepositoryAPI()->getBranchName();
}
$included = array_merge(
array(
"",
"Included commits{$in_branch}:",
"",
),
$included);
}
$issues = array_merge(
array(
'NEW DIFFERENTIAL REVISION',
'Describe the changes in this new revision.',
),
$included,
array(
'',
'arc could not identify any existing revision in your working copy.',
'If you intended to update an existing revision, use:',
'',
' $ arc diff --update <revision>',
));
if ($notes) {
$issues = array_merge($issues, array(''), $notes);
}
$done = false;
$first = true;
while (!$done) {
$template = rtrim($template, "\r\n")."\n\n";
foreach ($issues as $issue) {
$template .= '# '.$issue."\n";
}
$template .= "\n";
if ($first && $this->getArgument('verbatim') && !$template_is_default) {
$new_template = $template;
} else {
$new_template = $this->newInteractiveEditor($template)
->setName('new-commit')
->editInteractively();
}
$first = false;
if ($template_is_default && ($new_template == $template)) {
throw new ArcanistUsageException("Template not edited.");
}
$template = ArcanistCommentRemover::removeComments($new_template);
$repository_api = $this->getRepositoryAPI();
// special check for whether to amend here. optimizes a common git
// workflow. we can't do this for mercurial because the mq extension
// is popular and incompatible with hg commit --amend ; see T2011.
$should_amend = (count($included_commits) == 1 &&
$repository_api instanceof ArcanistGitAPI &&
$this->shouldAmend());
if ($should_amend) {
$repository_api->amendCommit($template);
$wrote = true;
$where = 'commit message';
} else {
$wrote = $this->writeScratchFile('create-message', $template);
$where = "'".$this->getReadableScratchFilePath('create-message')."'";
}
try {
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$template);
$message->pullDataFromConduit($conduit);
$this->validateCommitMessage($message);
$done = true;
} catch (ArcanistDifferentialCommitMessageParserException $ex) {
echo "Commit message has errors:\n\n";
$issues = array('Resolve these errors:');
foreach ($ex->getParserErrors() as $error) {
echo phutil_console_wrap("- ".$error."\n", 6);
$issues[] = ' - '.$error;
}
echo "\n";
echo "You must resolve these errors to continue.";
$again = phutil_console_confirm(
"Do you want to edit the message?",
$default_no = false);
if ($again) {
// Keep going.
} else {
$saved = null;
if ($wrote) {
$saved = "A copy was saved to {$where}.";
}
throw new ArcanistUsageException(
"Message has unresolved errrors. {$saved}");
}
} catch (Exception $ex) {
if ($wrote) {
echo phutil_console_wrap("(Message saved to {$where}.)\n");
}
throw $ex;
}
}
return $message;
}
/**
* @task message
*/
private function getCommitMessageFromFile($file) {
$conduit = $this->getConduit();
$data = Filesystem::readFile($file);
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus($data);
$message->pullDataFromConduit($conduit);
$this->validateCommitMessage($message);
return $message;
}
/**
* @task message
*/
private function getCommitMessageFromRevision($revision_id) {
$id = $revision_id;
$revision = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => array($id),
));
$revision = head($revision);
if (!$revision) {
throw new ArcanistUsageException(
"Revision '{$revision_id}' does not exist!");
}
$this->checkRevisionOwnership($revision);
$message = $this->getConduit()->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $id,
'edit' => false,
));
$this->commitMessageFromRevision = $message;
$obj = ArcanistDifferentialCommitMessage::newFromRawCorpus($message);
$obj->pullDataFromConduit($this->getConduit());
return $obj;
}
/**
* @task message
*/
private function validateCommitMessage(
ArcanistDifferentialCommitMessage $message) {
$futures = array();
$revision_id = $message->getRevisionID();
if ($revision_id) {
$futures['revision'] = $this->getConduit()->callMethod(
'differential.query',
array(
'ids' => array($revision_id),
));
}
$reviewers = $message->getFieldValue('reviewerPHIDs');
if (!$reviewers) {
$confirm = "You have not specified any reviewers. Continue anyway?";
if (!phutil_console_confirm($confirm)) {
throw new ArcanistUsageException('Specify reviewers and retry.');
}
} else {
$futures['reviewers'] = $this->getConduit()->callMethod(
'user.query',
array(
'phids' => $reviewers,
));
}
foreach (Futures($futures) as $key => $future) {
$result = $future->resolve();
switch ($key) {
case 'revision':
if (empty($result)) {
throw new ArcanistUsageException(
"There is no revision D{$revision_id}.");
}
$this->checkRevisionOwnership(head($result));
break;
case 'reviewers':
$untils = array();
foreach ($result as $user) {
if (idx($user, 'currentStatus') == 'away') {
$untils[] = $user['currentStatusUntil'];
}
}
if (count($untils) == count($reviewers)) {
$until = date('l, M j Y', min($untils));
$confirm = "All reviewers are away until {$until}. ".
"Continue anyway?";
if (!phutil_console_confirm($confirm)) {
throw new ArcanistUsageException(
'Specify available reviewers and retry.');
}
}
break;
}
}
}
/**
* @task message
*/
private function getUpdateMessage(array $fields, $template = '') {
if ($this->getArgument('raw')) {
throw new ArcanistUsageException(
"When using '--raw' to update a revision, specify an update message ".
"with '--message'. (Normally, we'd launch an editor to ask you for a ".
"message, but can not do that because stdin is the diff source.)");
}
// When updating a revision using git without specifying '--message', try
// to prefill with the message in HEAD if it isn't a template message. The
// idea is that if you do:
//
// $ git commit -a -m 'fix some junk'
// $ arc diff
//
// ...you shouldn't have to retype the update message. Similar things apply
// to Mercurial.
if ($template == '') {
$comments = $this->getDefaultUpdateMessage();
$template =
rtrim($comments).
"\n\n".
"# Updating D{$fields['revisionID']}: {$fields['title']}\n".
"#\n".
"# Enter a brief description of the changes included in this update.\n".
"# The first line is used as subject, next lines as comment.\n".
"#\n".
"# If you intended to create a new revision, use:\n".
"# $ arc diff --create\n".
"\n";
}
$comments = $this->newInteractiveEditor($template)
->setName('differential-update-comments')
->editInteractively();
return $comments;
}
private function getDefaultCreateFields() {
$result = array(array(), array(), array());
if ($this->isRawDiffSource()) {
return $result;
}
$repository_api = $this->getRepositoryAPI();
$local = $repository_api->getLocalCommitInformation();
if ($local) {
$result = $this->parseCommitMessagesIntoFields($local);
}
$result[0] = $this->dispatchWillBuildEvent($result[0]);
return $result;
}
/**
* Convert a list of commits from `getLocalCommitInformation()` into
* a format usable by arc to create a new diff. Specifically, we emit:
*
* - A dictionary of commit message fields.
* - A list of errors encountered while parsing the messages.
* - A human-readable list of the commits themselves.
*
* For example, if the user runs "arc diff HEAD^^^" and selects a diff range
* which includes several diffs, we attempt to merge them somewhat
* intelligently into a single message, because we can only send one
* "Summary:", "Reviewers:", etc., field to Differential. We also return
* errors (e.g., if the user typed a reviewer name incorrectly) and a
* summary of the commits themselves.
*
* @param dict Local commit information.
* @return list Complex output, see summary.
* @task message
*/
private function parseCommitMessagesIntoFields(array $local) {
$conduit = $this->getConduit();
$local = ipull($local, null, 'commit');
// If the user provided "--reviewers" or "--ccs", add a faux message to
// the list with the implied fields.
$faux_message = array();
if ($this->getArgument('reviewers')) {
$faux_message[] = 'Reviewers: '.$this->getArgument('reviewers');
}
if ($this->getArgument('cc')) {
$faux_message[] = 'CC: '.$this->getArgument('cc');
}
if ($faux_message) {
$faux_message = implode("\n\n", $faux_message);
$local = array(
'(Flags) ' => array(
'message' => $faux_message,
'summary' => 'Command-Line Flags',
),
) + $local;
}
// Build a human-readable list of the commits, so we can show the user which
// commits are included in the diff.
$included = array();
foreach ($local as $hash => $info) {
$included[] = substr($hash, 0, 12).' '.$info['summary'];
}
// Parse all of the messages into fields.
$messages = array();
foreach ($local as $hash => $info) {
$text = $info['message'];
$obj = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
$messages[$hash] = $obj;
}
$notes = array();
$fields = array();
foreach ($messages as $hash => $message) {
try {
$message->pullDataFromConduit($conduit, $partial = true);
$fields[$hash] = $message->getFields();
} catch (ArcanistDifferentialCommitMessageParserException $ex) {
if ($this->getArgument('verbatim')) {
// In verbatim mode, just bail when we hit an error. The user can
// rerun without --verbatim if they want to fix it manually. Most
// users will probably `git commit --amend` instead.
throw $ex;
}
$fields[$hash] = $message->getFields();
$frev = substr($hash, 0, 12);
$notes[] = "NOTE: commit {$frev} could not be completely parsed:";
foreach ($ex->getParserErrors() as $error) {
$notes[] = " - {$error}";
}
}
}
// Merge commit message fields. We do this somewhat-intelligently so that
// multiple "Reviewers" or "CC" fields will merge into the concatenation
// of all values.
// We have special parsing rules for 'title' because we can't merge
// multiple titles, and one-line commit messages like "fix stuff" will
// parse as titles. Instead, pick the first title we encounter. When we
// encounter subsequent titles, treat them as part of the summary. Then
// we merge all the summaries together below.
$result = array();
// Process fields in oldest-first order, so earlier commits get to set the
// title of record and reviewers/ccs are listed in chronological order.
$fields = array_reverse($fields);
foreach ($fields as $hash => $dict) {
$title = idx($dict, 'title');
if (!strlen($title)) {
continue;
}
if (!isset($result['title'])) {
// We don't have a title yet, so use this one.
$result['title'] = $title;
} else {
// We already have a title, so merge this new title into the summary.
$summary = idx($dict, 'summary');
if ($summary) {
$summary = $title."\n\n".$summary;
} else {
$summary = $title;
}
$fields[$hash]['summary'] = $summary;
}
}
// Now, merge all the other fields in a general sort of way.
foreach ($fields as $hash => $dict) {
foreach ($dict as $key => $value) {
if ($key == 'title') {
// This has been handled above, and either assigned directly or
// merged into the summary.
continue;
}
if (is_array($value)) {
// For array values, merge the arrays, appending the new values.
// Examples are "Reviewers" and "Cc", where this produces a list of
// all users specified as reviewers.
$cur = idx($result, $key, array());
$new = array_merge($cur, $value);
$result[$key] = $new;
continue;
} else {
if (!strlen(trim($value))) {
// Ignore empty fields.
continue;
}
// For string values, append the new field to the old field with
// a blank line separating them. Examples are "Test Plan" and
// "Summary".
$cur = idx($result, $key, '');
if (strlen($cur)) {
$new = $cur."\n\n".$value;
} else {
$new = $value;
}
$result[$key] = $new;
}
}
}
return array($result, $notes, $included);
}
private function getDefaultUpdateMessage() {
if ($this->isRawDiffSource()) {
return null;
}
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistGitAPI) {
return $this->getGitUpdateMessage();
}
if ($repository_api instanceof ArcanistMercurialAPI) {
return $this->getMercurialUpdateMessage();
}
return null;
}
/**
* Retrieve the git messages between HEAD and the last update.
*
* @task message
*/
private function getGitUpdateMessage() {
$repository_api = $this->getRepositoryAPI();
$parser = $this->newDiffParser();
$commit_messages = $repository_api->getGitCommitLog();
$commit_messages = $parser->parseDiff($commit_messages);
if (count($commit_messages) == 1) {
// If there's only one message, assume this is an amend-based workflow and
// that using it to prefill doesn't make sense.
return null;
}
// We have more than one message, so figure out which ones are new. We
// do this by pulling the current diff and comparing commit hashes in the
// working copy with attached commit hashes. It's not super important that
// we always get this 100% right, we're just trying to do something
// reasonable.
$local = $this->loadActiveLocalCommitInfo();
$hashes = ipull($local, null, 'commit');
$usable = array();
foreach ($commit_messages as $message) {
$text = $message->getMetadata('message');
$parsed = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
if ($parsed->getRevisionID()) {
// If this is an amended commit message with a revision ID, it's
// certainly not new. Stop marking commits as usable and break out.
break;
}
if (isset($hashes[$message->getCommitHash()])) {
// If this commit is currently part of the diff, stop using commit
// messages, since anything older than this isn't new.
break;
}
// Otherwise, this looks new, so it's a usable commit message.
$usable[] = $text;
}
if (!$usable) {
// No new commit messages, so we don't have anywhere to start from.
return null;
}
return $this->formatUsableLogs($usable);
}
/**
* Retrieve the hg messages between tip and the last update.
*
* @task message
*/
private function getMercurialUpdateMessage() {
$repository_api = $this->getRepositoryAPI();
$messages = $repository_api->getCommitMessageLog();
$local = $this->loadActiveLocalCommitInfo();
$hashes = ipull($local, null, 'commit');
$usable = array();
foreach ($messages as $rev => $message) {
if (isset($hashes[$rev])) {
// If this commit is currently part of the active diff on the revision,
// stop using commit messages, since anything older than this isn't new.
break;
}
// Otherwise, this looks new, so it's a usable commit message.
$usable[] = $message;
}
if (!$usable) {
// No new commit messages, so we don't have anywhere to start from.
return null;
}
return $this->formatUsableLogs($usable);
}
/**
* Format log messages to prefill a diff update.
*
* @task message
*/
private function formatUsableLogs(array $usable) {
// Flip messages so they'll read chronologically (oldest-first) in the
// template, e.g.:
//
// - Added foobar.
// - Fixed foobar bug.
// - Documented foobar.
$usable = array_reverse($usable);
$default = array();
foreach ($usable as $message) {
// Pick the first line out of each message.
$text = trim($message);
$text = head(explode("\n", $text));
$default[] = ' - '.$text."\n";
}
return implode('', $default);
}
private function loadActiveLocalCommitInfo() {
$current_diff = $this->getConduit()->callMethodSynchronous(
'differential.getdiff',
array(
'revision_id' => $this->revisionID,
));
$properties = idx($current_diff, 'properties', array());
return idx($properties, 'local:commits', array());
}
/* -( Diff Specification )------------------------------------------------- */
/**
* @task diffspec
*/
private function getLintStatus($lint_result) {
$map = array(
ArcanistLintWorkflow::RESULT_OKAY => 'okay',
ArcanistLintWorkflow::RESULT_ERRORS => 'fail',
ArcanistLintWorkflow::RESULT_WARNINGS => 'warn',
ArcanistLintWorkflow::RESULT_SKIP => 'skip',
ArcanistLintWorkflow::RESULT_POSTPONED => 'postponed',
);
return idx($map, $lint_result, 'none');
}
/**
* @task diffspec
*/
private function getUnitStatus($unit_result) {
$map = array(
ArcanistUnitWorkflow::RESULT_OKAY => 'okay',
ArcanistUnitWorkflow::RESULT_FAIL => 'fail',
ArcanistUnitWorkflow::RESULT_UNSOUND => 'warn',
ArcanistUnitWorkflow::RESULT_SKIP => 'skip',
ArcanistUnitWorkflow::RESULT_POSTPONED => 'postponed',
);
return idx($map, $unit_result, 'none');
}
/**
* @task diffspec
*/
private function buildDiffSpecification() {
$base_revision = null;
$base_path = null;
$vcs = null;
$repo_uuid = null;
$parent = null;
$source_path = null;
$branch = null;
$bookmark = null;
if (!$this->isRawDiffSource()) {
$repository_api = $this->getRepositoryAPI();
$base_revision = $repository_api->getSourceControlBaseRevision();
$base_path = $repository_api->getSourceControlPath();
$vcs = $repository_api->getSourceControlSystemName();
$source_path = $repository_api->getPath();
$branch = $repository_api->getBranchName();
if ($repository_api instanceof ArcanistGitAPI) {
$info = $this->getGitParentLogInfo();
if ($info['parent']) {
$parent = $info['parent'];
}
if ($info['base_revision']) {
$base_revision = $info['base_revision'];
}
if ($info['base_path']) {
$base_path = $info['base_path'];
}
if ($info['uuid']) {
$repo_uuid = $info['uuid'];
}
} else if ($repository_api instanceof ArcanistSubversionAPI) {
$repo_uuid = $repository_api->getRepositorySVNUUID();
} else if ($repository_api instanceof ArcanistMercurialAPI) {
$bookmark = $repository_api->getActiveBookmark();
$svn_info = $repository_api->getSubversionInfo();
$repo_uuid = idx($svn_info, 'uuid');
$base_path = idx($svn_info, 'base_path', $base_path);
$base_revision = idx($svn_info, 'base_revision', $base_revision);
// TODO: provide parent info
} else {
throw new Exception("Unsupported repository API!");
}
}
$project_id = null;
if ($this->requiresWorkingCopy()) {
$project_id = $this->getWorkingCopy()->getProjectID();
}
return array(
'sourceMachine' => php_uname('n'),
'sourcePath' => $source_path,
'branch' => $branch,
'bookmark' => $bookmark,
'sourceControlSystem' => $vcs,
'sourceControlPath' => $base_path,
'sourceControlBaseRevision' => $base_revision,
'parentRevisionID' => $parent,
'repositoryUUID' => $repo_uuid,
'creationMethod' => 'arc',
'arcanistProject' => $project_id,
'authorPHID' => $this->getUserPHID(),
);
}
/* -( Diff Properties )---------------------------------------------------- */
/**
* Update lint information for the diff.
*
* @return void
*
* @task diffprop
*/
private function updateLintDiffProperty() {
if (strlen($this->excuses['lint'])) {
$this->updateDiffProperty('arc:lint-excuse',
json_encode($this->excuses['lint']));
}
if ($this->unresolvedLint) {
$this->updateDiffProperty('arc:lint', json_encode($this->unresolvedLint));
}
$postponed = $this->postponedLinters;
if ($postponed) {
$this->updateDiffProperty('arc:lint-postponed', json_encode($postponed));
}
}
/**
* Update unit test information for the diff.
*
* @return void
*
* @task diffprop
*/
private function updateUnitDiffProperty() {
if (strlen($this->excuses['unit'])) {
$this->updateDiffProperty('arc:unit-excuse',
json_encode($this->excuses['unit']));
}
if ($this->testResults) {
$this->updateDiffProperty('arc:unit', json_encode($this->testResults));
}
}
/**
* Update local commit information for the diff.
*
* @task diffprop
*/
private function updateLocalDiffProperty() {
if ($this->isRawDiffSource()) {
return;
}
$local_info = $this->getRepositoryAPI()->getLocalCommitInformation();
if (!$local_info) {
return;
}
$this->updateDiffProperty('local:commits', json_encode($local_info));
}
/**
* Update an arbitrary diff property.
*
* @param string Diff property name.
* @param string Diff property value.
* @return void
*
* @task diffprop
*/
private function updateDiffProperty($name, $data) {
$this->diffPropertyFutures[] = $this->getConduit()->callMethod(
'differential.setdiffproperty',
array(
'diff_id' => $this->getDiffID(),
'name' => $name,
'data' => $data,
));
}
/**
* Wait for finishing all diff property updates.
*
* @return void
*
* @task diffprop
*/
private function resolveDiffPropertyUpdates() {
Futures($this->diffPropertyFutures)->resolveAll();
$this->diffPropertyFutures = array();
}
private function dispatchWillCreateRevisionEvent(array $fields) {
$event = $this->dispatchEvent(
ArcanistEventType::TYPE_REVISION_WILLCREATEREVISION,
array(
'specification' => $fields,
));
return $event->getValue('specification');
}
private function dispatchWillBuildEvent(array $fields) {
$event = $this->dispatchEvent(
ArcanistEventType::TYPE_DIFF_WILLBUILDMESSAGE,
array(
'fields' => $fields,
));
return $event->getValue('fields');
}
private function checkRevisionOwnership(array $revision) {
if ($revision['authorPHID'] == $this->getUserPHID()) {
return;
}
$id = $revision['id'];
$title = $revision['title'];
throw new ArcanistUsageException(
"You don't own revision D{$id} '{$title}'. You can only update ".
"revisions you own. You can 'Commandeer' this revision from the web ".
"interface if you want to become the owner.");
}
}
diff --git a/src/workflow/ArcanistDownloadWorkflow.php b/src/workflow/ArcanistDownloadWorkflow.php
index d3210d8a..94c38670 100644
--- a/src/workflow/ArcanistDownloadWorkflow.php
+++ b/src/workflow/ArcanistDownloadWorkflow.php
@@ -1,129 +1,113 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Download a file from Phabricator.
*
* @group workflow
*/
final class ArcanistDownloadWorkflow extends ArcanistBaseWorkflow {
private $id;
private $saveAs;
private $show;
public function getWorkflowName() {
return 'download';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**download** __file__ [--as __name__] [--show]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: filesystems
Download a file to local disk, e.g.:
$ arc download F33 # Download file 'F33'
EOTEXT
);
}
public function getArguments() {
return array(
'show' => array(
'conflicts' => array(
'as' => 'Use --show to direct the file to stdout, or --as to direct '.
'it to a named location.',
),
'help' => 'Write file to stdout instead of to disk.',
),
'as' => array(
'param' => 'name',
'help' => 'Save the file with a specific name rather than the default.',
),
'*' => 'argv',
);
}
protected function didParseArguments() {
$argv = $this->getArgument('argv');
if (!$argv) {
throw new ArcanistUsageException("Specify a file to download.");
}
if (count($argv) > 1) {
throw new ArcanistUsageException("Specify exactly one file to download.");
}
$file = reset($argv);
if (!preg_match('/^F?\d+$/', $file)) {
throw new ArcanistUsageException("Specify file by ID, e.g. F123.");
}
$this->id = (int)ltrim($file, 'F');
$this->saveAs = $this->getArgument('as');
$this->show = $this->getArgument('show');
}
public function requiresAuthentication() {
return true;
}
public function run() {
$conduit = $this->getConduit();
$this->writeStatusMessage("Getting file information...\n");
$info = $conduit->callMethodSynchronous(
'file.info',
array(
'id' => $this->id,
));
$bytes = number_format($info['byteSize']);
$desc = '('.$bytes.' bytes)';
if ($info['name']) {
$desc = "'".$info['name']."' ".$desc;
}
$this->writeStatusMessage("Downloading file {$desc}...\n");
$data = $conduit->callMethodSynchronous(
'file.download',
array(
'phid' => $info['phid'],
));
$data = base64_decode($data);
if ($this->show) {
echo $data;
} else {
$path = Filesystem::writeUniqueFile(
nonempty($this->saveAs, $info['name'], 'file'),
$data);
$this->writeStatusMessage("Saved file as '{$path}'.\n");
}
return 0;
}
}
diff --git a/src/workflow/ArcanistExportWorkflow.php b/src/workflow/ArcanistExportWorkflow.php
index 52143af5..759eabbe 100644
--- a/src/workflow/ArcanistExportWorkflow.php
+++ b/src/workflow/ArcanistExportWorkflow.php
@@ -1,263 +1,247 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Exports changes from Differential or the working copy to a file.
*
* @group workflow
*/
final class ArcanistExportWorkflow extends ArcanistBaseWorkflow {
const SOURCE_LOCAL = 'local';
const SOURCE_DIFF = 'diff';
const SOURCE_REVISION = 'revision';
const FORMAT_GIT = 'git';
const FORMAT_UNIFIED = 'unified';
const FORMAT_BUNDLE = 'arcbundle';
private $source;
private $sourceID;
private $format;
public function getWorkflowName() {
return 'export';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**export** [__paths__] __format__ (svn)
**export** [__commit_range__] __format__ (git)
**export** __--revision__ __revision_id__ __format__
**export** __--diff__ __diff_id__ __format__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn
Export the local changeset (or a Differential changeset) to a file,
in some __format__: git diff (__--git__), unified diff
(__--unified__), or arc bundle (__--arcbundle__ __path__) format.
EOTEXT
);
}
public function getArguments() {
return array(
'git' => array(
'help' =>
"Export change as a git patch. This format is more complete than ".
"unified, but less complete than arc bundles. These patches can be ".
"applied with 'git apply' or 'arc patch'.",
),
'unified' => array(
'help' =>
"Export change as a unified patch. This format is less complete ".
"than git patches or arc bundles. These patches can be applied with ".
"'patch' or 'arc patch'.",
),
'arcbundle' => array(
'param' => 'file',
'help' =>
"Export change as an arc bundle. This format can represent all ".
"changes. These bundles can be applied with 'arc patch'.",
),
'encoding' => array(
'param' => 'encoding',
'help' =>
"Attempt to convert non UTF-8 patch into specified encoding.",
),
'revision' => array(
'param' => 'revision_id',
'help' =>
"Instead of exporting changes from the working copy, export them ".
"from a Differential revision."
),
'diff' => array(
'param' => 'diff_id',
'help' =>
"Instead of exporting changes from the working copy, export them ".
"from a Differential diff."
),
'*' => 'paths',
);
}
protected function didParseArguments() {
$source = self::SOURCE_LOCAL;
$requested = 0;
if ($this->getArgument('revision')) {
$source = self::SOURCE_REVISION;
$requested++;
$source_id = $this->getArgument($source);
$this->sourceID = $this->normalizeRevisionID($source_id);
}
if ($this->getArgument('diff')) {
$source = self::SOURCE_DIFF;
$requested++;
$this->sourceID = $this->getArgument($source);
}
$this->source = $source;
if ($requested > 1) {
throw new ArcanistUsageException(
"Options '--revision' and '--diff' are not compatible. Choose exactly ".
"one change source.");
}
$format = null;
$requested = 0;
if ($this->getArgument('git')) {
$format = self::FORMAT_GIT;
$requested++;
}
if ($this->getArgument('unified')) {
$format = self::FORMAT_UNIFIED;
$requested++;
}
if ($this->getArgument('arcbundle')) {
$format = self::FORMAT_BUNDLE;
$requested++;
}
if ($requested === 0) {
throw new ArcanistUsageException(
"Specify one of '--git', '--unified' or '--arcbundle <path>' to ".
"choose an export format.");
} else if ($requested > 1) {
throw new ArcanistUsageException(
"Options '--git', '--unified' and '--arcbundle' are not compatible. ".
"Choose exactly one export format.");
}
$this->format = $format;
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return $this->requiresConduit();
}
public function requiresRepositoryAPI() {
return $this->getSource() == self::SOURCE_LOCAL;
}
public function requiresWorkingCopy() {
return $this->getSource() == self::SOURCE_LOCAL;
}
private function getSource() {
return $this->source;
}
private function getSourceID() {
return $this->sourceID;
}
private function getFormat() {
return $this->format;
}
public function run() {
$source = $this->getSource();
switch ($source) {
case self::SOURCE_LOCAL:
$repository_api = $this->getRepositoryAPI();
$parser = new ArcanistDiffParser();
$parser->setRepositoryAPI($repository_api);
if ($repository_api instanceof ArcanistGitAPI) {
$repository_api->parseRelativeLocalCommit(
$this->getArgument('paths'));
$diff = $repository_api->getFullGitDiff();
$changes = $parser->parseDiff($diff);
} else {
// TODO: paths support
$paths = $repository_api->getWorkingCopyStatus();
$changes = $parser->parseSubversionDiff(
$repository_api,
$paths);
}
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setProjectID($this->getWorkingCopy()->getProjectID());
$bundle->setBaseRevision(
$repository_api->getSourceControlBaseRevision());
// note we can't get a revision ID for SOURCE_LOCAL
break;
case self::SOURCE_REVISION:
$bundle = $this->loadRevisionBundleFromConduit(
$this->getConduit(),
$this->getSourceID());
break;
case self::SOURCE_DIFF:
$bundle = $this->loadDiffBundleFromConduit(
$this->getConduit(),
$this->getSourceID());
break;
}
$try_encoding = nonempty($this->getArgument('encoding'), null);
if (!$try_encoding) {
try {
$project_info = $this->getConduit()->callMethodSynchronous(
'arcanist.projectinfo',
array(
'name' => $bundle->getProjectID(),
));
$try_encoding = $project_info['encoding'];
} catch (ConduitClientException $e) {
$try_encoding = null;
}
}
if ($try_encoding) {
$bundle->setEncoding($try_encoding);
}
$format = $this->getFormat();
switch ($format) {
case self::FORMAT_GIT:
echo $bundle->toGitPatch();
break;
case self::FORMAT_UNIFIED:
echo $bundle->toUnifiedDiff();
break;
case self::FORMAT_BUNDLE:
$path = $this->getArgument('arcbundle');
echo "Writing bundle to '{$path}'...\n";
$bundle->writeToDisk($path);
echo "done.\n";
break;
}
return 0;
}
}
diff --git a/src/workflow/ArcanistFlagWorkflow.php b/src/workflow/ArcanistFlagWorkflow.php
index cb7c4b6d..a76d7ceb 100644
--- a/src/workflow/ArcanistFlagWorkflow.php
+++ b/src/workflow/ArcanistFlagWorkflow.php
@@ -1,235 +1,219 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* @group workflow
*/
final class ArcanistFlagWorkflow extends ArcanistBaseWorkflow {
private static $colorMap = array(
0 => 'red', // Red
1 => 'yellow', // Orange
2 => 'yellow', // Yellow
3 => 'green', // Green
4 => 'blue', // Blue
5 => 'magenta', // Pink
6 => 'magenta', // Purple
7 => 'default', // Checkered
);
private static $colorSpec = array(
'red' => 0, 'r' => 0, 0 => 0,
'orange' => 1, 'o' => 1, 1 => 1,
'yellow' => 2, 'y' => 2, 2 => 2,
'green' => 3, 'g' => 3, 3 => 3,
'blue' => 4, 'b' => 4, 4 => 4,
'pink' => 5, 'p' => 5, 5 => 5,
'purple' => 6, 'v' => 6, 6 => 6,
'checkered' => 7, 'c' => 7, 7 => 7,
);
public function getWorkflowName() {
return 'flag';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**flag** [__object__ ...]
**flag** __object__ --clear
**flag** __object__ [--edit] [--color __color__] [--note __note__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
In the first form, list objects you've flagged. You can provide the
names of one or more objects (Maniphest tasks T#\##, Differential
revisions D###, Diffusion references rXXX???, or PHIDs PHID-XXX-???)
to print only flags for those objects.
In the second form, clear an existing flag on one object.
In the third form, create or update a flag on one object. Color
defaults to blue and note to empty, but if you omit both you must
pass --edit.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'objects',
'clear' => array(
'help' => 'Delete the flag on an object.'
),
'edit' => array(
'help' => 'Edit the flag on an object.'
),
'color' => array(
'param' => 'color',
'help' => 'Set the color of a flag.'
),
'note' => array(
'param' => 'note',
'help' => 'Set the note on a flag.'
),
);
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
private static function flagWasEdited($flag, $verb) {
$color = idx(self::$colorMap, $flag['color'], 'cyan');
$note = $flag['note'];
if ($note) {
// Make sure notes that are long or have line breaks in them or
// whatever don't mess up the formatting.
$note = implode(' ', preg_split('/\s+/', $note));
$note = ' ('.phutil_utf8_shorten($note, 40, '...').')';
}
echo phutil_console_format(
"<fg:{$color}>%s</fg> flag%s $verb!\n",
$flag['colorName'],
$note);
}
public function run() {
$conduit = $this->getConduit();
$objects = $this->getArgument('objects', array());
$phids = array();
$clear = $this->getArgument('clear');
$edit = $this->getArgument('edit');
// I don't trust PHP to distinguish 0 (red) from null.
$color = $this->getArgument('color', -1);
$note = $this->getArgument('note');
$editing = $edit || ($color != -1) || $note;
if ($editing && $clear) {
throw new ArcanistUsageException("You can't both edit and clear a flag.");
}
if (($editing || $clear) && count($objects) != 1) {
throw new ArcanistUsageException("Specify exactly one object.");
}
if (!empty($objects)) {
// First off, convert the passed objects to PHIDs.
$handles = $conduit->callMethodSynchronous(
'phid.lookup',
array(
'names' => $objects,
));
foreach ($objects as $object) {
if (isset($handles[$object])) {
$phids[$object] = $handles[$object]['phid'];
} else {
echo phutil_console_format("**%s** doesn't exist.\n", $object);
}
}
if (empty($phids)) {
// flag.query treats an empty objectPHIDs parameter as "don't use this
// constraint". However, if the user gives a list of objects but none
// of them exist and have flags, we shouldn't dump the full list on
// them after telling them that. Conveniently, we already told them,
// so we can go quit now.
return 0;
}
}
if ($clear) {
// All right, we're going to clear a flag. First clear it. Then tell the
// user we cleared it. Step four: profit!
$flag = $conduit->callMethodSynchronous(
'flag.delete',
array(
'objectPHID' => head($phids),
));
if (!$flag) {
echo phutil_console_format("**%s** has no flag to clear.\n", $object);
} else {
self::flagWasEdited($flag, 'deleted');
}
} elseif ($editing) {
// Let's set some flags. Just like Minesweeper, but less distracting.
$flag_params = array(
'objectPHID' => head($phids),
);
if (isset(self::$colorSpec[$color])) {
$flag_params['color'] = self::$colorSpec[strtolower($color)];
}
if ($note) {
$flag_params['note'] = $note;
}
$flag = $conduit->callMethodSynchronous(
'flag.edit',
$flag_params
);
self::flagWasEdited($flag, $flag['new'] ? 'created' : 'edited');
} else {
// Okay, list mode. Let's find the flags, which we didn't need to do
// otherwise because Conduit does it for us.
$flags = ipull(
$this->getConduit()->callMethodSynchronous(
'flag.query',
array(
'ownerPHIDs' => array($this->getUserPHID()),
'objectPHIDs' => array_values($phids),
)),
null,
'objectPHID');
foreach ($phids as $object => $phid) {
if (!isset($flags[$phid])) {
echo phutil_console_format("**%s** has no flag.\n", $object);
}
}
if (empty($flags)) {
// If the user passed no object names, then we should print the full
// list, but it's empty, so tell the user they have no flags.
// If the user passed object names, we already told them all their
// objects are nonexistent or unflagged.
if (empty($objects)) {
echo "You have no flagged objects.\n";
}
} else {
// Print ALL the flags. With fancy formatting. Because fancy formatting
// is _cool_.
$name_len = 1 + max(array_map('strlen', ipull($flags, 'colorName')));
foreach ($flags as $flag) {
$color = idx(self::$colorMap, $flag['color'], 'cyan');
echo phutil_console_format(
"[<fg:{$color}>%s</fg>] %s\n",
str_pad($flag['colorName'], $name_len),
$flag['handle']['fullname']);
if ($flag['note']) {
$note = phutil_console_wrap($flag['note'], $name_len + 3);
echo rtrim($note)."\n";
}
}
}
}
}
}
diff --git a/src/workflow/ArcanistGetConfigWorkflow.php b/src/workflow/ArcanistGetConfigWorkflow.php
index fa8f5481..955ebd58 100644
--- a/src/workflow/ArcanistGetConfigWorkflow.php
+++ b/src/workflow/ArcanistGetConfigWorkflow.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Read configuration settings.
*
* @group workflow
*/
final class ArcanistGetConfigWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'get-config';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**get-config** -- [__name__ ...]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Reads an arc configuration option. With no argument, reads all
options.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'argv',
);
}
public function desiresRepositoryAPI() {
return true;
}
public function run() {
$argv = $this->getArgument('argv');
$settings = new ArcanistSettings();
$configs = array(
'system' => self::readSystemArcConfig(),
'global' => self::readGlobalArcConfig(),
'project' => $this->getWorkingCopy()->getProjectConfig(),
'local' => $this->readLocalArcConfig(),
);
if ($argv) {
$keys = $argv;
} else {
$keys = array_mergev(array_map('array_keys', $configs));
$keys = array_unique($keys);
sort($keys);
}
$multi = (count($keys) > 1);
foreach ($keys as $key) {
if ($multi) {
echo "{$key}\n";
}
foreach ($configs as $name => $config) {
switch ($name) {
case 'project':
// Respect older names in project config.
$val = $this->getWorkingCopy()->getConfig($key);
break;
default:
$val = idx($config, $key);
break;
}
if ($val === null) {
continue;
}
$val = $settings->formatConfigValueForDisplay($key, $val);
printf("% 10.10s: %s\n", $name, $val);
}
if ($multi) {
echo "\n";
}
}
return 0;
}
}
diff --git a/src/workflow/ArcanistGitHookPreReceiveWorkflow.php b/src/workflow/ArcanistGitHookPreReceiveWorkflow.php
index 1abf89c9..b8201085 100644
--- a/src/workflow/ArcanistGitHookPreReceiveWorkflow.php
+++ b/src/workflow/ArcanistGitHookPreReceiveWorkflow.php
@@ -1,138 +1,122 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Installable as a git pre-receive hook.
*
* @group workflow
*/
final class ArcanistGitHookPreReceiveWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'git-hook-pre-receive';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**git-hook-pre-receive**
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git
You can install this as a git pre-receive hook.
EOTEXT
);
}
public function getArguments() {
return array(
);
}
public function requiresConduit() {
return true;
}
public function requiresWorkingCopy() {
return true;
}
public function shouldShellComplete() {
return false;
}
public function run() {
$working_copy = $this->getWorkingCopy();
if (!$working_copy->getProjectID()) {
throw new ArcanistUsageException(
"You have installed a git pre-receive hook in a remote without an ".
".arcconfig.");
}
// Git repositories have special rules in pre-receive hooks. We need to
// construct the API against the .git directory instead of the project
// root or commands don't work properly.
$repository_api = ArcanistGitAPI::newHookAPI($_SERVER['PWD']);
$root = $working_copy->getProjectRoot();
$parser = new ArcanistDiffParser();
$mark_revisions = array();
$stdin = file_get_contents('php://stdin');
$commits = array_filter(explode("\n", $stdin));
foreach ($commits as $commit) {
list($old_ref, $new_ref, $refname) = explode(' ', $commit);
list($log) = execx(
'(cd %s && git log -n1 %s)',
$repository_api->getPath(),
$new_ref);
$message_log = reset($parser->parseDiff($log));
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$message_log->getMetadata('message'));
$revision_id = $message->getRevisionID();
if ($revision_id) {
$mark_revisions[] = $revision_id;
}
// TODO: Do commit message junk.
$info = $repository_api->getPreReceiveHookStatus($old_ref, $new_ref);
$paths = ipull($info, 'mask');
$frefs = ipull($info, 'ref');
$data = array();
foreach ($paths as $path => $mask) {
list($stdout) = execx(
'(cd %s && git cat-file blob %s)',
$repository_api->getPath(),
$frefs[$path]);
$data[$path] = $stdout;
}
// TODO: Do commit content junk.
$commit_name = $new_ref;
if ($revision_id) {
$commit_name = 'D'.$revision_id.' ('.$commit_name.')';
}
echo "[arc pre-receive] {$commit_name} OK...\n";
}
$conduit = $this->getConduit();
$futures = array();
foreach ($mark_revisions as $revision_id) {
$futures[] = $conduit->callMethod(
'differential.close',
array(
'revisionID' => $revision_id,
));
}
Futures($futures)->resolveAll();
return 0;
}
}
diff --git a/src/workflow/ArcanistHelpWorkflow.php b/src/workflow/ArcanistHelpWorkflow.php
index ca9a7776..8b358e33 100644
--- a/src/workflow/ArcanistHelpWorkflow.php
+++ b/src/workflow/ArcanistHelpWorkflow.php
@@ -1,221 +1,205 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Seduces the reader with majestic prose.
*
* @group workflow
*/
final class ArcanistHelpWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'help';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**help** [__command__]
**help** --full
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: english
Shows this help. With __command__, shows help about a specific
command.
EOTEXT
);
}
public function getArguments() {
return array(
'full' => array(
'help' => 'Print detailed information about each command.',
),
'*' => 'command',
);
}
public function run() {
$arc_config = $this->getArcanistConfiguration();
$workflows = $arc_config->buildAllWorkflows();
ksort($workflows);
$target = null;
if ($this->getArgument('command')) {
$target = head($this->getArgument('command'));
if (empty($workflows[$target])) {
throw new ArcanistUsageException(
"Unrecognized command '{$target}'. Try 'arc help'.");
}
}
$cmdref = array();
foreach ($workflows as $command => $workflow) {
if ($target && $target != $command) {
continue;
}
if (!$target && !$this->getArgument('full')) {
$cmdref[] = $workflow->getCommandSynopses();
continue;
}
$optref = array();
$arguments = $workflow->getArguments();
$config_arguments = $arc_config->getCustomArgumentsForCommand($command);
// This juggling is to put the extension arguments after the normal
// arguments, and make sure the normal arguments aren't overwritten.
ksort($arguments);
ksort($config_arguments);
foreach ($config_arguments as $argument => $spec) {
if (empty($arguments[$argument])) {
$arguments[$argument] = $spec;
}
}
foreach ($arguments as $argument => $spec) {
if ($argument == '*') {
continue;
}
if (!empty($spec['hide'])) {
continue;
}
if (isset($spec['param'])) {
if (isset($spec['short'])) {
$optref[] = phutil_console_format(
" __--%s__ __%s__, __-%s__ __%s__",
$argument,
$spec['param'],
$spec['short'],
$spec['param']);
} else {
$optref[] = phutil_console_format(
" __--%s__ __%s__",
$argument,
$spec['param']);
}
} else {
if (isset($spec['short'])) {
$optref[] = phutil_console_format(
" __--%s__, __-%s__",
$argument,
$spec['short']);
} else {
$optref[] = phutil_console_format(
" __--%s__",
$argument);
}
}
if (isset($config_arguments[$argument])) {
$optref[] = " (This is a custom option for this ".
"project.)";
}
if (isset($spec['supports'])) {
$optref[] = " Supports: ".
implode(', ', $spec['supports']);
}
if (isset($spec['help'])) {
$docs = $spec['help'];
} else {
$docs = 'This option is not documented.';
}
$docs = phutil_console_wrap($docs, 14);
$optref[] = "{$docs}\n";
}
if ($optref) {
$optref = implode("\n", $optref);
$optref = "\n\n".$optref;
} else {
$optref = "\n";
}
$cmdref[] =
$workflow->getCommandSynopses()."\n".
$workflow->getCommandHelp().
$optref;
}
$cmdref = implode("\n\n", $cmdref);
if ($target) {
echo "\n".$cmdref."\n";
return;
}
$self = 'arc';
echo phutil_console_format(<<<EOTEXT
**NAME**
**{$self}** - arcanist, a code review and revision management utility
**SYNOPSIS**
**{$self}** __command__ [__options__] [__args__]
This help file provides a detailed command reference.
**COMMAND REFERENCE**
{$cmdref}
EOTEXT
);
if (!$this->getArgument('full')) {
echo "Run 'arc help --full' to get commands and options descriptions.\n";
return;
}
echo phutil_console_format(<<<EOTEXT
**OPTION REFERENCE**
__--trace__
Debugging command. Shows underlying commands as they are executed,
and full stack traces when exceptions are thrown.
__--no-ansi__
Output in plain ASCII text only, without color or style.
__--ansi__
Use formatting even in environments which probably don't support it.
Example: arc --ansi unit | less -r
__--load-phutil-library=/path/to/library__
Ignore libraries listed in .arcconfig and explicitly load specified
libraries instead. Mostly useful for Arcanist development.
__--conduit-uri__ __uri__
Ignore configured Conduit URI and use an explicit one instead. Mostly
useful for Arcanist development.
__--conduit-version__ __version__
Ignore software version and claim to be running some other version
instead. Mostly useful for Arcanist development. May cause bad things
to happen.
__--conduit-timeout__ __timeout__
Override the default Conduit timeout. Specified in seconds.
EOTEXT
);
}
}
diff --git a/src/workflow/ArcanistInlinesWorkflow.php b/src/workflow/ArcanistInlinesWorkflow.php
index 6b92c1d3..5b2539dd 100644
--- a/src/workflow/ArcanistInlinesWorkflow.php
+++ b/src/workflow/ArcanistInlinesWorkflow.php
@@ -1,121 +1,105 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* @group workflow
*/
final class ArcanistInlinesWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'inlines';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**inlines** [--revision __revision_id__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Display inline comments related to a particular revision.
EOTEXT
);
}
public function getArguments() {
return array(
'revision' => array(
'param' => 'revision_id',
'help' =>
"Display inline comments for a specific revision. If you do not ".
"specify a revision, arc will look in the commit message at HEAD.",
),
'root' => array(
'param' => 'directory',
'help' => "Specify a string printed in front of each path.",
),
);
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function run() {
if ($this->getArgument('revision')) {
$revision_id = $this->normalizeRevisionID($this->getArgument('revision'));
} else {
$revisions = $this->getRepositoryAPI()
->loadWorkingCopyDifferentialRevisions($this->getConduit(), array());
$revision_id = head(ipull($revisions, 'id'));
}
if (!$revision_id) {
throw new ArcanistUsageException("No revisions found.");
}
$comments = array_mergev(
$this->getConduit()->callMethodSynchronous(
'differential.getrevisioncomments',
array(
'ids' => array($revision_id),
'inlines' => true,
)));
$authors = array();
if ($comments) {
$authors = $this->getConduit()->callMethodSynchronous(
'user.query',
array(
'phids' => array_unique(ipull($comments, 'authorPHID')),
));
$authors = ipull($authors, 'userName', 'phid');
}
$inlines = array();
foreach ($comments as $comment) {
$author = idx($authors, $comment['authorPHID']);
foreach ($comment['inlines'] as $inline) {
$file = $inline['filePath'];
$line = $inline['lineNumber'];
$inlines[$file][$line][] = "({$author}) {$inline['content']}";
}
}
$root = $this->getArgument('root');
ksort($inlines);
foreach ($inlines as $file => $file_inlines) {
ksort($file_inlines);
foreach ($file_inlines as $line => $line_inlines) {
foreach ($line_inlines as $content) {
echo "{$root}{$file}:{$line}:{$content}\n";
}
}
}
}
}
diff --git a/src/workflow/ArcanistInstallCertificateWorkflow.php b/src/workflow/ArcanistInstallCertificateWorkflow.php
index 56a29ef8..5d05892a 100644
--- a/src/workflow/ArcanistInstallCertificateWorkflow.php
+++ b/src/workflow/ArcanistInstallCertificateWorkflow.php
@@ -1,154 +1,138 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Installs arcanist certificates.
*
* @group workflow
*/
final class ArcanistInstallCertificateWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'install-certificate';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**install-certificate** [uri]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: http, https
Installs Conduit credentials into your ~/.arcrc for the given install
of Phabricator. You need to do this before you can use 'arc', as it
enables 'arc' to link your command-line activity with your account on
the web. Run this command from within a project directory to install
that project's certificate, or specify an explicit URI (like
"https://phabricator.example.com/").
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'uri',
);
}
public function shouldShellComplete() {
return false;
}
public function requiresConduit() {
return false;
}
public function requiresWorkingCopy() {
return false;
}
public function run() {
$uri = $this->determineConduitURI();
echo "Installing certificate for '{$uri}'...\n";
$config = self::readUserConfigurationFile();
echo "Trying to connect to server...\n";
$conduit = new ConduitClient($uri);
try {
$conduit->callMethodSynchronous('conduit.ping', array());
} catch (Exception $ex) {
throw new ArcanistUsageException(
"Failed to connect to server: ".$ex->getMessage());
}
echo "Connection OK!\n";
$token_uri = new PhutilURI($uri);
$token_uri->setPath('/conduit/token/');
echo "\n";
echo phutil_console_format("**LOGIN TO PHABRICATOR**\n");
echo "Open this page in your browser and login to Phabricator if ".
"necessary:\n";
echo "\n";
echo " {$token_uri}\n";
echo "\n";
echo "Then paste the token on that page below.";
do {
$token = phutil_console_prompt('Paste token from that page:');
$token = trim($token);
if (strlen($token)) {
break;
}
} while (true);
echo "\n";
echo "Downloading authentication certificate...\n";
$info = $conduit->callMethodSynchronous(
'conduit.getcertificate',
array(
'token' => $token,
'host' => $uri,
));
$user = $info['username'];
echo "Installing certificate for '{$user}'...\n";
$config['hosts'][$uri] = array(
'user' => $user,
'cert' => $info['certificate'],
);
echo "Writing ~/.arcrc...\n";
self::writeUserConfigurationFile($config);
echo phutil_console_format(
"<bg:green>** SUCCESS! **</bg> Certificate installed.\n");
return 0;
}
private function determineConduitURI() {
$uri = $this->getArgument('uri');
if (count($uri) > 1) {
throw new ArcanistUsageException("Specify at most one URI.");
} else if (count($uri) == 1) {
$uri = reset($uri);
} else {
$conduit_uri = $this->getConduitURI();
if (!$conduit_uri) {
throw new ArcanistUsageException(
"Specify an explicit URI or run this command from within a project ".
"which is configured with a .arcconfig.");
}
$uri = $conduit_uri;
}
$uri = new PhutilURI($uri);
$uri->setPath('/api/');
return (string)$uri;
}
}
diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php
index 044e23f4..ec8e2214 100644
--- a/src/workflow/ArcanistLandWorkflow.php
+++ b/src/workflow/ArcanistLandWorkflow.php
@@ -1,401 +1,385 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Lands a branch by rebasing, merging and amending it.
*
* @group workflow
*/
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'land';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**land** [__options__] [__branch__] [--onto __master__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git
Land an accepted change (currently sitting in local feature branch
__branch__) onto __master__ and push it to the remote. Then, delete
the feature branch. If you omit __branch__, the current branch will
be used.
In mutable repositories, this will perform a --squash merge (the
entire branch will be represented by one commit on __master__). In
immutable repositories (or when --merge is provided), it will perform
a --no-ff merge (the branch will always be merged into __master__ with
a merge commit).
EOTEXT
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function getArguments() {
return array(
'onto' => array(
'param' => 'master',
'help' => "Land feature branch onto a branch other than ".
"'master' (default). You can change the default by setting ".
"'arc.land.onto.default' with `arc set-config` or for the ".
"entire project in .arcconfig.",
),
'hold' => array(
'help' => "Prepare the change to be pushed, but do not actually ".
"push it.",
),
'keep-branch' => array(
'help' => "Keep the feature branch after pushing changes to the ".
"remote (by default, it is deleted).",
),
'remote' => array(
'param' => 'origin',
'help' => "Push to a remote other than 'origin' (default).",
),
'merge' => array(
'help' => 'Perform a --no-ff merge, not a --squash merge. If the '.
'project is marked as having an immutable history, this is '.
'the default behavior.',
),
'squash' => array(
'help' => 'Perform a --squash merge, not a --no-ff merge. If the '.
'project is marked as having a mutable history, this is '.
'the default behavior.',
'conflicts' => array(
'merge' => '--merge and --squash are conflicting merge strategies.',
),
),
'delete-remote' => array(
'help' => 'Delete the feature branch in the remote after '.
'landing it.',
'conflicts' => array(
'keep-branch' => true,
),
),
'revision' => array(
'param' => 'id',
'help' => 'Use the message from a specific revision, rather than '.
'inferring the revision based on branch content.',
),
'*' => 'branch',
);
}
public function run() {
$repository_api = $this->getRepositoryAPI();
if (!($repository_api instanceof ArcanistGitAPI)) {
throw new ArcanistUsageException("'arc land' only supports git.");
}
$branch = $this->getArgument('branch');
if (empty($branch)) {
$branch = $repository_api->getBranchName();
if ($branch) {
echo "Landing current branch '{$branch}'.\n";
$branch = array($branch);
}
}
if (count($branch) !== 1) {
throw new ArcanistUsageException(
"Specify exactly one branch to land changes from.");
}
$branch = head($branch);
$onto_default = nonempty(
$this->getWorkingCopy()->getConfigFromAnySource('arc.land.onto.default'),
'master');
$remote = $this->getArgument('remote', 'origin');
$onto = $this->getArgument('onto', $onto_default);
$is_immutable = $this->isHistoryImmutable();
if ($onto == $branch) {
$message =
"You can not land a branch onto itself -- you are trying to land ".
"'{$branch}' onto '{$onto}'. For more information on how to push ".
"changes, see 'Pushing and Closing Revisions' in ".
"'Arcanist User Guide: arc diff' in the documentation.";
if (!$is_immutable) {
$message .= " You may be able to 'arc amend' instead.";
}
throw new ArcanistUsageException($message);
}
if ($this->getArgument('merge')) {
$use_squash = false;
} else if ($this->getArgument('squash')) {
$use_squash = true;
} else {
$use_squash = !$is_immutable;
}
list($err) = $repository_api->execManualLocal(
'rev-parse --verify %s',
$branch);
if ($err) {
throw new ArcanistUsageException("Branch '{$branch}' does not exist.");
}
$this->requireCleanWorkingCopy();
$repository_api->parseRelativeLocalCommit(array($remote.'/'.$onto));
$old_branch = $repository_api->getBranchName();
$repository_api->execxLocal('checkout %s', $onto);
echo phutil_console_format(
"Switched to branch **%s**. Updating branch...\n",
$onto);
$repository_api->execxLocal('pull --ff-only');
list($out) = $repository_api->execxLocal(
'log %s/%s..%s',
$remote,
$onto,
$onto);
if (strlen(trim($out))) {
throw new ArcanistUsageException(
"Local branch '{$onto}' is ahead of '{$remote}/{$onto}', so landing ".
"a feature branch would push additional changes. Push or reset the ".
"changes in '{$onto}' before running 'arc land'.");
}
$repository_api->execxLocal(
'checkout %s',
$branch);
echo phutil_console_format(
"Switched to branch **%s**. Identifying and merging...\n",
$branch);
if ($use_squash) {
chdir($repository_api->getPath());
$err = phutil_passthru('git rebase %s', $onto);
if ($err) {
throw new ArcanistUsageException(
"'git rebase {$onto}' failed. You can abort with 'git rebase ".
"--abort', or resolve conflicts and use 'git rebase --continue' to ".
"continue forward. After resolving the rebase, run 'arc land' ".
"again.");
}
// Now that we've rebased, the merge-base of origin/master and HEAD may
// be different. Reparse the relative commit.
$repository_api->parseRelativeLocalCommit(array($remote.'/'.$onto));
}
$revision_id = $this->getArgument('revision');
if ($revision_id) {
$revision_id = $this->normalizeRevisionID($revision_id);
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => array($revision_id),
));
if (!$revisions) {
throw new ArcanistUsageException("No such revision 'D{$revision_id}'!");
}
} else {
$revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
array(
'authors' => array($this->getUserPHID()),
));
}
if (!count($revisions)) {
throw new ArcanistUsageException(
"arc can not identify which revision exists on branch '{$branch}'. ".
"Update the revision with recent changes to synchronize the branch ".
"name and hashes, or use 'arc amend' to amend the commit message at ".
"HEAD, or use '--revision <id>' to select a revision explicitly.");
} else if (count($revisions) > 1) {
$message =
"There are multiple revisions on feature branch '{$branch}' which are ".
"not present on '{$onto}':\n\n".
$this->renderRevisionList($revisions)."\n".
"Separate these revisions onto different branches, or use ".
"'--revision <id>' to select one.";
throw new ArcanistUsageException($message);
}
$revision = head($revisions);
$rev_id = $revision['id'];
$rev_title = $revision['title'];
if ($revision['status'] != ArcanistDifferentialRevisionStatus::ACCEPTED) {
$ok = phutil_console_confirm(
"Revision 'D{$rev_id}: {$rev_title}' has not been accepted. Continue ".
"anyway?");
if (!$ok) {
throw new ArcanistUserAbortException();
}
}
echo "Landing revision 'D{$rev_id}: {$rev_title}'...\n";
$message = $this->getConduit()->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $revision['id'],
));
$repository_api->execxLocal('checkout %s', $onto);
if (!$use_squash) {
// In immutable histories, do a --no-ff merge to force a merge commit with
// the right message.
chdir($repository_api->getPath());
$err = phutil_passthru(
'git merge --no-ff --no-commit %s',
$branch);
if ($err) {
throw new ArcanistUsageException(
"'git merge' failed. Your working copy has been left in a partially ".
"merged state. You can: abort with 'git merge --abort'; or follow ".
"the instructions to complete the merge.");
}
} else {
// In mutable histories, do a --squash merge.
$repository_api->execxLocal(
'merge --squash --ff-only %s',
$branch);
}
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$repository_api->execxLocal(
'commit -F %s',
$tmp_file);
if ($this->getArgument('hold')) {
echo phutil_console_format(
"Holding change in **%s**: it has NOT been pushed yet.\n",
$onto);
} else {
echo "Pushing change...\n\n";
chdir($repository_api->getPath());
$err = phutil_passthru(
'git push %s %s',
$remote,
$onto);
if ($err) {
throw new ArcanistUsageException("'git push' failed.");
}
$mark_workflow = $this->buildChildWorkflow(
'close-revision',
array(
'--finalize',
'--quiet',
$revision['id'],
));
$mark_workflow->run();
echo "\n";
}
if (!$this->getArgument('keep-branch')) {
list($ref) = $repository_api->execxLocal(
'rev-parse --verify %s',
$branch);
$ref = trim($ref);
$recovery_command = csprintf(
'git checkout -b %s %s',
$branch,
$ref);
echo "Cleaning up feature branch...\n";
echo "(Use `{$recovery_command}` if you want it back.)\n";
$repository_api->execxLocal(
'branch -D %s',
$branch);
if ($this->getArgument('delete-remote')) {
list($err, $ref) = $repository_api->execManualLocal(
'rev-parse --verify %s/%s',
$remote,
$branch);
if ($err) {
echo "No remote feature branch to clean up.\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 "Cleaning up remote feature branch...\n";
$repository_api->execxLocal(
'push %s :%s',
$remote,
$branch);
}
}
}
// If we were on some branch A and the user ran "arc land B", switch back
// to A.
if (($old_branch != $branch) && ($old_branch != $onto)) {
$repository_api->execxLocal(
'checkout %s',
$old_branch);
echo phutil_console_format(
"Switched back to branch **%s**.\n",
$old_branch);
}
echo "Done.\n";
return 0;
}
protected function getSupportedRevisionControlSystems() {
return array('git');
}
}
diff --git a/src/workflow/ArcanistLiberateWorkflow.php b/src/workflow/ArcanistLiberateWorkflow.php
index 6b2c8e89..10d9ed32 100644
--- a/src/workflow/ArcanistLiberateWorkflow.php
+++ b/src/workflow/ArcanistLiberateWorkflow.php
@@ -1,246 +1,230 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Create and update libphutil libraries.
*
* This workflow is unusual and involves reexecuting 'arc liberate' as a
* subprocess with "--remap" and "--verify". This is because there is no way
* to unload or reload a library, so every process is stuck with the library
* definition it had when it first loaded. This is normally fine, but
* problematic in this case because 'arc liberate' modifies library definitions.
*
* @group workflow
*/
final class ArcanistLiberateWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'liberate';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**liberate** [__path__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: libphutil
Create or update a libphutil library, generating required metadata
files like \__init__.php.
EOTEXT
);
}
public function getArguments() {
return array(
'all' => array(
'help' =>
"Drop the module cache before liberating. This will completely ".
"reanalyze the entire library. Thorough, but slow!",
),
'force-update' => array(
'help' =>
"Force the library map to be updated, even in the presence of ".
"lint errors.",
),
'remap' => array(
'hide' => true,
'help' =>
"Internal. Run the remap step of liberation. You do not need to ".
"run this unless you are debugging the workflow.",
),
'verify' => array(
'hide' => true,
'help' =>
"Internal. Run the verify step of liberation. You do not need to ".
"run this unless you are debugging the workflow.",
),
'upgrade' => array(
'hide' => true,
'help' => "Experimental. Upgrade library to v2.",
),
'*' => 'argv',
);
}
public function run() {
$argv = $this->getArgument('argv');
if (count($argv) > 1) {
throw new ArcanistUsageException(
"Provide only one path to 'arc liberate'. The path should be a ".
"directory where you want to create or update a libphutil library.");
} else if (count($argv) == 0) {
$path = getcwd();
} else {
$path = reset($argv);
}
$is_remap = $this->getArgument('remap');
$is_verify = $this->getArgument('verify');
$path = Filesystem::resolvePath($path);
if (Filesystem::pathExists($path) && is_dir($path)) {
$init = id(new FileFinder($path))
->withPath('*/__phutil_library_init__.php')
->find();
} else {
$init = null;
}
if ($init) {
if (count($init) > 1) {
throw new ArcanistUsageException(
"Specified directory contains more than one libphutil library. Use ".
"a more specific path.");
}
$path = Filesystem::resolvePath(dirname(reset($init)), $path);
} else {
$found = false;
foreach (Filesystem::walkToRoot($path) as $dir) {
if (Filesystem::pathExists($dir.'/__phutil_library_init__.php')) {
$path = $dir;
$found = true;
break;
}
}
if (!$found) {
echo "No library currently exists at that path...\n";
$this->liberateCreateDirectory($path);
$this->liberateCreateLibrary($path);
return;
}
}
$version = $this->getLibraryFormatVersion($path);
switch ($version) {
case 1:
if ($this->getArgument('upgrade')) {
return $this->upgradeLibrary($path);
}
throw new ArcanistUsageException(
"This library is using libphutil v1, which is no longer supported. ".
"Run 'arc liberate --upgrade' to upgrade to v2.");
case 2:
if ($this->getArgument('upgrade')) {
throw new ArcanistUsageException(
"Can't upgrade a v2 library!");
}
return $this->liberateVersion2($path);
default:
throw new ArcanistUsageException(
"Unknown library version '{$version}'!");
}
}
private function getLibraryFormatVersion($path) {
$map_file = $path.'/__phutil_library_map__.php';
if (!Filesystem::pathExists($map_file)) {
// Default to library v1.
return 1;
}
$map = Filesystem::readFile($map_file);
$matches = null;
if (preg_match('/@phutil-library-version (\d+)/', $map, $matches)) {
return (int)$matches[1];
}
return 1;
}
private function liberateVersion2($path) {
$bin = $this->getScriptPath('scripts/phutil_rebuild_map.php');
return phutil_passthru(
'%s %C %s',
$bin,
$this->getArgument('all') ? '--drop-cache' : '',
$path);
}
private function upgradeLibrary($path) {
$inits = id(new FileFinder($path))
->withPath('*/__init__.php')
->withType('f')
->find();
echo "Removing __init__.php files...\n";
foreach ($inits as $init) {
Filesystem::remove($path.'/'.$init);
}
echo "Upgrading library to v2...\n";
$this->liberateVersion2($path);
}
private function liberateCreateDirectory($path) {
if (Filesystem::pathExists($path)) {
if (!is_dir($path)) {
throw new ArcanistUsageException(
"Provide a directory to create or update a libphutil library in.");
}
return;
}
echo "The directory '{$path}' does not exist.";
if (!phutil_console_confirm('Do you want to create it?')) {
throw new ArcanistUsageException("Cancelled.");
}
execx('mkdir -p %s', $path);
}
private function liberateCreateLibrary($path) {
$init_path = $path.'/__phutil_library_init__.php';
if (Filesystem::pathExists($init_path)) {
return;
}
echo "Creating new libphutil library in '{$path}'.\n";
echo "Choose a name for the new library.\n";
do {
$name = phutil_console_prompt('What do you want to name this library?');
if (preg_match('/^[a-z-]+$/', $name)) {
break;
} else {
echo "Library name should contain only lowercase letters and ".
"hyphens.\n";
}
} while (true);
$template =
"<?php\n\n".
"phutil_register_library('{$name}', __FILE__);\n";
echo "Writing '__phutil_library_init__.php' to '{$path}'...\n";
Filesystem::writeFile($init_path, $template);
$this->liberateVersion2($path);
}
private function getScriptPath($script) {
$root = dirname(phutil_get_library_root('arcanist'));
return $root.'/'.$script;
}
}
diff --git a/src/workflow/ArcanistLintWorkflow.php b/src/workflow/ArcanistLintWorkflow.php
index fa7769bb..592c6c4e 100644
--- a/src/workflow/ArcanistLintWorkflow.php
+++ b/src/workflow/ArcanistLintWorkflow.php
@@ -1,396 +1,380 @@
<?php
-/*
- * Copyright 2012 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;
const RESULT_POSTPONED = 4;
private $unresolvedMessages;
private $shouldAmendChanges = false;
private $shouldAmendWithoutPrompt = false;
private $shouldAmendAutofixesWithoutPrompt = false;
private $engine;
private $postponedLinters;
public function getWorkflowName() {
return 'lint';
}
public function setShouldAmendChanges($should_amend) {
$this->shouldAmendChanges = $should_amend;
return $this;
}
public function setShouldAmendWithoutPrompt($should_amend) {
$this->shouldAmendWithoutPrompt = $should_amend;
return $this;
}
public function setShouldAmendAutofixesWithoutPrompt($should_amend) {
$this->shouldAmendAutofixesWithoutPrompt = $should_amend;
return $this;
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**lint** [__options__] [__paths__]
**lint** [__options__] --rev [__rev__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
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."
),
'rev' => array(
'param' => 'revision',
'help' => "Lint changes since a specific revision.",
'supports' => array(
'git',
'hg',
),
'nosupport' => array(
'svn' => "Lint does not currently support --rev in SVN.",
),
),
'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. ".
"With 'compiler', show lint warnings in suitable for your editor."
),
'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,
),
),
'amend-all' => array(
'help' =>
'When linting git repositories, amend HEAD with all patches '.
'suggested by lint without prompting.',
),
'amend-autofixes' => array(
'help' =>
'When linting git repositories, amend HEAD with autofix '.
'patches suggested by lint without prompting.',
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function run() {
$working_copy = $this->getWorkingCopy();
$engine = $this->getArgument('engine');
if (!$engine) {
$engine = $working_copy->getConfigFromAnySource('lint.engine');
if (!$engine) {
throw new ArcanistNoEngineException(
"No lint engine configured for this project. Edit .arcconfig to ".
"specify a lint engine.");
}
}
$rev = $this->getArgument('rev');
$paths = $this->getArgument('paths');
if ($rev && $paths) {
throw new ArcanistUsageException("Specify either --rev or paths.");
}
$should_lint_all = $this->getArgument('lintall');
if ($paths) {
// NOTE: When the user specifies paths, we imply --lintall and show all
// warnings for the paths in question. This is easier to deal with for
// us and less confusing for users.
$should_lint_all = true;
}
$paths = $this->selectPathsForWorkflow($paths, $rev);
if (!class_exists($engine) ||
!is_subclass_of($engine, 'ArcanistLintEngine')) {
throw new ArcanistUsageException(
"Configured lint engine '{$engine}' is not a subclass of ".
"'ArcanistLintEngine'.");
}
$engine = newv($engine, array());
$this->engine = $engine;
$engine->setWorkingCopy($working_copy);
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ADVICE);
// Propagate information about which lines changed to the lint engine.
// This is used so that the lint engine can drop warning messages
// concerning lines that weren't in the change.
$engine->setPaths($paths);
if (!$should_lint_all) {
foreach ($paths as $path) {
// Note that getChangedLines() returns null to indicate that a file
// is binary or a directory (i.e., changed lines are not relevant).
$engine->setPathChangedLines(
$path,
$this->getChangedLines($path, 'new'));
}
}
// Enable possible async linting only for 'arc diff' not 'arc lint'
if ($this->getParentWorkflow()) {
$engine->setEnableAsyncLint(true);
} else {
$engine->setEnableAsyncLint(false);
}
$failed = null;
try {
$engine->run();
} catch (Exception $ex) {
$failed = $ex;
}
$results = $engine->getResults();
// It'd be nice to just return a single result from the run method above
// which contains both the lint messages and the postponed linters.
// However, to maintain compatibility with existing lint subclasses, use
// a separate method call to grab the postponed linters.
$this->postponedLinters = $engine->getPostponedLinters();
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;
}
if ($this->getArgument('amend-all')) {
$this->shouldAmendChanges = true;
$this->shouldAmendWithoutPrompt = true;
}
if ($this->getArgument('amend-autofixes')) {
$prompt_autofix_patches = false;
$this->shouldAmendChanges = true;
$this->shouldAmendAutofixesWithoutPrompt = true;
} else {
$prompt_autofix_patches = true;
}
$wrote_to_disk = false;
switch ($this->getArgument('output')) {
case 'json':
$renderer = new ArcanistLintJSONRenderer();
$prompt_patches = false;
$apply_patches = $this->getArgument('apply-patches');
break;
case 'summary':
$renderer = new ArcanistLintSummaryRenderer();
break;
case 'compiler':
$renderer = new ArcanistLintLikeCompilerRenderer();
$prompt_patches = false;
$apply_patches = $this->getArgument('apply-patches');
break;
default:
$renderer = new ArcanistLintConsoleRenderer();
$renderer->setShowAutofixPatches($prompt_autofix_patches);
break;
}
$all_autofix = true;
$console = PhutilConsole::getConsole();
foreach ($results as $result) {
$result_all_autofix = $result->isAllAutofix();
if (!$result->getMessages() && !$result_all_autofix) {
continue;
}
if (!$result_all_autofix) {
$all_autofix = false;
}
$lint_result = $renderer->renderLintResult($result);
if ($lint_result) {
$console->writeOut('%s', $lint_result);
}
if ($apply_patches && $result->isPatchable()) {
$patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
if ($prompt_patches &&
!($result_all_autofix && !$prompt_autofix_patches)) {
$old_file = $result->getFilePathOnDisk();
if (!Filesystem::pathExists($old_file)) {
$old_file = '/dev/null';
}
$new_file = new TempFile();
$new = $patcher->getModifiedFileContent();
Filesystem::writeFile($new_file, $new);
// TODO: Improve the behavior here, make it more like
// difference_render().
list(, $stdout, $stderr) =
exec_manual("diff -u %s %s", $old_file, $new_file);
$console->writeOut('%s', $stdout);
$console->writeErr('%s', $stderr);
$prompt = phutil_console_format(
"Apply this patch to __%s__?",
$result->getPath());
if (!$console->confirm($prompt, $default_no = false)) {
continue;
}
}
$patcher->writePatchToDisk();
$wrote_to_disk = true;
}
}
if ($failed) {
throw $failed;
}
$repository_api = $this->getRepositoryAPI();
if ($wrote_to_disk &&
($repository_api instanceof ArcanistGitAPI) &&
$this->shouldAmendChanges) {
if ($this->shouldAmendWithoutPrompt ||
($this->shouldAmendAutofixesWithoutPrompt && $all_autofix)) {
$console->writeOut(
"<bg:yellow>** LINT NOTICE **</bg> Automatically amending HEAD ".
"with lint patches.\n");
$amend = true;
} else {
$amend = $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 if (!empty($this->postponedLinters)) {
$result_code = self::RESULT_POSTPONED;
} else {
$result_code = self::RESULT_OKAY;
}
if (!$this->getParentWorkflow()) {
if ($result_code == self::RESULT_OKAY) {
$console->writeOut('%s', $renderer->renderOkayResult());
}
}
return $result_code;
}
public function getUnresolvedMessages() {
return $this->unresolvedMessages;
}
public function getPostponedLinters() {
return $this->postponedLinters;
}
}
diff --git a/src/workflow/ArcanistListWorkflow.php b/src/workflow/ArcanistListWorkflow.php
index c3486edc..5a74b959 100644
--- a/src/workflow/ArcanistListWorkflow.php
+++ b/src/workflow/ArcanistListWorkflow.php
@@ -1,109 +1,93 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Lists open revisions in Differential.
*
* @group workflow
*/
final class ArcanistListWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'list';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**list**
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
List your open Differential revisions.
EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function run() {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'authors' => array($this->getUserPHID()),
'status' => 'status-open',
));
if (!$revisions) {
echo "You have no open Differential revisions.\n";
return 0;
}
$repository_api = $this->getRepositoryAPI();
$info = array();
$status_len = 0;
foreach ($revisions as $key => $revision) {
$revision_path = Filesystem::resolvePath($revision['sourcePath']);
$current_path = Filesystem::resolvePath($repository_api->getPath());
if ($revision_path == $current_path) {
$info[$key]['here'] = 1;
} else {
$info[$key]['here'] = 0;
}
$info[$key]['sort'] = sprintf(
'%d%04d%08d',
$info[$key]['here'],
$revision['status'],
$revision['id']);
$info[$key]['statusName'] = $revision['statusName'];
$status_len = max(
$status_len,
strlen($info[$key]['statusName']));
}
$info = isort($info, 'sort');
foreach ($info as $key => $spec) {
$revision = $revisions[$key];
printf(
"%s %-".($status_len + 4)."s D%d: %s\n",
$spec['here']
? phutil_console_format('**%s**', '*')
: ' ',
$spec['statusName'],
$revision['id'],
$revision['title']);
}
return 0;
}
}
diff --git a/src/workflow/ArcanistMarkCommittedWorkflow.php b/src/workflow/ArcanistMarkCommittedWorkflow.php
index 99de2ebf..33791410 100644
--- a/src/workflow/ArcanistMarkCommittedWorkflow.php
+++ b/src/workflow/ArcanistMarkCommittedWorkflow.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* @group workflow
*/
final class ArcanistMarkCommittedWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'mark-committed';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**mark-committed** (DEPRECATED)
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Deprecated. Moved to "close-revision".
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'deprecated',
);
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function run() {
throw new ArcanistUsageException(
"'arc mark-committed' is now 'arc close-revision' (because ".
"'mark-committed' only really made sense under SVN).");
}
}
diff --git a/src/workflow/ArcanistPasteWorkflow.php b/src/workflow/ArcanistPasteWorkflow.php
index 1c684c4a..0200ce87 100644
--- a/src/workflow/ArcanistPasteWorkflow.php
+++ b/src/workflow/ArcanistPasteWorkflow.php
@@ -1,169 +1,153 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Upload a chunk of text to the Paste application, or download one.
*
* @group workflow
*/
final class ArcanistPasteWorkflow extends ArcanistBaseWorkflow {
private $id;
private $language;
private $title;
private $json;
public function getWorkflowName() {
return 'paste';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**paste** [--title __title__] [--lang __language__] [--json]
**paste** __id__ [--json]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: text
Share and grab text using the Paste application. To create a paste,
use stdin to provide the text:
$ cat list_of_ducks.txt | arc paste
To retrieve a paste, specify the paste ID:
$ arc paste P123
EOTEXT
);
}
public function getArguments() {
return array(
'title' => array(
'param' => 'title',
'help' => 'Title for the paste.',
),
'lang' => array(
'param' => 'language',
'help' => 'Language for syntax highlighting.',
),
'json' => array(
'help' => 'Output in JSON format.',
),
'*' => 'argv',
);
}
public function requiresAuthentication() {
return true;
}
protected function didParseArguments() {
$this->language = $this->getArgument('lang');
$this->title = $this->getArgument('title');
$this->json = $this->getArgument('json');
$argv = $this->getArgument('argv');
if (count($argv) > 1) {
throw new ArcanistUsageException("Specify only one paste to retrieve.");
} else if (count($argv) == 1) {
$id = $argv[0];
if (!preg_match('/^P?\d+/', $id)) {
throw new ArcanistUsageException("Specify a paste ID, like P123.");
}
$this->id = (int)ltrim($id, 'P');
if ($this->language || $this->title) {
throw new ArcanistUsageException(
"Use options --lang and --title only when creating pastes.");
}
}
}
private function getTitle() {
return $this->title;
}
private function getLanguage() {
return $this->language;
}
private function getJSON() {
return $this->json;
}
public function run() {
if ($this->id) {
return $this->getPaste();
} else {
return $this->createPaste();
}
}
private function getPaste() {
$conduit = $this->getConduit();
$info = $conduit->callMethodSynchronous(
'paste.query',
array(
'ids' => array($this->id),
));
$info = head($info);
if ($this->getJSON()) {
echo json_encode($info)."\n";
} else {
echo $info['content'];
if (!preg_match('/\\n$/', $info['content'])) {
// If there's no newline, add one, since it looks stupid otherwise. If
// you want byte-for-byte equivalence you can use --json.
echo "\n";
}
}
return 0;
}
private function createPaste() {
$conduit = $this->getConduit();
// Avoid confusion when people type "arc paste" with nothing else.
$this->writeStatusMessage("Reading paste from stdin...\n");
$info = $conduit->callMethodSynchronous(
'paste.create',
array(
'content' => file_get_contents('php://stdin'),
'title' => $this->getTitle(),
'language' => $this->getLanguage(),
));
if ($this->getArgument('json')) {
echo json_encode($info)."\n";
} else {
echo $info['objectName'].': '.$info['uri']."\n";
}
return 0;
}
}
diff --git a/src/workflow/ArcanistPatchWorkflow.php b/src/workflow/ArcanistPatchWorkflow.php
index c0896250..0f1b7964 100644
--- a/src/workflow/ArcanistPatchWorkflow.php
+++ b/src/workflow/ArcanistPatchWorkflow.php
@@ -1,1001 +1,985 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Applies changes from Differential or a file to the working copy.
*
* @group workflow
*/
final class ArcanistPatchWorkflow extends ArcanistBaseWorkflow {
const SOURCE_BUNDLE = 'bundle';
const SOURCE_PATCH = 'patch';
const SOURCE_REVISION = 'revision';
const SOURCE_DIFF = 'diff';
private $source;
private $sourceParam;
public function getWorkflowName() {
return 'patch';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**patch** __D12345__
**patch** __--revision__ __revision_id__
**patch** __--diff__ __diff_id__
**patch** __--patch__ __file__
**patch** __--arcbundle__ __bundlefile__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
Apply the changes in a Differential revision, patchfile, or arc
bundle to the working copy.
EOTEXT
);
}
public function getArguments() {
return array(
'revision' => array(
'param' => 'revision_id',
'paramtype' => 'complete',
'help' =>
"Apply changes from a Differential revision, using the most recent ".
"diff that has been attached to it. You can run 'arc patch D12345' ".
"as a shorthand.",
),
'diff' => array(
'param' => 'diff_id',
'help' =>
"Apply changes from a Differential diff. Normally you want to use ".
"--revision to get the most recent changes, but you can ".
"specifically apply an out-of-date diff or a diff which was never ".
"attached to a revision by using this flag.",
),
'arcbundle' => array(
'param' => 'bundlefile',
'paramtype' => 'file',
'help' =>
"Apply changes from an arc bundle generated with 'arc export'.",
),
'patch' => array(
'param' => 'patchfile',
'paramtype' => 'file',
'help' =>
"Apply changes from a git patchfile or unified patchfile.",
),
'encoding' => array(
'param' => 'encoding',
'help' =>
"Attempt to convert non UTF-8 patch into specified encoding.",
),
'update' => array(
'supports' => array(
'git', 'svn', 'hg'
),
'help' =>
"Update the local working copy before applying the patch.",
'conflicts' => array(
'nobranch' => true,
'bookmark' => true,
),
),
'nocommit' => array(
'supports' => array(
'git', 'hg'
),
'help' =>
"Normally under git/hg, if the patch is successful, the changes ".
"are committed to the working copy. This flag prevents the commit.",
),
'nobranch' => array(
'supports' => array(
'git'
),
'help' =>
"Normally under git, a new branch is created and then the patch ".
"is applied and committed in the new branch. This flag ".
"cherry-picks the resultant commit onto the original branch and ".
"deletes the temporary branch.",
'conflicts' => array(
'update' => true,
),
),
'bookmark' => array(
'supports' => array(
'hg'
),
'help' =>
"Normally under hg, a new bookmark is not created and the patch ".
"is applied and committed in the current bookmark. With this flag, ".
"a new bookmark is created and the patch is applied and committed ".
"in the new bookmark.",
'conflicts' => array(
'update' => true,
),
),
'force' => array(
'help' =>
"Do not run any sanity checks.",
),
'*' => 'name',
);
}
protected function didParseArguments() {
$source = null;
$requested = 0;
if ($this->getArgument('revision')) {
$source = self::SOURCE_REVISION;
$requested++;
}
if ($this->getArgument('diff')) {
$source = self::SOURCE_DIFF;
$requested++;
}
if ($this->getArgument('arcbundle')) {
$source = self::SOURCE_BUNDLE;
$requested++;
}
if ($this->getArgument('patch')) {
$source = self::SOURCE_PATCH;
$requested++;
}
$use_revision_id = null;
if ($this->getArgument('name')) {
$namev = $this->getArgument('name');
if (count($namev) > 1) {
throw new ArcanistUsageException("Specify at most one revision name.");
}
$source = self::SOURCE_REVISION;
$requested++;
$use_revision_id = $this->normalizeRevisionID(head($namev));
}
if ($requested === 0) {
throw new ArcanistUsageException(
"Specify one of 'D12345', '--revision <revision_id>' (to select the ".
"current changes attached to a Differential revision), ".
"'--diff <diff_id>' (to select a specific, out-of-date diff or a ".
"diff which is not attached to a revision), '--arcbundle <file>' ".
"or '--patch <file>' to choose a patch source.");
} else if ($requested > 1) {
throw new ArcanistUsageException(
"Options 'D12345', '--revision', '--diff', '--arcbundle' and ".
"'--patch' are not compatible. Choose exactly one patch source.");
}
$this->source = $source;
$this->sourceParam = nonempty(
$use_revision_id,
$this->getArgument($source));
}
public function requiresConduit() {
return ($this->getSource() != self::SOURCE_PATCH);
}
public function requiresRepositoryAPI() {
return true;
}
public function requiresWorkingCopy() {
return true;
}
private function getSource() {
return $this->source;
}
private function getSourceParam() {
return $this->sourceParam;
}
private function shouldCommit() {
$no_commit = $this->getArgument('nocommit', false);
if ($no_commit) {
return false;
}
return true;
}
private function canBranch() {
// git only for now
$repository_api = $this->getRepositoryAPI();
if (!($repository_api instanceof ArcanistGitAPI)) {
return false;
}
return true;
}
private function shouldBranch() {
$no_branch = $this->getArgument('nobranch', false);
if ($no_branch) {
return false;
}
return true;
}
private function shouldBookmark() {
// specific to hg
$repository_api = $this->getRepositoryAPI();
if (!($repository_api instanceof ArcanistMercurialAPI)) {
return false;
}
$bookmark = $this->getArgument('bookmark', false);
if ($bookmark) {
return true;
}
return false;
}
private function getBranchName(ArcanistBundle $bundle) {
$branch_name = null;
$repository_api = $this->getRepositoryAPI();
$revision_id = $bundle->getRevisionID();
$base_name = "arcpatch";
if ($revision_id) {
$base_name .= "-D{$revision_id}";
}
$suffixes = array(null, '-1', '-2', '-3');
foreach ($suffixes as $suffix) {
$proposed_name = $base_name.$suffix;
list($err) = $repository_api->execManualLocal(
'rev-parse --verify %s',
$proposed_name);
// no error means git rev-parse found a branch
if (!$err) {
echo phutil_console_format(
"Branch name {$proposed_name} already exists; trying a new name.\n"
);
continue;
} else {
$branch_name = $proposed_name;
break;
}
}
if (!$branch_name) {
throw new Exception(
"Arc was unable to automagically make a name for this patch. ".
"Please clean up your working copy and try again."
);
}
return $branch_name;
}
private function getBookmarkName(ArcanistBundle $bundle) {
$bookmark_name = null;
$repository_api = $this->getRepositoryAPI();
$revision_id = $bundle->getRevisionID();
$base_name = "arcpatch";
if ($revision_id) {
$base_name .= "-D{$revision_id}";
}
$suffixes = array(null, '-1', '-2', '-3');
foreach ($suffixes as $suffix) {
$proposed_name = $base_name.$suffix;
list($err) = $repository_api->execManualLocal(
'log -r %s',
$proposed_name);
// no error means hg log found a bookmark
if (!$err) {
echo phutil_console_format(
"Bookmark name {$proposed_name} already exists; trying a new name.\n"
);
continue;
} else {
$bookmark_name = $proposed_name;
break;
}
}
if (!$bookmark_name) {
throw new Exception(
"Arc was unable to automagically make a name for this patch. ".
"Please clean up your working copy and try again."
);
}
return $bookmark_name;
}
private function hasBaseRevision(ArcanistBundle $bundle) {
$base_revision = $bundle->getBaseRevision();
$repository_api = $this->getRepositoryAPI();
// verify the base revision is valid
// in a working copy that uses the git-svn bridge, the base revision might
// be a svn uri instead of a git ref
// NOTE: Use 'cat-file', not 'rev-parse --verify', because 'rev-parse'
// always "verifies" any properly-formatted commit even if it does not
// exist.
list($err) = $repository_api->execManualLocal(
'cat-file -t %s',
$base_revision);
return !$err;
}
private function createBranch(ArcanistBundle $bundle, $has_base_revision) {
$branch_name = $this->getBranchName($bundle);
$base_revision = $bundle->getBaseRevision();
$repository_api = $this->getRepositoryAPI();
if ($base_revision && $has_base_revision) {
$repository_api->execxLocal(
'checkout -b %s %s',
$branch_name,
$base_revision);
} else {
$repository_api->execxLocal(
'checkout -b %s',
$branch_name);
}
echo phutil_console_format(
"Created and checked out branch %s.\n",
$branch_name);
return $branch_name;
}
private function createBookmark(ArcanistBundle $bundle) {
$bookmark_name = $this->getBookmarkName($bundle);
$repository_api = $this->getRepositoryAPI();
$repository_api->execxLocal(
'bookmark %s',
$bookmark_name);
echo phutil_console_format(
"Created and applied bookmark %s.\n",
$bookmark_name);
}
private function shouldUpdateWorkingCopy() {
return $this->getArgument('update', false);
}
private function updateWorkingCopy() {
echo "Updating working copy...\n";
$this->getRepositoryAPI()->updateWorkingCopy();
echo "Done.\n";
}
public function run() {
$source = $this->getSource();
$param = $this->getSourceParam();
try {
switch ($source) {
case self::SOURCE_PATCH:
if ($param == '-') {
$patch = @file_get_contents('php://stdin');
if (!strlen($patch)) {
throw new ArcanistUsageException(
"Failed to read patch from stdin!");
}
} else {
$patch = Filesystem::readFile($param);
}
$bundle = ArcanistBundle::newFromDiff($patch);
break;
case self::SOURCE_BUNDLE:
$path = $this->getArgument('arcbundle');
$bundle = ArcanistBundle::newFromArcBundle($path);
break;
case self::SOURCE_REVISION:
$bundle = $this->loadRevisionBundleFromConduit(
$this->getConduit(),
$param);
break;
case self::SOURCE_DIFF:
$bundle = $this->loadDiffBundleFromConduit(
$this->getConduit(),
$param);
break;
}
} catch (ConduitClientException $ex) {
if ($ex->getErrorCode() == 'ERR-INVALID-SESSION') {
// Phabricator is not configured to allow anonymous access to
// Differential.
$this->authenticateConduit();
return $this->run();
} else {
throw $ex;
}
}
$try_encoding = nonempty($this->getArgument('encoding'), null);
if (!$try_encoding) {
if ($this->requiresConduit()) {
try {
$try_encoding = $this->getRepositoryEncoding();
} catch (ConduitClientException $e) {
$try_encoding = null;
}
}
}
if ($try_encoding) {
$bundle->setEncoding($try_encoding);
}
$force = $this->getArgument('force', false);
if ($force) {
// force means don't do any sanity checks about the patch
} else {
$this->sanityCheck($bundle);
}
// we should update the working copy before we do ANYTHING else
if ($this->shouldUpdateWorkingCopy()) {
$this->updateWorkingCopy();
}
$repository_api = $this->getRepositoryAPI();
$has_base_revision = $this->hasBaseRevision($bundle);
if ($this->shouldCommit() &&
$this->canBranch() &&
($this->shouldBranch() || $has_base_revision)) {
$original_branch = $repository_api->getBranchName();
// If we weren't on a branch, then record the ref we'll return to
// instead.
if ($original_branch === null) {
$original_branch = $repository_api->getCanonicalRevisionName('HEAD');
}
$new_branch = $this->createBranch($bundle, $has_base_revision);
}
if ($this->shouldBookmark()) {
$this->createBookmark($bundle);
}
if ($repository_api instanceof ArcanistSubversionAPI) {
$patch_err = 0;
$copies = array();
$deletes = array();
$patches = array();
$propset = array();
$adds = array();
$symlinks = array();
$changes = $bundle->getChanges();
foreach ($changes as $change) {
$type = $change->getType();
$should_patch = true;
$filetype = $change->getFileType();
switch ($filetype) {
case ArcanistDiffChangeType::FILE_SYMLINK:
$should_patch = false;
$symlinks[] = $change;
break;
}
switch ($type) {
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
case ArcanistDiffChangeType::TYPE_MULTICOPY:
case ArcanistDiffChangeType::TYPE_DELETE:
$path = $change->getCurrentPath();
$fpath = $repository_api->getPath($path);
if (!@file_exists($fpath)) {
$ok = phutil_console_confirm(
"Patch deletes file '{$path}', but the file does not exist in ".
"the working copy. Continue anyway?");
if (!$ok) {
throw new ArcanistUserAbortException();
}
} else {
$deletes[] = $change->getCurrentPath();
}
$should_patch = false;
break;
case ArcanistDiffChangeType::TYPE_COPY_HERE:
case ArcanistDiffChangeType::TYPE_MOVE_HERE:
$path = $change->getOldPath();
$fpath = $repository_api->getPath($path);
if (!@file_exists($fpath)) {
$cpath = $change->getCurrentPath();
if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) {
$verbs = 'copies';
} else {
$verbs = 'moves';
}
$ok = phutil_console_confirm(
"Patch {$verbs} '{$path}' to '{$cpath}', but source path ".
"does not exist in the working copy. Continue anyway?");
if (!$ok) {
throw new ArcanistUserAbortException();
}
} else {
$copies[] = array(
$change->getOldPath(),
$change->getCurrentPath());
}
break;
case ArcanistDiffChangeType::TYPE_ADD:
$adds[] = $change->getCurrentPath();
break;
}
if ($should_patch) {
$cbundle = ArcanistBundle::newFromChanges(array($change));
$patches[$change->getCurrentPath()] = $cbundle->toUnifiedDiff();
$prop_old = $change->getOldProperties();
$prop_new = $change->getNewProperties();
$props = $prop_old + $prop_new;
foreach ($props as $key => $ignored) {
if (idx($prop_old, $key) !== idx($prop_new, $key)) {
$propset[$change->getCurrentPath()][$key] = idx($prop_new, $key);
}
}
}
}
// Before we start doing anything, create all the directories we're going
// to add files to if they don't already exist.
foreach ($copies as $copy) {
list($src, $dst) = $copy;
$this->createParentDirectoryOf($dst);
}
foreach ($patches as $path => $patch) {
$this->createParentDirectoryOf($path);
}
foreach ($adds as $add) {
$this->createParentDirectoryOf($add);
}
// TODO: The SVN patch workflow likely does not work on windows because
// of the (cd ...) stuff.
foreach ($copies as $copy) {
list($src, $dst) = $copy;
passthru(
csprintf(
'(cd %s; svn cp %s %s)',
$repository_api->getPath(),
$src,
$dst));
}
foreach ($deletes as $delete) {
passthru(
csprintf(
'(cd %s; svn rm %s)',
$repository_api->getPath(),
$delete));
}
foreach ($symlinks as $symlink) {
$link_target = $symlink->getSymlinkTarget();
$link_path = $symlink->getCurrentPath();
switch ($symlink->getType()) {
case ArcanistDiffChangeType::TYPE_ADD:
case ArcanistDiffChangeType::TYPE_CHANGE:
case ArcanistDiffChangeType::TYPE_MOVE_HERE:
case ArcanistDiffChangeType::TYPE_COPY_HERE:
execx(
'(cd %s && ln -sf %s %s)',
$repository_api->getPath(),
$link_target,
$link_path);
break;
}
}
foreach ($patches as $path => $patch) {
$err = null;
if ($patch) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $patch);
passthru(
csprintf(
'(cd %s; patch -p0 < %s)',
$repository_api->getPath(),
$tmp),
$err);
} else {
passthru(
csprintf(
'(cd %s; touch %s)',
$repository_api->getPath(),
$path),
$err);
}
if ($err) {
$patch_err = max($patch_err, $err);
}
}
foreach ($adds as $add) {
passthru(
csprintf(
'(cd %s; svn add %s)',
$repository_api->getPath(),
$add));
}
foreach ($propset as $path => $changes) {
foreach ($changes as $prop => $value) {
if ($prop == 'unix:filemode') {
// Setting this property also changes the file mode.
$prop = 'svn:executable';
$value = (octdec($value) & 0111 ? 'on' : null);
}
if ($value === null) {
passthru(
csprintf(
'(cd %s; svn propdel %s %s)',
$repository_api->getPath(),
$prop,
$path));
} else {
passthru(
csprintf(
'(cd %s; svn propset %s %s %s)',
$repository_api->getPath(),
$prop,
$value,
$path));
}
}
}
if ($patch_err == 0) {
echo phutil_console_format(
"<bg:green>** OKAY **</bg> Successfully applied patch ".
"to the working copy.\n");
} else {
echo phutil_console_format(
"\n\n<bg:yellow>** WARNING **</bg> Some hunks could not be applied ".
"cleanly by the unix 'patch' utility. Your working copy may be ".
"different from the revision's base, or you may be in the wrong ".
"subdirectory. You can export the raw patch file using ".
"'arc export --unified', and then try to apply it by fiddling with ".
"options to 'patch' (particularly, -p), or manually. The output ".
"above, from 'patch', may be helpful in figuring out what went ".
"wrong.\n");
}
return $patch_err;
} else if ($repository_api instanceof ArcanistGitAPI) {
$future = $repository_api->execFutureLocal(
'apply --index --reject');
$future->write($bundle->toGitPatch());
try {
$future->resolvex();
} catch (CommandException $ex) {
echo phutil_console_format(
"\n<bg:red>** Patch Failed! **</bg>\n");
$stderr = $ex->getStdErr();
if (preg_match('/already exists in working directory/', $stderr)) {
echo phutil_console_wrap(
phutil_console_format(
"\n<bg:yellow>** WARNING **</bg> This patch may have failed ".
"because it attempts to change the case of a filename (for ".
"instance, from 'example.c' to 'Example.c'). Git cannot apply ".
"patches like this on case-insensitive filesystems. You must ".
"apply this patch manually.\n"));
}
throw $ex;
}
if ($this->shouldCommit()) {
$commit_message = $this->getCommitMessage($bundle);
$future = $repository_api->execFutureLocal(
'commit -a -F -');
$future->write($commit_message);
$future->resolvex();
$verb = 'committed';
} else {
$verb = 'applied';
}
if ($this->shouldCommit() && $this->canBranch() &&
!$this->shouldBranch() && $has_base_revision) {
$repository_api->execxLocal('checkout %s', $original_branch);
$ex = null;
try {
$repository_api->execxLocal('cherry-pick %s', $new_branch);
} catch (Exception $ex) {}
$repository_api->execxLocal('branch -D %s', $new_branch);
if ($ex) {
echo phutil_console_format(
"\n<bg:red>** Cherry Pick Failed!**</bg>\n");
throw $ex;
}
}
echo phutil_console_format(
"<bg:green>** OKAY **</bg> Successfully {$verb} patch.\n");
} else if ($repository_api instanceof ArcanistMercurialAPI) {
$future = $repository_api->execFutureLocal(
'import --no-commit -');
$future->write($bundle->toGitPatch());
try {
$future->resolvex();
} catch (CommandException $ex) {
echo phutil_console_format(
"\n<bg:red>** Patch Failed! **</bg>\n");
$stderr = $ex->getStdErr();
if (preg_match('/case-folding collision/', $stderr)) {
echo phutil_console_wrap(
phutil_console_format(
"\n<bg:yellow>** WARNING **</bg> This patch may have failed ".
"because it attempts to change the case of a filename (for ".
"instance, from 'example.c' to 'Example.c'). Mercurial cannot ".
"apply patches like this on case-insensitive filesystems. You ".
"must apply this patch manually.\n"));
}
throw $ex;
}
if ($this->shouldCommit()) {
$commit_message = $this->getCommitMessage($bundle);
$future = $repository_api->execFutureLocal(
'commit -A -l -');
$future->write($commit_message);
$future->resolvex();
$verb = 'committed';
} else {
$verb = 'applied';
}
echo phutil_console_format(
"<bg:green>** OKAY **</bg> Successfully {$verb} patch.\n");
} else {
throw new Exception('Unknown version control system.');
}
return 0;
}
private function getCommitMessage(ArcanistBundle $bundle) {
$revision_id = $bundle->getRevisionID();
$commit_message = null;
$prompt_message = null;
// if we have a revision id the commit message is in differential
// TODO: See T848 for the authenticated stuff.
if ($revision_id && $this->isConduitAuthenticated()) {
$conduit = $this->getConduit();
$commit_message = $conduit->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $revision_id,
));
$prompt_message = " Note arcanist failed to load the commit message ".
"from differential for revision D{$revision_id}.";
}
// no revision id or failed to fetch commit message so get it from the
// user on the command line
if (!$commit_message) {
$template =
"\n\n".
"# Enter a commit message for this patch. If you just want to apply ".
"the patch to the working copy without committing, re-run arc patch ".
"with the --nocommit flag.".
$prompt_message.
"\n";
$commit_message = $this->newInteractiveEditor($template)
->setName('arcanist-patch-commit-message')
->editInteractively();
$commit_message = ArcanistCommentRemover::removeComments($commit_message);
if (!strlen(trim($commit_message))) {
throw new ArcanistUserAbortException();
}
}
return $commit_message;
}
public function getShellCompletions(array $argv) {
// TODO: Pull open diffs from 'arc list'?
return array('ARGUMENT');
}
/**
* Do the best we can to prevent PEBKAC and id10t issues.
*/
private function sanityCheck(ArcanistBundle $bundle) {
$repository_api = $this->getRepositoryAPI();
if ($repository_api->supportsRelativeLocalCommits()) {
$repository_api->setDefaultBaseCommit();
}
// Require clean working copy
$this->requireCleanWorkingCopy();
// Check to see if the bundle's project id matches the working copy
// project id
$bundle_project_id = $bundle->getProjectID();
$working_copy_project_id = $this->getWorkingCopy()->getProjectID();
if (empty($bundle_project_id)) {
// this means $source is SOURCE_PATCH || SOURCE_BUNDLE w/ $version = 0
// they don't come with a project id so just do nothing
} else if ($bundle_project_id != $working_copy_project_id) {
if ($working_copy_project_id) {
$issue =
"This patch is for the '{$bundle_project_id}' project, but the ".
"working copy belongs to the '{$working_copy_project_id}' project.";
} else {
$issue =
"This patch is for the '{$bundle_project_id}' project, but the ".
"working copy does not have an '.arcconfig' file to identify which ".
"project it belongs to.";
}
$ok = phutil_console_confirm(
"{$issue} Still try to apply the patch?",
$default_no = false
);
if (!$ok) {
throw new ArcanistUserAbortException();
}
}
// Check to see if the bundle's base revision matches the working copy
// base revision
if ($repository_api->supportsRelativeLocalCommits()) {
$bundle_base_rev = $bundle->getBaseRevision();
if (empty($bundle_base_rev)) {
// this means $source is SOURCE_PATCH || SOURCE_BUNDLE w/ $version < 2
// they don't have a base rev so just do nothing
$commit_exists = true;
} else {
$commit_exists =
$repository_api->hasLocalCommit($bundle_base_rev);
}
if (!$commit_exists) {
// we have a problem...! lots of work because we need to ask
// differential for revision information for these base revisions
// to improve our error message.
$bundle_base_rev_str = null;
$source_base_rev = $repository_api->getWorkingCopyRevision();
$source_base_rev_str = null;
if ($repository_api instanceof ArcanistGitAPI) {
$hash_type = ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT;
} else if ($repository_api instanceof ArcanistMercurialAPI) {
$hash_type = ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT;
} else {
$hash_type = null;
}
if ($hash_type) {
// 2 round trips because even though we could send off one query
// we wouldn't be able to tell which revisions were for which hash
$hash = array($hash_type, $bundle_base_rev);
$bundle_revision = $this->loadRevisionFromHash($hash);
$hash = array($hash_type, $source_base_rev);
$source_revision = $this->loadRevisionFromHash($hash);
if ($bundle_revision) {
$bundle_base_rev_str = $bundle_base_rev .
' \ D' . $bundle_revision['id'];
}
if ($source_revision) {
$source_base_rev_str = $source_base_rev .
' \ D' . $source_revision['id'];
}
}
$bundle_base_rev_str = nonempty($bundle_base_rev_str,
$bundle_base_rev);
$source_base_rev_str = nonempty($source_base_rev_str,
$source_base_rev);
$ok = phutil_console_confirm(
"This diff is against commit {$bundle_base_rev_str}, but the ".
"commit is nowhere in the working copy. Try to apply it against ".
"the current working copy state? ({$source_base_rev_str})",
$default_no = false
);
if (!$ok) {
throw new ArcanistUserAbortException();
}
}
}
// TODO -- more sanity checks here
}
/**
* Create parent directories one at a time, since we need to "svn add" each
* one. (Technically we could "svn add" just the topmost new directory.)
*/
private function createParentDirectoryOf($path) {
$repository_api = $this->getRepositoryAPI();
$dir = dirname($path);
if (Filesystem::pathExists($dir)) {
return;
} else {
// Make sure the parent directory exists before we make this one.
$this->createParentDirectoryOf($dir);
execx(
'(cd %s && mkdir %s)',
$repository_api->getPath(),
$dir);
passthru(
csprintf(
'(cd %s && svn add %s)',
$repository_api->getPath(),
$dir));
}
}
private function loadRevisionFromHash($hash) {
// TODO -- de-hack this as permissions become more clear with things
// like T848 (add scope to OAuth)
if (!$this->isConduitAuthenticated()) {
return null;
}
$conduit = $this->getConduit();
$revisions = $conduit->callMethodSynchronous(
'differential.query',
array(
'commitHashes' => array($hash),
)
);
// grab the latest closed revision only
$found_revision = null;
$revisions = isort($revisions, 'dateModified');
foreach ($revisions as $revision) {
if ($revision['status'] == ArcanistDifferentialRevisionStatus::CLOSED) {
$found_revision = $revision;
}
}
return $found_revision;
}
}
diff --git a/src/workflow/ArcanistSetConfigWorkflow.php b/src/workflow/ArcanistSetConfigWorkflow.php
index a6829ff8..9f7a0e32 100644
--- a/src/workflow/ArcanistSetConfigWorkflow.php
+++ b/src/workflow/ArcanistSetConfigWorkflow.php
@@ -1,172 +1,156 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Write configuration settings.
*
* @group workflow
*/
final class ArcanistSetConfigWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'set-config';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**set-config** [__options__] -- __name__ __value__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Sets an arc configuration option.
Options are either global (apply to all arc commands you invoke
from the current user) or local (apply only to the current working
copy). By default, global configuration is written. Use __--local__
to write local configuration.
Global values are written to '~/.arcrc' on Linux and Mac OS X, and an
undisclosed location on Windows. Local values are written to an arc
directory under either .git, .hg, or .svn as appropriate.
With __--show__, a description of supported configuration values
is shown.
EOTEXT
);
}
public function getArguments() {
return array(
'show' => array(
'help' => 'Show available configuration values.',
),
'local' => array(
'help' => 'Set a local config value instead of a global one',
),
'*' => 'argv',
);
}
public function requiresRepositoryAPI() {
return $this->getArgument('local');
}
public function run() {
if ($this->getArgument('show')) {
return $this->show();
}
$argv = $this->getArgument('argv');
if (count($argv) != 2) {
throw new ArcanistUsageException(
"Specify a key and a value, or --show.");
}
$is_local = $this->getArgument('local');
if ($is_local) {
$config = $this->readLocalArcConfig();
$which = 'local';
} else {
$config = self::readGlobalArcConfig();
$which = 'global';
}
$key = $argv[0];
$val = $argv[1];
$settings = new ArcanistSettings();
$old = null;
if (array_key_exists($key, $config)) {
$old = $config[$key];
}
if (!strlen($val)) {
unset($config[$key]);
if ($is_local) {
$this->writeLocalArcConfig($config);
} else {
self::writeGlobalArcConfig($config);
}
$old = $settings->formatConfigValueForDisplay($key, $old);
if ($old === null) {
echo "Deleted key '{$key}' from {$which} config.\n";
} else {
echo "Deleted key '{$key}' from {$which} config (was {$old}).\n";
}
} else {
$val = $settings->willWriteValue($key, $val);
$config[$key] = $val;
if ($is_local) {
$this->writeLocalArcConfig($config);
} else {
self::writeGlobalArcConfig($config);
}
$val = $settings->formatConfigValueForDisplay($key, $val);
$old = $settings->formatConfigValueForDisplay($key, $old);
if ($old === null) {
echo "Set key '{$key}' = {$val} in {$which} config.\n";
} else {
echo "Set key '{$key}' = {$val} in {$which} config (was {$old}).\n";
}
}
return 0;
}
private function show() {
$config = self::readGlobalArcConfig();
$settings = new ArcanistSettings();
$keys = $settings->getAllKeys();
sort($keys);
foreach ($keys as $key) {
$type = $settings->getType($key);
$example = $settings->getExample($key);
$help = $settings->getHelp($key);
$value = idx($config, $key);
$value = $settings->formatConfigValueForDisplay($key, $value);
echo phutil_console_format("**__%s__** (%s)\n\n", $key, $type);
if ($example !== null) {
echo phutil_console_format(" Example: %s\n", $example);
}
if (strlen($value)) {
echo phutil_console_format(" Global Setting: %s\n", $value);
}
echo "\n";
echo phutil_console_wrap($help, 4);
echo "\n\n\n";
}
return 0;
}
}
diff --git a/src/workflow/ArcanistShellCompleteWorkflow.php b/src/workflow/ArcanistShellCompleteWorkflow.php
index f86cac65..76b81c88 100644
--- a/src/workflow/ArcanistShellCompleteWorkflow.php
+++ b/src/workflow/ArcanistShellCompleteWorkflow.php
@@ -1,207 +1,191 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Powers shell-completion scripts.
*
* @group workflow
*/
final class ArcanistShellCompleteWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'shell-complete';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**shell-complete** __--current__ __N__ -- [__argv__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: bash, etc.
Implements shell completion. To use shell completion, source the
appropriate script from 'resources/shell/' in your .shellrc.
EOTEXT
);
}
public function getArguments() {
return array(
'current' => array(
'help' => 'Current term in the argument list being completed.',
'param' => 'cursor_position',
),
'*' => 'argv',
);
}
public function shouldShellComplete() {
return false;
}
public function run() {
$pos = $this->getArgument('current');
$argv = $this->getArgument('argv', array());
$argc = count($argv);
if ($pos === null) {
$pos = $argc - 1;
}
// Determine which revision control system the working copy uses, so we
// can filter out commands and flags which aren't supported. If we can't
// figure it out, just return all flags/commands.
$vcs = null;
// We have to build our own because if we requiresWorkingCopy() we'll throw
// if we aren't in a .arcconfig directory. We probably still can't do much,
// but commands can raise more detailed errors.
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(getcwd());
if ($working_copy->getProjectRoot()) {
$repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
$working_copy);
$vcs = $repository_api->getSourceControlSystemName();
}
$arc_config = $this->getArcanistConfiguration();
if ($pos == 1) {
$workflows = $arc_config->buildAllWorkflows();
$complete = array();
foreach ($workflows as $name => $workflow) {
if (!$workflow->shouldShellComplete()) {
continue;
}
$supported = $workflow->getSupportedRevisionControlSystems();
$ok = (in_array('any', $supported)) ||
(in_array($vcs, $supported));
if (!$ok) {
continue;
}
$complete[] = $name;
}
// Also permit autocompletion of "arc alias" commands.
foreach (
ArcanistAliasWorkflow::getAliases($working_copy) as $key => $value) {
$complete[] = $key;
}
echo implode(' ', $complete)."\n";
return 0;
} else {
$workflow = $arc_config->buildWorkflow($argv[1]);
if (!$workflow) {
list($new_command, $new_args) = ArcanistAliasWorkflow::resolveAliases(
$argv[1],
$arc_config,
array_slice($argv, 2),
$working_copy);
if ($new_command) {
$workflow = $arc_config->buildWorkflow($new_command);
}
if (!$workflow) {
return 1;
} else {
$argv = array_merge(
array($argv[0]),
array($new_command),
$new_args);
}
}
$arguments = $workflow->getArguments();
$prev = idx($argv, $pos - 1, null);
if (!strncmp($prev, '--', 2)) {
$prev = substr($prev, 2);
} else {
$prev = null;
}
if ($prev !== null &&
isset($arguments[$prev]) &&
isset($arguments[$prev]['param'])) {
$type = idx($arguments[$prev], 'paramtype');
switch ($type) {
case 'file':
echo "FILE\n";
break;
case 'complete':
echo implode(' ', $workflow->getShellCompletions($argv))."\n";
break;
default:
echo "ARGUMENT\n";
break;
}
return 0;
} else {
$output = array();
foreach ($arguments as $argument => $spec) {
if ($argument == '*') {
continue;
}
if ($vcs &&
isset($spec['supports']) &&
!in_array($vcs, $spec['supports'])) {
continue;
}
$output[] = '--'.$argument;
}
$cur = idx($argv, $pos, '');
$any_match = false;
if (strlen($cur)) {
foreach ($output as $possible) {
if (!strncmp($possible, $cur, strlen($cur))) {
$any_match = true;
}
}
}
if (!$any_match && isset($arguments['*'])) {
// TODO: This is mega hacktown but something else probably breaks
// if we use a rich argument specification; fix it when we move to
// PhutilArgumentParser since everything will need to be tested then
// anyway.
if ($arguments['*'] == 'branch' && isset($repository_api)) {
$branches = $repository_api->getAllBranches();
$branches = ipull($branches, 'name');
$output = $branches;
} else {
$output = array("FILE");
}
}
echo implode(' ', $output)."\n";
return 0;
}
}
}
}
diff --git a/src/workflow/ArcanistSvnHookPreCommitWorkflow.php b/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
index 3f21fa46..a89a8a6e 100644
--- a/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
+++ b/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
@@ -1,247 +1,231 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Installable as an SVN "pre-commit" hook.
*
* @group workflow
*/
final class ArcanistSvnHookPreCommitWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'svn-hook-pre-commit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**svn-hook-pre-commit** __repository__ __transaction__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: svn
You can install this as an SVN pre-commit hook. For more information,
see the article "Installing Arcanist SVN Hooks" in the Arcanist
documentation.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'svnargs',
);
}
public function shouldShellComplete() {
return false;
}
public function run() {
$svnargs = $this->getArgument('svnargs');
$repository = $svnargs[0];
$transaction = $svnargs[1];
list($commit_message) = execx(
'svnlook log --transaction %s %s',
$transaction,
$repository);
if (strpos($commit_message, '@bypass-lint') !== false) {
return 0;
}
// TODO: Do stuff with commit message.
list($changed) = execx(
'svnlook changed --transaction %s %s',
$transaction,
$repository);
$paths = array();
$changed = explode("\n", trim($changed));
foreach ($changed as $line) {
$matches = null;
preg_match('/^..\s*(.*)$/', $line, $matches);
$paths[$matches[1]] = strlen($matches[1]);
}
$resolved = array();
$failed = array();
$missing = array();
$found = array();
asort($paths);
foreach ($paths as $path => $length) {
foreach ($resolved as $rpath => $root) {
if (!strncmp($path, $rpath, strlen($rpath))) {
$resolved[$path] = $root;
continue 2;
}
}
$config = $path;
if (basename($config) == '.arcconfig') {
$resolved[$config] = $config;
continue;
}
$config = rtrim($config, '/');
$last_config = $config;
do {
if (!empty($missing[$config])) {
break;
} else if (!empty($found[$config])) {
$resolved[$path] = $found[$config];
break;
}
list($err) = exec_manual(
'svnlook cat --transaction %s %s %s',
$transaction,
$repository,
$config ? $config.'/.arcconfig' : '.arcconfig');
if ($err) {
$missing[$path] = true;
} else {
$resolved[$path] = $config ? $config.'/.arcconfig' : '.arcconfig';
$found[$config] = $resolved[$path];
break;
}
$config = dirname($config);
if ($config == '.') {
$config = '';
}
if ($config == $last_config) {
break;
}
$last_config = $config;
} while (true);
if (empty($resolved[$path])) {
$failed[] = $path;
}
}
if ($failed && $resolved) {
$failed_paths = ' '.implode("\n ", $failed);
$resolved_paths = ' '.implode("\n ", array_keys($resolved));
throw new ArcanistUsageException(
"This commit includes a mixture of files in Arcanist projects and ".
"outside of Arcanist projects. A commit which affects an Arcanist ".
"project must affect only that project.\n\n".
"Files in projects:\n\n".
$resolved_paths."\n\n".
"Files not in projects:\n\n".
$failed_paths);
}
if (!$resolved) {
// None of the affected paths are beneath a .arcconfig file.
return 0;
}
$groups = array();
foreach ($resolved as $path => $project) {
$groups[$project][] = $path;
}
if (count($groups) > 1) {
$message = array();
foreach ($groups as $project => $group) {
$message[] = "Files underneath '{$project}':\n\n";
$message[] = " ".implode("\n ", $group)."\n\n";
}
$message = implode('', $message);
throw new ArcanistUsageException(
"This commit includes a mixture of files from different Arcanist ".
"projects. A commit which affects an Arcanist project must affect ".
"only that project.\n\n".
$message);
}
$config_file = key($groups);
$project_root = dirname($config_file);
$paths = reset($groups);
list($config) = execx(
'svnlook cat --transaction %s %s %s',
$transaction,
$repository,
$config_file);
$working_copy = ArcanistWorkingCopyIdentity::newFromRootAndConfigFile(
$project_root,
$config,
$config_file." (svnlook: {$transaction} {$repository})");
$repository_api = new ArcanistSubversionHookAPI(
$project_root,
$transaction,
$repository);
$lint_engine = $working_copy->getConfig('lint_engine');
if (!$lint_engine) {
return 0;
}
$engine = newv($lint_engine, array());
$engine->setWorkingCopy($working_copy);
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ERROR);
$engine->setPaths($paths);
$engine->setCommitHookMode(true);
$engine->setHookAPI($repository_api);
try {
$results = $engine->run();
} catch (ArcanistNoEffectException $no_effect) {
// Nothing to do, bail out.
return 0;
}
$failures = array();
foreach ($results as $result) {
if (!$result->getMessages()) {
continue;
}
$failures[] = $result;
}
if ($failures) {
$at = "@";
$msg = phutil_console_format(
"\n**LINT ERRORS**\n\n".
"This changeset has lint errors. You must fix all lint errors before ".
"you can commit.\n\n".
"You can add '{$at}bypass-lint' to your commit message to disable ".
"lint checks for this commit, or '{$at}nolint' to the file with ".
"errors to disable lint for that file.\n\n");
echo phutil_console_wrap($msg);
$renderer = new ArcanistLintConsoleRenderer();
foreach ($failures as $result) {
echo $renderer->renderLintResult($result);
}
return 1;
}
return 0;
}
}
diff --git a/src/workflow/ArcanistTasksWorkflow.php b/src/workflow/ArcanistTasksWorkflow.php
index 19b2f26a..be6dced3 100644
--- a/src/workflow/ArcanistTasksWorkflow.php
+++ b/src/workflow/ArcanistTasksWorkflow.php
@@ -1,258 +1,242 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Displays User Tasks
*
* @group workflow
*/
final class ArcanistTasksWorkflow extends ArcanistBaseWorkflow {
private $tasks;
public function getWorkflowName() {
return 'tasks';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**tasks** [__options__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
View all assigned tasks.
EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function requiresRepositoryAPI() {
return false;
}
public function requiresAuthentication() {
return true;
}
public function getArguments() {
return array(
'status' => array(
'param' => 'task_status',
'help' => "Show tasks that or open or closed, default is open.",
),
'owner' => array(
'param' => 'username',
'paramtype' => 'username',
'help' =>
"Only show tasks assigned to the given username, ".
"also accepts @all to show all, default is you.",
),
'order' => array(
'param' => 'task_order',
'help' =>
"Arrange tasks based on priority, created, or modified, ".
"default is priority.",
),
'limit' => array(
'param' => 'n',
'paramtype' => 'int',
'help' => "Limit the amount of tasks outputted, default is all.",
)
);
}
public function run() {
$output = array();
$status = $this->getArgument('status');
$owner = $this->getArgument('owner');
$order = $this->getArgument('order');
$limit = $this->getArgument('limit');
$this->tasks = $this->loadManiphestTasks(
($status == 'all' ? 'any' : $status),
($owner ? $this->findOwnerPhid($owner) : $this->getUserPHID()),
$order,
$limit);
if (!$this->tasks) {
echo "No tasks found.\n";
return 0;
}
$task_rows = array();
foreach ($this->tasks as $task) {
$output = array();
// Render the "T123" column.
$task_id = "T".$task['id'];
$formatted_task_id = phutil_console_format(
'**%s**',
$task_id);
$output['id'] = array(
'text' => $formatted_task_id,
'len' => phutil_utf8_console_strlen($task_id),
);
// Render the "Title" column.
$formatted_title = rtrim($task['title']);
$output['title'] = array(
'text' => $formatted_title,
'len' => phutil_utf8_console_strlen($formatted_title),
);
// Render the "Priority" column.
switch ($task['priority']) {
case 'Needs Triage':
$color = 'magenta';
break;
case 'Unbreak Now!':
$color = 'red';
break;
case 'High':
$color = 'yellow';
break;
case 'Normal':
$color = 'green';
break;
case 'Low':
$color = 'blue';
break;
case 'Wishlist':
$color = 'cyan';
break;
default:
$color = 'white';
break;
}
$formatted_priority = phutil_console_format(
"<bg:{$color}> </bg> %s",
$task['priority']);
$output['priority'] = array(
'text' => $formatted_priority,
'len' => phutil_utf8_console_strlen($task['priority']) + 2,
);
// Render the "Status" column.
if ($task['status']) {
$status_text = 'Closed';
$status_color = 'red';
} else {
$status_text = 'Open';
$status_color = 'green';
}
$formatted_status = phutil_console_format(
"<bg:{$status_color}> </bg> %s",
$status_text);
$output['status'] = array(
'text' => $formatted_status,
'len' => phutil_utf8_console_strlen('status') + 2,
);
$task_rows[] = $output;
}
// Find the longest string in each column.
$col_size = array();
foreach ($task_rows as $row) {
foreach ($row as $key => $col) {
if (empty($col_size[$key])) {
$col_size[$key] = 0;
}
$col_size[$key] = max($col_size[$key], $col['len']);
}
}
// Determine the terminal width. If we can't figure it out, assume 80.
$width = nonempty(phutil_console_get_terminal_width(), 80);
// We're going to clip the titles so they'll all fit in one line on the
// terminal. Figure out where to clip them.
$padding_between_columns = 4;
$clip_title_at = max(
// Always show at least a little bit of text even if it will make the
// UI wrap, since it's useless if we don't show anything.
16,
$width -
($col_size['id'] + $col_size['priority'] + $col_size['status'] +
($padding_between_columns * 3)));
$col_size['title'] = min($col_size['title'], $clip_title_at);
foreach ($task_rows as $key => $cols) {
$new_title = phutil_utf8_shorten($cols['title']['text'], $clip_title_at);
$task_rows[$key]['title']['len'] = phutil_utf8_console_strlen($new_title);
$task_rows[$key]['title']['text'] = $new_title;
}
$table = array();
foreach ($task_rows as $row) {
$trow = array();
foreach ($row as $col => $cell) {
$text = $cell['text'];
$pad_len = $col_size[$col] - $cell['len'];
if ($pad_len) {
$text .= str_repeat(' ', $pad_len);
}
$trow[] = $text;
}
$table[] = implode(str_repeat(' ', $padding_between_columns), $trow);
}
$table = implode("\n", $table)."\n";
echo $table;
}
private function findOwnerPhid($owner) {
$conduit = $this->getConduit();
$owner_phid = $conduit->callMethodSynchronous(
'user.find',
array(
'aliases' => array($owner),
));
return (isset($owner_phid[$owner])?$owner_phid[$owner]:false);
}
private function loadManiphestTasks($status, $owner_phid, $order, $limit) {
$conduit = $this->getConduit();
$find_params = array();
if ($owner_phid !== false) {
$find_params['ownerPHIDs'] = array($owner_phid);
}
if ($limit !== false) {
$find_params['limit'] = $limit;
}
$find_params['order'] = ($order?"order-".$order:"order-priority");
$find_params['status'] = ($status?"status-".$status:"status-open");
$tasks = $conduit->callMethodSynchronous(
'maniphest.find',
$find_params
);
return $tasks;
}
}
diff --git a/src/workflow/ArcanistTodoWorkflow.php b/src/workflow/ArcanistTodoWorkflow.php
index 474d4d0e..8a1a248e 100644
--- a/src/workflow/ArcanistTodoWorkflow.php
+++ b/src/workflow/ArcanistTodoWorkflow.php
@@ -1,108 +1,92 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Quickly create a task
*
* @group workflow
*/
final class ArcanistTodoWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'todo';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**todo** __summary__ [__options__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Quickly create a task for yourself.
EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function desiresWorkingCopy() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function getArguments() {
return array(
'*' => 'summary',
'cc' => array(
'param' => 'cc',
'short' => 'C',
'repeat' => true,
'help' => 'Other users to CC on the new task.',
),
);
}
public function run() {
$summary = implode(' ', $this->getArgument('summary'));
$ccs = $this->getArgument('cc');
$conduit = $this->getConduit();
if (trim($summary) == '') {
echo "Please provide a summary.\n";
return;
}
$args = array(
'title' => $summary,
'ownerPHID' => $this->getUserPHID()
);
if ($ccs) {
$phids = array();
$users = $conduit->callMethodSynchronous(
'user.query',
array(
'usernames' => $ccs
));
foreach ($users as $user => $info) {
$phids[] = $info['phid'];
}
$args['ccPHIDs'] = $phids;
}
$result = $conduit->callMethodSynchronous(
'maniphest.createtask',
$args);
echo phutil_console_format(
"Created task T%s: '<fg:green>**%s**</fg>' at <fg:blue>**%s**</fg>\n",
$result['id'],
$result['title'],
$result['uri']);
}
}
diff --git a/src/workflow/ArcanistUnitWorkflow.php b/src/workflow/ArcanistUnitWorkflow.php
index befacbb1..7da6d268 100644
--- a/src/workflow/ArcanistUnitWorkflow.php
+++ b/src/workflow/ArcanistUnitWorkflow.php
@@ -1,342 +1,326 @@
<?php
-/*
- * Copyright 2012 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 unit tests which cover your changes.
*
* @group workflow
*/
final class ArcanistUnitWorkflow extends ArcanistBaseWorkflow {
const RESULT_OKAY = 0;
const RESULT_UNSOUND = 1;
const RESULT_FAIL = 2;
const RESULT_SKIP = 3;
const RESULT_POSTPONED = 4;
private $unresolvedTests;
private $testResults;
private $engine;
public function getWorkflowName() {
return 'unit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**unit** [__options__] [__paths__]
**unit** [__options__] --rev [__rev__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
Run unit tests that cover specified paths. If no paths are specified,
unit tests covering all modified files will be run.
EOTEXT
);
}
public function getArguments() {
return array(
'rev' => array(
'param' => 'revision',
'help' => "Run unit tests covering changes since a specific revision.",
'supports' => array(
'git',
'hg',
),
'nosupport' => array(
'svn' => "Arc unit does not currently support --rev in SVN.",
),
),
'engine' => array(
'param' => 'classname',
'help' =>
"Override configured unit engine for this project."
),
'coverage' => array(
'help' => 'Always enable coverage information.',
'conflicts' => array(
'no-coverage' => null,
),
),
'no-coverage' => array(
'help' => 'Always disable coverage information.',
),
'detailed-coverage' => array(
'help' => "Show a detailed coverage report on the CLI. Implies ".
"--coverage.",
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function getEngine() {
return $this->engine;
}
public function run() {
$working_copy = $this->getWorkingCopy();
$engine_class = $this->getArgument(
'engine',
$working_copy->getConfigFromAnySource('unit.engine'));
if (!$engine_class) {
throw new ArcanistNoEngineException(
"No unit test engine is configured for this project. Edit .arcconfig ".
"to specify a unit test engine.");
}
$paths = $this->getArgument('paths');
$rev = $this->getArgument('rev');
$paths = $this->selectPathsForWorkflow($paths, $rev);
if (!class_exists($engine_class) ||
!is_subclass_of($engine_class, 'ArcanistBaseUnitTestEngine')) {
throw new ArcanistUsageException(
"Configured unit test engine '{$engine_class}' is not a subclass of ".
"'ArcanistBaseUnitTestEngine'.");
}
$this->engine = newv($engine_class, array());
$this->engine->setWorkingCopy($working_copy);
$this->engine->setPaths($paths);
$this->engine->setArguments($this->getPassthruArgumentsAsMap('unit'));
$enable_coverage = null; // Means "default".
if ($this->getArgument('coverage') ||
$this->getArgument('detailed-coverage')) {
$enable_coverage = true;
} else if ($this->getArgument('no-coverage')) {
$enable_coverage = false;
}
$this->engine->setEnableCoverage($enable_coverage);
// Enable possible async tests only for 'arc diff' not 'arc unit'
if ($this->getParentWorkflow()) {
$this->engine->setEnableAsyncTests(true);
} else {
$this->engine->setEnableAsyncTests(false);
}
$results = $this->engine->run();
$this->testResults = $results;
$console = PhutilConsole::getConsole();
$unresolved = array();
$coverage = array();
$postponed_count = 0;
foreach ($results as $result) {
$result_code = $result->getResult();
if ($result_code == ArcanistUnitTestResult::RESULT_POSTPONED) {
$postponed_count++;
$unresolved[] = $result;
} else {
if ($this->engine->shouldEchoTestResults()) {
$duration = '';
if ($result_code == ArcanistUnitTestResult::RESULT_PASS) {
$duration = ' '.self::formatTestDuration($result->getDuration());
}
$console->writeOut(
" %s %s\n",
$result->getConsoleFormattedResult().$duration,
$result->getName());
}
if ($result_code != ArcanistUnitTestResult::RESULT_PASS) {
if ($this->engine->shouldEchoTestResults()) {
$console->writeOut("%s\n", $result->getUserData());
}
$unresolved[] = $result;
}
}
if ($result->getCoverage()) {
foreach ($result->getCoverage() as $file => $report) {
$coverage[$file][] = $report;
}
}
}
if ($postponed_count) {
$postponed = id(new ArcanistUnitTestResult())
->setResult(ArcanistUnitTestResult::RESULT_POSTPONED);
$console->writeOut(
"%s %s\n",
$postponed->getConsoleFormattedResult(),
pht('%d test(s)', $postponed_count));
}
if ($coverage) {
$file_coverage = array_fill_keys(
$paths,
0);
$file_reports = array();
foreach ($coverage as $file => $reports) {
$report = ArcanistUnitTestResult::mergeCoverage($reports);
$cov = substr_count($report, 'C');
$uncov = substr_count($report, 'U');
if ($cov + $uncov) {
$coverage = $cov / ($cov + $uncov);
} else {
$coverage = 0;
}
$file_coverage[$file] = $coverage;
$file_reports[$file] = $report;
}
$console->writeOut("\n__COVERAGE REPORT__\n");
asort($file_coverage);
foreach ($file_coverage as $file => $coverage) {
$console->writeOut(
" **%s%%** %s\n",
sprintf('% 3d', (int)(100 * $coverage)),
$file);
$full_path = $working_copy->getProjectRoot().'/'.$file;
if ($this->getArgument('detailed-coverage') &&
Filesystem::pathExists($full_path) &&
is_file($full_path)) {
$console->writeOut(
'%s',
$this->renderDetailedCoverageReport(
Filesystem::readFile($full_path),
$file_reports[$file]));
}
}
}
$this->unresolvedTests = $unresolved;
$overall_result = self::RESULT_OKAY;
foreach ($results as $result) {
$result_code = $result->getResult();
if ($result_code == ArcanistUnitTestResult::RESULT_FAIL ||
$result_code == ArcanistUnitTestResult::RESULT_BROKEN) {
$overall_result = self::RESULT_FAIL;
break;
} else if ($result_code == ArcanistUnitTestResult::RESULT_UNSOUND) {
$overall_result = self::RESULT_UNSOUND;
} else if ($result_code == ArcanistUnitTestResult::RESULT_POSTPONED &&
$overall_result != self::RESULT_UNSOUND) {
$overall_result = self::RESULT_POSTPONED;
}
}
return $overall_result;
}
public function getUnresolvedTests() {
return $this->unresolvedTests;
}
public function getTestResults() {
return $this->testResults;
}
private static function formatTestDuration($seconds) {
// Very carefully define inclusive upper bounds on acceptable unit test
// durations. Times are in milliseconds and are in increasing order.
$acceptableness = array(
50 => "<fg:green>%s</fg><fg:yellow>\xE2\x98\x85</fg> ",
200 => '<fg:green>%s</fg> ',
500 => '<fg:yellow>%s</fg> ',
INF => '<fg:red>%s</fg> ',
);
$milliseconds = $seconds * 1000;
$duration = self::formatTime($seconds);
foreach ($acceptableness as $upper_bound => $formatting) {
if ($milliseconds <= $upper_bound) {
return phutil_console_format($formatting, $duration);
}
}
return phutil_console_format(end($acceptableness), $duration);
}
private static function formatTime($seconds) {
if ($seconds >= 60) {
$minutes = floor($seconds / 60);
return sprintf('%dm%02ds', $minutes, round($seconds % 60));
}
if ($seconds >= 1) {
return sprintf('%4.1fs', $seconds);
}
$milliseconds = $seconds * 1000;
if ($milliseconds >= 1) {
return sprintf('%3dms', round($milliseconds));
}
return ' <1ms';
}
private function renderDetailedCoverageReport($data, $report) {
$data = explode("\n", $data);
$out = '';
$n = 0;
foreach ($data as $line) {
$out .= sprintf('% 5d ', $n + 1);
$line = str_pad($line, 80, ' ');
if (empty($report[$n])) {
$c = 'N';
} else {
$c = $report[$n];
}
switch ($c) {
case 'C':
$out .= phutil_console_format(
'<bg:green> %s </bg>',
$line);
break;
case 'U':
$out .= phutil_console_format(
'<bg:red> %s </bg>',
$line);
break;
case 'X':
$out .= phutil_console_format(
'<bg:magenta> %s </bg>',
$line);
break;
default:
$out .= ' '.$line.' ';
break;
}
$out .= "\n";
$n++;
}
return $out;
}
}
diff --git a/src/workflow/ArcanistUpgradeWorkflow.php b/src/workflow/ArcanistUpgradeWorkflow.php
index 47bd3ba5..348686ce 100644
--- a/src/workflow/ArcanistUpgradeWorkflow.php
+++ b/src/workflow/ArcanistUpgradeWorkflow.php
@@ -1,102 +1,86 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Upgrade arcanist itself.
*
* @group workflow
*/
final class ArcanistUpgradeWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'upgrade';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**upgrade**
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Upgrade arcanist and libphutil to the latest versions.
EOTEXT
);
}
public function getArguments() {
return array();
}
public function run() {
$roots = array();
$roots['libphutil'] = dirname(phutil_get_library_root('phutil'));
$roots['arcanist'] = dirname(phutil_get_library_root('arcanist'));
foreach ($roots as $lib => $root) {
echo "Upgrading {$lib}...\n";
if (!Filesystem::pathExists($root.'/.git')) {
throw new ArcanistUsageException(
"{$lib} must be in its git working copy to be automatically ".
"upgraded. This copy of {$lib} (in '{$root}') is not in a git ".
"working copy.");
}
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($root);
$repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
$working_copy);
if ($repository_api->supportsRelativeLocalCommits()) {
$repository_api->setDefaultBaseCommit();
}
$this->setRepositoryAPI($repository_api);
// Require no local changes.
$this->requireCleanWorkingCopy();
// Require the library be on master.
$branch_name = $repository_api->getBranchName();
if ($branch_name != 'master') {
throw new ArcanistUsageException(
"{$lib} must be on branch 'master' to be automatically upgraded. ".
"This copy of {$lib} (in '{$root}') is on branch '{$branch_name}'.");
}
chdir($root);
try {
phutil_passthru('git pull --rebase');
} catch (Exception $ex) {
phutil_passthru('git rebase --abort');
throw $ex;
}
}
echo phutil_console_wrap(
phutil_console_format(
"**Updated!** Your copy of arc is now up to date.\n"));
return 0;
}
}
diff --git a/src/workflow/ArcanistUploadWorkflow.php b/src/workflow/ArcanistUploadWorkflow.php
index 66adf225..f460641a 100644
--- a/src/workflow/ArcanistUploadWorkflow.php
+++ b/src/workflow/ArcanistUploadWorkflow.php
@@ -1,125 +1,109 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Upload a file to Phabricator.
*
* @group workflow
*/
final class ArcanistUploadWorkflow extends ArcanistBaseWorkflow {
private $paths;
private $json;
public function getWorkflowName() {
return 'upload';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**upload** __file__ [__file__ ...] [--json]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: filesystems
Upload a file from local disk.
EOTEXT
);
}
public function getArguments() {
return array(
'json' => array(
'help' => 'Output upload information in JSON format.',
),
'*' => 'paths',
);
}
protected function didParseArguments() {
if (!$this->getArgument('paths')) {
throw new ArcanistUsageException("Specify one or more files to upload.");
}
$this->paths = $this->getArgument('paths');
$this->json = $this->getArgument('json');
}
public function requiresAuthentication() {
return true;
}
private function getPaths() {
return $this->paths;
}
private function getJSON() {
return $this->json;
}
public function run() {
$conduit = $this->getConduit();
$results = array();
foreach ($this->paths as $path) {
$name = basename($path);
$this->writeStatusMessage("Uploading '{$name}'...\n");
try {
$data = Filesystem::readFile($path);
} catch (FilesystemException $ex) {
$this->writeStatusMessage(
"Unable to upload file: ".$ex->getMessage()."\n");
$results[$path] = null;
continue;
}
$phid = $conduit->callMethodSynchronous(
'file.upload',
array(
'data_base64' => base64_encode($data),
'name' => $name,
));
$info = $conduit->callMethodSynchronous(
'file.info',
array(
'phid' => $phid,
));
$results[$path] = $info;
if (!$this->getJSON()) {
$id = $info['id'];
echo " F{$id} {$name}: ".$info['uri']."\n\n";
}
}
if ($this->getJSON()) {
echo json_encode($results)."\n";
} else {
$this->writeStatusMessage("Done.\n");
}
return 0;
}
}
diff --git a/src/workflow/ArcanistWhichWorkflow.php b/src/workflow/ArcanistWhichWorkflow.php
index 4169aa49..7717087f 100644
--- a/src/workflow/ArcanistWhichWorkflow.php
+++ b/src/workflow/ArcanistWhichWorkflow.php
@@ -1,208 +1,192 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Show which revision or revisions are in the working copy.
*
* @group workflow
*/
final class ArcanistWhichWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'which';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**which** (svn)
**which** [commit] (hg, git)
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: svn, git, hg
Shows which commits 'arc diff' will select, and which revision is in
the working copy (or which revisions, if more than one matches).
EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function getArguments() {
return array(
'any-author' => array(
'help' => "Show revisions by any author, not just you.",
),
'any-status' => array(
'help' => "Show committed and abandoned revisions.",
),
'base' => array(
'param' => 'rules',
'help' => 'Additional rules for determining base revision.',
'nosupport' => array(
'svn' => 'Subversion does not use base commits.',
),
'supports' => array('git', 'hg'),
),
'show-base' => array(
'help' => 'Print base commit only and exit.',
'nosupport' => array(
'svn' => 'Subversion does not use base commits.',
),
'supports' => array('git', 'hg'),
),
'*' => 'commit',
);
}
public function run() {
$repository_api = $this->getRepositoryAPI();
$arg_commit = $this->getArgument('commit');
if (count($arg_commit)) {
if (!$repository_api->supportsRelativeLocalCommits()) {
throw new ArcanistUsageException(
"This version control system does not support relative commits.");
} else {
$repository_api->parseRelativeLocalCommit($arg_commit);
}
}
$arg = $arg_commit ? ' '.head($arg_commit) : '';
$repository_api->setBaseCommitArgumentRules(
$this->getArgument('base', ''));
if ($repository_api->supportsRelativeLocalCommits()) {
$relative = $repository_api->getRelativeCommit();
if ($this->getArgument('show-base')) {
echo $relative."\n";
return 0;
}
$info = $repository_api->getLocalCommitInformation();
if ($info) {
$commits = array();
foreach ($info as $commit) {
$hash = substr($commit['commit'], 0, 16);
$summary = $commit['summary'];
$commits[] = " {$hash} {$summary}";
}
$commits = implode("\n", $commits);
} else {
$commits = ' (No commits.)';
}
$explanation = $repository_api->getBaseCommitExplanation();
$relative_summary = $repository_api->getCommitSummary($relative);
$relative = substr($relative, 0, 16);
if ($repository_api instanceof ArcanistGitAPI) {
$command = "git diff {$relative}..HEAD";
} else if ($repository_api instanceof ArcanistMercurialAPI) {
$command = "hg diff --rev {$relative}";
} else {
throw new Exception("Unknown VCS!");
}
echo phutil_console_wrap(
phutil_console_format(
"**RELATIVE COMMIT**\n".
"If you run 'arc diff{$arg}', changes between the commit:\n\n"));
echo " {$relative} {$relative_summary}\n\n";
echo phutil_console_wrap(
"...and the current working copy state will be sent to ".
"Differential, because {$explanation}\n\n".
"You can see the exact changes that will be sent by running ".
"this command:\n\n".
" $ {$command}\n\n".
"These commits will be included in the diff:\n\n");
echo $commits."\n\n\n";
}
$any_author = $this->getArgument('any-author');
$any_status = $this->getArgument('any-status');
$query = array(
'authors' => $any_author
? null
: array($this->getUserPHID()),
'status' => $any_status
? 'status-any'
: 'status-open',
);
$revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
$query);
echo phutil_console_wrap(
phutil_console_format(
"**MATCHING REVISIONS**\n".
"These Differential revisions match the changes in this working ".
"copy:\n\n"));
if (empty($revisions)) {
echo " (No revisions match.)\n";
echo "\n";
echo phutil_console_wrap(
phutil_console_format(
"Since there are no revisions in Differential which match this ".
"working copy, a new revision will be **created** if you run ".
"'arc diff{$arg}'.\n\n"));
} else {
foreach ($revisions as $revision) {
echo ' D'.$revision['id'].' '.$revision['title']."\n";
echo ' Reason: '.$revision['why']."\n";
echo "\n";
}
if (count($revisions) == 1) {
echo phutil_console_wrap(
phutil_console_format(
"Since exactly one revision in Differential matches this working ".
"copy, it will be **updated** if you run 'arc diff{$arg}'."));
} else {
echo phutil_console_wrap(
"Since more than one revision in Differential matches this working ".
"copy, you will be asked which revision you want to update if ".
"you run 'arc diff {$arg}'.");
}
echo "\n\n";
}
return 0;
}
}
diff --git a/src/workflow/exception/ArcanistCapabilityNotSupportedException.php b/src/workflow/exception/ArcanistCapabilityNotSupportedException.php
index f57fcf86..cd4e4be4 100644
--- a/src/workflow/exception/ArcanistCapabilityNotSupportedException.php
+++ b/src/workflow/exception/ArcanistCapabilityNotSupportedException.php
@@ -1,28 +1,12 @@
<?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.
- */
-
final class ArcanistCapabilityNotSupportedException extends Exception {
public function __construct(ArcanistRepositoryAPI $api) {
$name = $api->getSourceControlSystemName();
parent::__construct(
"This repository API ('{$name}') does not support the requested ".
"capability.");
}
}
diff --git a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
index dd3dec43..b501c0a8 100644
--- a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
+++ b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
@@ -1,259 +1,243 @@
<?php
-/*
- * Copyright 2012 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.
- */
-
/**
* Interfaces with basic information about the working copy.
*
*
* @task config
*
* @group workingcopy
*/
final class ArcanistWorkingCopyIdentity {
protected $localConfig;
protected $projectConfig;
protected $projectRoot;
public static function newFromPath($path) {
$project_id = null;
$project_root = null;
$config = array();
foreach (Filesystem::walkToRoot($path) as $dir) {
$config_file = $dir.'/.arcconfig';
if (!Filesystem::pathExists($config_file)) {
continue;
}
$proj_raw = Filesystem::readFile($config_file);
$config = self::parseRawConfigFile($proj_raw, $config_file);
$project_root = $dir;
break;
}
if (!$project_root) {
foreach (Filesystem::walkToRoot($path) as $dir) {
$try = array(
$dir.'/.svn',
$dir.'/.hg',
$dir.'/.git',
);
foreach ($try as $trydir) {
if (Filesystem::pathExists($trydir)) {
$project_root = $dir;
break 2;
}
}
}
}
return new ArcanistWorkingCopyIdentity($project_root, $config);
}
public static function newFromRootAndConfigFile(
$root,
$config_raw,
$from_where) {
$config = self::parseRawConfigFile($config_raw, $from_where);
return new ArcanistWorkingCopyIdentity($root, $config);
}
private static function parseRawConfigFile($raw_config, $from_where) {
$proj = json_decode($raw_config, true);
if (!is_array($proj)) {
throw new Exception(
"Unable to parse '.arcconfig' file '{$from_where}'. The file contents ".
"should be valid JSON.\n\n".
"FILE CONTENTS\n".
substr($raw_config, 0, 2048));
}
$required_keys = array(
'project_id',
);
foreach ($required_keys as $key) {
if (!array_key_exists($key, $proj)) {
throw new Exception(
"Required key '{$key}' is missing from '.arcconfig' file ".
"'{$from_where}'.");
}
}
return $proj;
}
protected function __construct($root, array $config) {
$this->projectRoot = $root;
$this->projectConfig = $config;
$this->localConfig = array();
$vc_dirs = array(
'.git',
'.hg',
'.svn',
);
$found_meta_dir = false;
foreach ($vc_dirs as $dir) {
$meta_path = Filesystem::resolvePath(
$dir,
$this->projectRoot);
if (Filesystem::pathExists($meta_path)) {
$found_meta_dir = true;
$local_path = Filesystem::resolvePath(
'arc/config',
$meta_path);
if (Filesystem::pathExists($local_path)) {
$file = Filesystem::readFile($local_path);
if ($file) {
$this->localConfig = json_decode($file, true);
}
}
break;
}
}
if (!$found_meta_dir) {
// Try for a single higher-level .svn directory as used by svn 1.7+
foreach (Filesystem::walkToRoot($this->projectRoot) as $parent_path) {
$local_path = Filesystem::resolvePath(
'.svn/arc/config',
$parent_path);
if (Filesystem::pathExists($local_path)) {
$file = Filesystem::readFile($local_path);
if ($file) {
$this->localConfig = json_decode($file, true);
}
}
}
}
}
public function getProjectID() {
return $this->getConfig('project_id');
}
public function getProjectRoot() {
return $this->projectRoot;
}
public function getConduitURI() {
return $this->getConfig('conduit_uri');
}
/* -( Config )------------------------------------------------------------- */
public function getProjectConfig() {
return $this->projectConfig;
}
/**
* Read a configuration directive from project configuration. This reads ONLY
* permanent project configuration (i.e., ".arcconfig"), not other
* configuration sources. See @{method:getConfigFromAnySource} to read from
* user configuration.
*
* @param key Key to read.
* @param wild Default value if key is not found.
* @return wild Value, or default value if not found.
*
* @task config
*/
public function getConfig($key, $default = null) {
$settings = new ArcanistSettings();
$pval = idx($this->projectConfig, $key);
// Test for older names in the per-project config only, since
// they've only been used there.
if ($pval === null) {
$legacy = $settings->getLegacyName($key);
if ($legacy) {
$pval = $this->getConfig($legacy);
}
}
if ($pval === null) {
$pval = $default;
} else {
$pval = $settings->willReadValue($key, $pval);
}
return $pval;
}
/**
* Read a configuration directive from local configuration. This
* reads ONLY the per-working copy configuration,
* i.e. .(git|hg|svn)/arc/config, and not other configuration
* sources. See @{method:getConfigFromAnySource} to read from any
* config source or @{method:getConfig} to read permanent
* project-level config.
*
* @task config
*/
public function getLocalConfig($key, $default=null) {
return idx($this->localConfig, $key, $default);
}
/**
* Read a configuration directive from any available configuration source.
* In contrast to @{method:getConfig}, this will look for the directive in
* local and user configuration in addition to project configuration.
* The precedence is local > project > user
*
* @param key Key to read.
* @param wild Default value if key is not found.
* @return wild Value, or default value if not found.
*
* @task config
*/
public function getConfigFromAnySource($key, $default = null) {
$settings = new ArcanistSettings();
// try local config first
$pval = $this->getLocalConfig($key);
// then per-project config
if ($pval === null) {
$pval = $this->getConfig($key);
}
// now try global (i.e. user-level) config
if ($pval === null) {
$global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
$pval = idx($global_config, $key);
}
// Finally, try system-level config.
if ($pval === null) {
$system_config = ArcanistBaseWorkflow::readSystemArcConfig();
$pval = idx($system_config, $key);
}
if ($pval === null) {
$pval = $default;
} else {
$pval = $settings->willReadValue($key, $pval);
}
return $pval;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 22, 10:13 (8 h, 33 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
93/93/5e61dafc4a6a41134812b2bd3fd0
Default Alt Text
(1 MB)

Event Timeline