1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-09-20 08:58:55 +02:00

Merge branch 'master' of github.com:facebook/arcanist into get_engine

This commit is contained in:
mgummelt 2011-09-08 18:26:07 -07:00
commit e9b7f8e3ca
27 changed files with 1233 additions and 95 deletions

View file

@ -122,7 +122,7 @@ foreach (Futures($futures) as $file => $future) {
}
$requirements->addSourceDependency($name, $value);
} else if ($call_name == 'phutil_require_module') {
analyze_phutil_require_module($call, $requirements);
analyze_phutil_require_module($call, $requirements, true);
}
}
} else {
@ -154,7 +154,7 @@ foreach (Futures($futures) as $file => $future) {
$call_name = $name->getConcreteString();
if ($call_name == 'phutil_require_module') {
analyze_phutil_require_module($call, $requirements);
analyze_phutil_require_module($call, $requirements, false);
} else if ($call_name == 'call_user_func' ||
$call_name == 'call_user_func_array') {
$params = $call->getChildByIndex(1)->getChildren();
@ -303,7 +303,7 @@ foreach (Futures($futures) as $file => $future) {
$extends = $interface->getChildByIndex(2);
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) {
$requirements->addInterfaceDependency(
$class_name->getConcreteString(),
$interface_name->getConcreteString(),
$parent,
$parent->getConcreteString());
}
@ -327,7 +327,8 @@ echo json_encode($requirements->toDictionary());
*/
function analyze_phutil_require_module(
XHPASTNode $call,
PhutilModuleRequirements $requirements) {
PhutilModuleRequirements $requirements,
$create_dependency) {
$name = $call->getChildByIndex(0);
$params = $call->getChildByIndex(1)->getChildren();
@ -363,7 +364,9 @@ function analyze_phutil_require_module(
return;
}
$requirements->addModuleDependency(
$name,
$library_value.':'.$module_value);
if ($create_dependency) {
$requirements->addModuleDependency(
$name,
$library_value.':'.$module_value);
}
}

View file

@ -31,6 +31,7 @@ phutil_register_library_map(array(
'ArcanistDifferentialCommitMessage' => 'differential/commitmessage',
'ArcanistDifferentialCommitMessageParserException' => 'differential/commitmessage',
'ArcanistDifferentialRevisionRef' => 'differential/revision',
'ArcanistDownloadWorkflow' => 'workflow/download',
'ArcanistExportWorkflow' => 'workflow/export',
'ArcanistFilenameLinter' => 'lint/linter/filename',
'ArcanistGeneratedLinter' => 'lint/linter/generated',
@ -54,11 +55,13 @@ phutil_register_library_map(array(
'ArcanistLinterTestCase' => 'lint/linter/base/test',
'ArcanistListWorkflow' => 'workflow/list',
'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed',
'ArcanistMercurialAPI' => 'repository/api/mercurial',
'ArcanistNoEffectException' => 'exception/usage/noeffect',
'ArcanistNoEngineException' => 'exception/usage/noengine',
'ArcanistNoLintLinter' => 'lint/linter/nolint',
'ArcanistNoLintTestCaseMisnamed' => 'lint/linter/nolint/__tests__',
'ArcanistPEP8Linter' => 'lint/linter/pep8',
'ArcanistPasteWorkflow' => 'workflow/paste',
'ArcanistPatchWorkflow' => 'workflow/patch',
'ArcanistPhutilModuleLinter' => 'lint/linter/phutilmodule',
'ArcanistPhutilTestCase' => 'unit/engine/phutil/testcase',
@ -73,9 +76,11 @@ phutil_register_library_map(array(
'ArcanistTextLinterTestCase' => 'lint/linter/text/__tests__',
'ArcanistUnitTestResult' => 'unit/result',
'ArcanistUnitWorkflow' => 'workflow/unit',
'ArcanistUploadWorkflow' => 'workflow/upload',
'ArcanistUsageException' => 'exception/usage',
'ArcanistUserAbortException' => 'exception/usage/userabort',
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity',
'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/naminghook',
'ArcanistXHPASTLinter' => 'lint/linter/xhpast',
'ArcanistXHPASTLinterTestCase' => 'lint/linter/xhpast/__tests__',
'BranchInfo' => 'branch',
@ -100,6 +105,7 @@ phutil_register_library_map(array(
'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDiffParserTestCase' => 'ArcanistPhutilTestCase',
'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFilenameLinter' => 'ArcanistLinter',
'ArcanistGeneratedLinter' => 'ArcanistLinter',
@ -114,11 +120,13 @@ phutil_register_library_map(array(
'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase',
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
'ArcanistNoEffectException' => 'ArcanistUsageException',
'ArcanistNoEngineException' => 'ArcanistUsageException',
'ArcanistNoLintLinter' => 'ArcanistLinter',
'ArcanistNoLintTestCaseMisnamed' => 'ArcanistLinterTestCase',
'ArcanistPEP8Linter' => 'ArcanistLinter',
'ArcanistPasteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPhutilModuleLinter' => 'ArcanistLinter',
'ArcanistPyFlakesLinter' => 'ArcanistLinter',
@ -129,6 +137,7 @@ phutil_register_library_map(array(
'ArcanistTextLinter' => 'ArcanistLinter',
'ArcanistTextLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistUnitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUploadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUserAbortException' => 'ArcanistUsageException',
'ArcanistXHPASTLinter' => 'ArcanistLinter',
'ArcanistXHPASTLinterTestCase' => 'ArcanistLinterTestCase',

View file

@ -28,7 +28,7 @@ class ArcanistDifferentialRevisionRef {
protected $statusName;
protected $sourcePath;
public function newFromDictionary(array $dictionary) {
public static function newFromDictionary(array $dictionary) {
$ref = new ArcanistDifferentialRevisionRef();
$ref->id = $dictionary['id'];
$ref->name = $dictionary['name'];

View file

@ -65,7 +65,7 @@ abstract class ArcanistLintEngine {
protected $charToLine = array();
protected $lineToFirstChar = array();
private $results = array();
private $minimumSeverity = null;
private $minimumSeverity = ArcanistLintSeverity::SEVERITY_DISABLED;
private $changedLines = array();
private $commitHookMode = false;

View file

@ -155,6 +155,30 @@ class ArcanistXHPASTLinter extends ArcanistLinter {
$this->lintPlusOperatorOnStrings($root);
$this->lintDuplicateKeysInArray($root);
$this->lintReusedIterators($root);
$this->lintBraceFormatting($root);
}
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 (count($before) == 1) {
$before = reset($before);
if ($before->getValue() != ' ') {
$this->raiseLintAtToken(
$before,
self::LINT_FORMATTING_CONVENTIONS,
'Put opening braces on the same line as control statements and '.
'declarations, with a single space before them.',
' ');
}
}
}
}
private function lintTautologicalExpressions($root) {
@ -641,27 +665,37 @@ class ArcanistXHPASTLinter extends ArcanistLinter {
}
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();
$is_xhp = ($name_string[0] == ':');
if ($is_xhp) {
if (!$this->isLowerCaseWithXHP($name_string)) {
$this->raiseLintAtNode(
$name_token,
self::LINT_NAMING_CONVENTIONS,
'Follow naming conventions: xhp elements should be named using '.
'lower case.');
}
$names[] = array(
'xhp-class',
$name_string,
$name_token,
$this->isLowerCaseWithXHP($name_string)
? null
: 'Follow naming conventions: XHP elements should be named using '.
'lower case.',
);
} else {
if (!$this->isUpperCamelCase($name_string)) {
$this->raiseLintAtNode(
$name_token,
self::LINT_NAMING_CONVENTIONS,
'Follow naming conventions: classes should be named using '.
'UpperCamelCase.');
}
$names[] = array(
'class',
$name_string,
$name_token,
$this->isUpperCamelCase($name_string)
? null
: 'Follow naming conventions: classes should be named using '.
'UpperCamelCase.',
);
}
}
@ -669,13 +703,15 @@ class ArcanistXHPASTLinter extends ArcanistLinter {
foreach ($ifaces as $iface) {
$name_token = $iface->getChildByIndex(1);
$name_string = $name_token->getConcreteString();
if (!$this->isUpperCamelCase($name_string)) {
$this->raiseLintAtNode(
$name_token,
self::LINT_NAMING_CONVENTIONS,
'Follow naming conventions: interfaces should be named using '.
'UpperCamelCase.');
}
$names[] = array(
'interface',
$name_string,
$name_token,
$this->isUpperCamelCase($name_string)
? null
: 'Follow naming conventions: interfaces should be named using '.
'UpperCamelCase.',
);
}
@ -687,13 +723,15 @@ class ArcanistXHPASTLinter extends ArcanistLinter {
continue;
}
$name_string = $name_token->getConcreteString();
if (!$this->isLowercaseWithUnderscores($name_string)) {
$this->raiseLintAtNode(
$name_token,
self::LINT_NAMING_CONVENTIONS,
'Follow naming conventions: functions should be named using '.
'lowercase_with_underscores.');
}
$names[] = array(
'function',
$name_string,
$name_token,
$this->isLowercaseWithUnderscores($name_string)
? null
: 'Follow naming conventions: functions should be named using '.
'lowercase_with_underscores.',
);
}
@ -701,13 +739,15 @@ class ArcanistXHPASTLinter extends ArcanistLinter {
foreach ($methods as $method) {
$name_token = $method->getChildByIndex(2);
$name_string = $name_token->getConcreteString();
if (!$this->isLowerCamelCase($name_string)) {
$this->raiseLintAtNode(
$name_token,
self::LINT_NAMING_CONVENTIONS,
'Follow naming conventions: methods should be named using '.
'lowerCamelCase.');
}
$names[] = array(
'method',
$name_string,
$name_token,
$this->isLowerCamelCase($name_string)
? null
: 'Follow naming conventions: methods should be named using '.
'lowerCamelCase.',
);
}
@ -716,13 +756,15 @@ class ArcanistXHPASTLinter extends ArcanistLinter {
foreach ($param_list->getChildren() as $param) {
$name_token = $param->getChildByIndex(1);
$name_string = $name_token->getConcreteString();
if (!$this->isLowercaseWithUnderscores($name_string)) {
$this->raiseLintAtNode(
$name_token,
self::LINT_NAMING_CONVENTIONS,
'Follow naming conventions: parameters should be named using '.
'lowercase_with_underscores.');
}
$names[] = array(
'parameter',
$name_string,
$name_token,
$this->isLowercaseWithUnderscores($name_string)
? null
: 'Follow naming conventions: parameters should be named using '.
'lowercase_with_underscores.',
);
}
}
@ -733,13 +775,15 @@ class ArcanistXHPASTLinter extends ArcanistLinter {
foreach ($constant_list->getChildren() as $constant) {
$name_token = $constant->getChildByIndex(0);
$name_string = $name_token->getConcreteString();
if (!$this->isUppercaseWithUnderscores($name_string)) {
$this->raiseLintAtNode(
$name_token,
self::LINT_NAMING_CONVENTIONS,
'Follow naming conventions: class constants should be named using '.
'UPPERCASE_WITH_UNDERSCORES.');
}
$names[] = array(
'constant',
$name_string,
$name_token,
$this->isUppercaseWithUnderscores($name_string)
? null
: 'Follow naming conventions: class constants should be named '.
'using UPPERCASE_WITH_UNDERSCORES.',
);
}
}
@ -751,15 +795,45 @@ class ArcanistXHPASTLinter extends ArcanistLinter {
}
$name_token = $prop->getChildByIndex(0);
$name_string = $name_token->getConcreteString();
if (!$this->isLowerCamelCase($name_string)) {
$this->raiseLintAtNode(
$name_token,
self::LINT_NAMING_CONVENTIONS,
'Follow naming conventions: class properties should be named '.
'using lowerCamelCase.');
$names[] = array(
'member',
$name_string,
$name_token,
$this->isLowerCamelCase($name_string)
? null
: 'Follow naming conventions: class properties should be named '.
'using lowerCamelCase.',
);
}
}
$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 isUpperCamelCase($str) {

View file

@ -0,0 +1,56 @@
<?php
function f() {
}
function g()
{
}
if (1)
{}
foreach (x() as $y)
{}
while (1)
{}
switch (1)
{}
try
{}
catch (Exception $x)
{}
~~~~~~~~~~
warning:7:13
warning:12:7
warning:15:20
warning:18:10
warning:21:11
warning:24:4
warning:26:21
~~~~~~~~~~
<?php
function f() {
}
function g() {
}
if (1) {}
foreach (x() as $y) {}
while (1) {}
switch (1) {}
try {}
catch (Exception $x) {}

View file

@ -0,0 +1,60 @@
<?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.
*/
/**
* 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
* @group lint
*/
abstract class ArcanistXHPASTLintNamingHook {
final public function __construct() {
// <empty>
}
/**
* 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);
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('ArcanistXHPASTLintNamingHook.php');

View file

@ -168,7 +168,8 @@ class ArcanistLintMessage {
}
public function isPatchable() {
return ($this->getReplacementText() !== null);
return ($this->getReplacementText() !== null) &&
($this->getReplacementText() !== $this->getOriginalText());
}
public function didApplyPatch() {

View file

@ -150,6 +150,17 @@ class ArcanistBundle {
$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;
$result[] = '+++ '.$cur_path;

View file

@ -27,6 +27,7 @@ class ArcanistDiffParser {
protected $text;
protected $line;
protected $isGit;
protected $isMercurial;
protected $detectBinaryFiles = false;
protected $changes = array();
@ -209,6 +210,9 @@ class ArcanistDiffParser {
'(?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".
'(?P<type>diff -r) (?P<hgrev>[a-f0-9]+) (?P<cur>.+)',
);
$ok = false;
@ -274,6 +278,10 @@ class ArcanistDiffParser {
$line = $this->nextLine();
$this->parseChangeset($change);
break;
case 'diff -r':
$this->setIsMercurial(true);
$this->parseIndexHunk($change);
break;
default:
$this->didFailParse("Unknown diff type.");
}
@ -432,8 +440,19 @@ class ArcanistDiffParser {
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);
$line = $this->getLine();
if ($is_git) {
@ -532,19 +551,27 @@ class ArcanistDiffParser {
}
$line = $this->getLine();
$ok = preg_match('/^=+$/', $line) ||
($is_git && preg_match('/^index .*$/', $line));
if (!$ok) {
if ($is_git) {
$this->didFailParse(
"Expected 'index af23f...a98bc' header line.");
if ($is_svn) {
$ok = preg_match('/^=+$/', $line);
if (!$ok) {
$this->didFailParse("Expected '=======================' divider line.");
} else {
$this->didFailParse(
"Expected '==========================' divider line.");
// 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 {
$line = $this->nextLine();
}
}
// Adding an empty file in SVN can produce an empty line here.
$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
@ -596,14 +623,23 @@ class ArcanistDiffParser {
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>.*?)(?:\s*\(.*\))?$@',
'@^[-+]{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'];
}
@ -715,7 +751,7 @@ class ArcanistDiffParser {
$is_binary = false;
if ($this->detectBinaryFiles) {
$is_binary = preg_match('/([^\x09\x0A\x20-\x7E]+)/', $corpus);
$is_binary = !phutil_is_utf8($corpus);
}
if ($is_binary) {

View file

@ -65,6 +65,7 @@ class PhutilModuleRequirements {
}
public function addClassDeclaration(XHPASTNode $where, $name) {
$name = self::mungeXHPClassName($name);
return $this->addDeclaration('class', $where, $name);
}
@ -98,8 +99,10 @@ class PhutilModuleRequirements {
}
public function addClassDependency($child, XHPASTNode $where, $name) {
$name = self::mungeXHPClassName($name);
if ($child !== null) {
if (empty($this->builtins['class'][$name])) {
$child = self::mungeXHPClassName($child);
$this->chain['class'][$child] = $name;
}
}
@ -173,4 +176,13 @@ class PhutilModuleRequirements {
'messages' => $this->messages,
);
}
private static function mungeXHPClassName($name) {
if (strlen($name) && $name[0] == ':') {
// XHP's semantic actions munge element names without a preceding colon.
$name = substr($name, 1);
return 'xhp_'.str_replace(array(':', '-'), array('__', '_'), $name);
}
return $name;
}
}

View file

@ -65,11 +65,21 @@ abstract class ArcanistRepositoryAPI {
"any parent directory. Create an '.arcconfig' file to configure arc.");
}
if (@file_exists($root.'/.svn')) {
phutil_require_module('arcanist', 'repository/api/subversion');
return new ArcanistSubversionAPI($root);
if (Filesystem::pathExists($root.'/.svn')) {
return newv('ArcanistSubversionAPI', array($root));
}
if (Filesystem::pathExists($root.'/.hg')) {
// TODO: Stabilize and remove.
file_put_contents(
'php://stderr',
phutil_console_format(
"**WARNING:** Mercurial support is largely imaginary right now.\n"));
return newv('ArcanistMercurialAPI', array($root));
}
$git_root = self::discoverGitBaseDirectory($root);
if ($git_root) {
if (!Filesystem::pathsAreEquivalent($root, $git_root)) {
@ -77,16 +87,16 @@ abstract class ArcanistRepositoryAPI {
"'.arcconfig' file is located at '{$root}', but working copy root ".
"is '{$git_root}'. Move '.arcconfig' file to the working copy root.");
}
phutil_require_module('arcanist', 'repository/api/git');
return new ArcanistGitAPI($root);
return newv('ArcanistGitAPI', array($root));
}
throw new ArcanistUsageException(
"The current working directory is not part of a working copy for a ".
"supported version control system (svn or git).");
"supported version control system (svn, git or mercurial).");
}
protected function __construct($path) {
public function __construct($path) {
$this->path = $path;
}
@ -147,5 +157,6 @@ abstract class ArcanistRepositoryAPI {
abstract public function getRawDiffText($path);
abstract public function getOriginalFileData($path);
abstract public function getCurrentFileData($path);
abstract public function getLocalCommitInformation();
}

View file

@ -8,8 +8,10 @@
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('phutil', 'console');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistRepositoryAPI.php');

View file

@ -46,6 +46,35 @@ class ArcanistGitAPI extends ArcanistRepositoryAPI {
return $this;
}
public function getLocalCommitInformation() {
list($info) = execx(
'(cd %s && git log %s..%s --format=%s --)',
$this->getPath(),
$this->getRelativeCommit(),
'HEAD',
'%H%x00%T%x00%P%x00%at%x00%an%x00%s');
$commits = array();
$info = trim($info);
$info = explode("\n", $info);
foreach ($info as $line) {
list($commit, $tree, $parents, $time, $author, $title)
= explode("\0", $line, 6);
$commits[] = array(
'commit' => $commit,
'tree' => $tree,
'parents' => array_filter(explode(' ', $parents)),
'time' => $time,
'author' => $author,
'summary' => $title,
);
}
return $commits;
}
public function getRelativeCommit() {
if ($this->relativeCommit === null) {
list($err) = exec_manual(
@ -97,7 +126,6 @@ class ArcanistGitAPI extends ArcanistRepositoryAPI {
}
public function getRawDiffText($path) {
$relative_commit = $this->getRelativeCommit();
$options = $this->getDiffFullOptions();
list($stdout) = execx(
"(cd %s; git diff {$options} %s -- %s)",
@ -317,7 +345,7 @@ class ArcanistGitAPI extends ArcanistRepositoryAPI {
public function getBlame($path) {
// TODO: 'git blame' supports --porcelain and we should probably use it.
list($stdout) = execx(
'(cd %s; git blame -w -C %s -- %s)',
'(cd %s; git blame --date=iso -w -C %s -- %s)',
$this->getPath(),
$this->getRelativeCommit(),
$path);

View file

@ -0,0 +1,317 @@
<?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.
*/
/**
* Interfaces with the Mercurial working copies.
*
* @group workingcopy
*/
class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private $status;
private $base;
private $relativeCommit;
public function getSourceControlSystemName() {
return 'hg';
}
public function getSourceControlBaseRevision() {
list($stdout) = execx(
'(cd %s && hg id -ir %s)',
$this->getPath(),
$this->getRelativeCommit());
return $stdout;
}
public function getSourceControlPath() {
return '/';
}
public function getBranchName() {
// TODO: I have nearly no idea how hg local branches work.
list($stdout) = execx(
'(cd %s && hg branch)',
$this->getPath());
return $stdout;
}
public function setRelativeCommit($commit) {
list($err) = exec_manual(
'(cd %s && hg id -ir %s)',
$this->getPath(),
$commit);
if ($err) {
throw new ArcanistUsageException(
"Commit '{$commit}' is not a valid Mercurial commit identifier.");
}
$this->relativeCommit = $commit;
return $this;
}
public function getRelativeCommit() {
if (empty($this->relativeCommit)) {
list($stdout) = execx(
'(cd %s && hg outgoing --limit 1)',
$this->getPath());
$logs = $this->parseMercurialLog($stdout);
if (!count($logs)) {
throw new ArcanistUsageException("You have no outgoing changes!");
}
$oldest_log = head($logs);
$this->relativeCommit = $oldest_log['rev'].'~1';
}
return $this->relativeCommit;
}
public function getLocalCommitInformation() {
list($info) = execx(
'(cd %s && hg log --rev %s..%s --)',
$this->getPath(),
$this->getRelativeCommit(),
'tip');
return $this->parseMercurialLog($info);
}
public function getBlame($path) {
list($stdout) = execx(
'(cd %s && hg blame -u -v -c --rev %s -- %s)',
$this->getPath(),
$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() {
// 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();
$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) = execx(
'(cd %s && hg status)',
$this->getPath());
$working_status = $this->parseMercurialStatus($stdout);
foreach ($working_status as $path => $status) {
$status |= self::FLAG_UNCOMMITTED;
if (!empty($status_map[$path])) {
$status_map[$path] |= $status;
} else {
$status_map[$path] = $status;
}
}
return $status_map;
}
private function getDiffOptions() {
$options = array(
'-g',
'-U'.$this->getDiffLinesOfContext(),
);
return implode(' ', $options);
}
public function getRawDiffText($path) {
$options = $this->getDiffOptions();
list($stdout) = execx(
'(cd %s && hg diff %C --rev %s --rev tip -- %s)',
$this->getPath(),
$options,
$this->getRelativeCommit(),
$path);
return $stdout;
}
public function getFullMercurialDiff() {
$options = $this->getDiffOptions();
list($stdout) = execx(
'(cd %s && hg diff %C --rev %s --rev tip --)',
$this->getPath(),
$options,
$this->getRelativeCommit());
return $stdout;
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getRelativeCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision($path, 'tip');
}
private function getFileDataAtRevision($path, $revision) {
list($stdout) = execx(
'(cd %s && hg cat --rev %s -- %s)',
$this->getPath(),
$path);
return $stdout;
}
private function parseMercurialStatus($status) {
$result = array();
$status = trim($status);
if (!strlen($status)) {
return $result;
}
$lines = explode("\n", $status);
foreach ($lines as $line) {
$flags = 0;
list($code, $path) = explode(' ', $line, 2);
switch ($code) {
case 'A':
$flags |= self::FLAG_ADDED;
break;
case 'R':
$flags |= self::FLAG_REMOVED;
break;
case 'M':
$flags |= self::FLAG_MODIFIED;
break;
case 'C':
// This is "clean" and included only for completeness, these files
// have not been changed.
break;
case '!':
$flags |= self::FLAG_MISSING;
break;
case '?':
$flags |= self::FLAG_UNTRACKED;
break;
case 'I':
// This is "ignored" and included only for completeness.
break;
default:
throw new Exception("Unknown Mercurial status '{$code}'.");
}
$result[$path] = $flags;
}
return $result;
}
private function parseMercurialLog($log) {
$result = array();
$chunks = explode("\n\n", trim($log));
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;
default:
throw new Exception("Unknown Mercurial log field '{$name}'!");
}
}
$result[] = $commit;
}
return $result;
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'parser/diff');
phutil_require_module('arcanist', 'parser/diff/changetype');
phutil_require_module('arcanist', 'repository/api/base');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistMercurialAPI.php');

View file

@ -478,4 +478,8 @@ EODIFF;
return $info['Repository UUID'];
}
public function getLocalCommitInformation() {
return null;
}
}

View file

@ -222,6 +222,7 @@ class ArcanistBaseWorkflow {
} catch (ConduitClientException $ex) {
if ($ex->getErrorCode() == 'ERR-NO-CERTIFICATE' ||
$ex->getErrorCode() == 'ERR-INVALID-USER') {
$conduit_uri = $this->conduitURI;
$message =
"\n".
phutil_console_format(
@ -597,6 +598,10 @@ class ArcanistBaseWorkflow {
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?";
@ -919,4 +924,15 @@ class ArcanistBaseWorkflow {
return $user_config;
}
/**
* Write a message to stderr so that '--json' flags or stdout which is meant
* to be piped somewhere aren't disrupted.
*
* @param string Message to write to stderr.
* @return void
*/
protected function writeStatusMessage($msg) {
file_put_contents('php://stderr', $msg);
}
}

View file

@ -280,8 +280,12 @@ EOTEXT
if ($info['uuid']) {
$repo_uuid = $info['uuid'];
}
} else {
} else if ($repository_api instanceof ArcanistSubversionAPI) {
$repo_uuid = $repository_api->getRepositorySVNUUID();
} else if ($repository_api instanceof ArcanistMercurialAPI) {
// TODO: Provide this information.
} else {
throw new Exception("Unsupported repository API!");
}
$working_copy = $this->getWorkingCopy();
@ -335,6 +339,17 @@ EOTEXT
));
}
$local_info = $repository_api->getLocalCommitInformation();
if ($local_info) {
$conduit->callMethodSynchronous(
'differential.setdiffproperty',
array(
'diff_id' => $diff_info['diffid'],
'name' => 'local:commits',
'data' => json_encode($local_info),
));
}
if ($this->unresolvedTests) {
$data = array();
foreach ($this->unresolvedTests as $test) {
@ -527,10 +542,18 @@ EOTEXT
}
protected function shouldOnlyCreateDiff() {
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) {
return true;
}
if ($repository_api instanceof ArcanistMercurialAPI) {
// TODO: This is unlikely to be correct since it excludes using local
// branching in Mercurial.
return true;
}
return $this->getArgument('preview') ||
$this->getArgument('only');
}
@ -580,11 +603,19 @@ EOTEXT
}
}
} else {
} else if ($repository_api instanceof ArcanistGitAPI) {
$this->parseGitRelativeCommit(
$repository_api,
$this->getArgument('paths', array()));
$paths = $repository_api->getWorkingCopyStatus();
} else if ($repository_api instanceof ArcanistMercurialAPI) {
// TODO: Unify this and the previous block.
// TODO: Parse the relative commit.
$paths = $repository_api->getWorkingCopyStatus();
} else {
throw new Exception("Unknown VCS!");
}
foreach ($paths as $path => $mask) {
@ -669,6 +700,9 @@ EOTEXT
}
$changes = $parser->parseDiff($diff);
} else if ($repository_api instanceof ArcanistMercurialAPI) {
$diff = $repository_api->getFullMercurialDiff();
$changes = $parser->parseDiff($diff);
} else {
throw new Exception("Repository API is not supported.");
}
@ -819,12 +853,6 @@ EOTEXT
$mime_type = trim($mime_type);
$result['mime'] = $mime_type;
// TODO: Make this configurable.
$bin_limit = 1024 * 1024; // 1 MB limit
if (strlen($data) > $bin_limit) {
return $result;
}
$bytes = strlen($data);
echo "Uploading {$desc} '{$name}' ({$mime_type}, {$bytes} bytes)...\n";

View file

@ -0,0 +1,120 @@
<?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.
*/
/**
* Download a file from Phabricator.
*
* @group workflow
*/
final class ArcanistDownloadWorkflow extends ArcanistBaseWorkflow {
private $id;
private $saveAs;
private $show;
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**download** __file__ [--as __name__] [--show]
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;
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'workflow/base');
phutil_require_module('phutil', 'console');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistDownloadWorkflow.php');

View file

@ -34,7 +34,7 @@ class ArcanistLiberateWorkflow extends ArcanistBaseWorkflow {
**liberate** [__path__]
Supports: libphutil
Create or update a libphutil library, generating required metadata
files like __init__.php.
files like \__init__.php.
EOTEXT
);
}

View file

@ -0,0 +1,159 @@
<?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.
*/
/**
* 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 getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**paste** [--title __title__] [--lang __language__] [--json]
**paste** __id__ [--json]
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.info',
array(
'paste_id' => $this->id,
));
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;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'workflow/base');
phutil_require_module('phutil', 'console');
phutil_require_source('ArcanistPasteWorkflow.php');

View file

@ -0,0 +1,115 @@
<?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.
*/
/**
* Upload a file to Phabricator.
*
* @group workflow
*/
final class ArcanistUploadWorkflow extends ArcanistBaseWorkflow {
private $paths;
private $json;
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**upload** __file__ [__file__ ...] [--json]
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()) {
echo " {$name}: ".$info['uri']."\n\n";
}
}
if ($this->getJSON()) {
echo json_encode($results)."\n";
} else {
$this->writeStatusMessage("Done.\n");
}
return 0;
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'workflow/base');
phutil_require_module('phutil', 'console');
phutil_require_module('phutil', 'filesystem');
phutil_require_source('ArcanistUploadWorkflow.php');