1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2025-01-24 21:48:20 +01:00

(stable) Promote 2020 Week 16

This commit is contained in:
epriestley 2020-04-24 08:40:47 -07:00
commit acf38083f7
730 changed files with 176111 additions and 4336 deletions

View file

@ -36,12 +36,34 @@
"exclude": "(^resources/spelling/.*\\.json$)"
},
"text": {
"type": "text"
"type": "text",
"exclude": [
"(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json|expect)|/Makefile\\z)"
]
},
"text-without-length": {
"type": "text",
"include": [
"(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json|expect))"
],
"severity": {
"3": "disabled"
}
},
"text-without-tabs": {
"type": "text",
"include": [
"(/Makefile\\z)"
],
"severity": {
"2": "disabled"
}
},
"xhpast": {
"type": "xhpast",
"include": "(\\.php$)",
"standard": "phutil.xhpast"
"standard": "phutil.xhpast",
"xhpast.php-version": "5.5.0"
}
}
}

22
.gitignore vendored
View file

@ -11,3 +11,25 @@
# User extensions
/externals/includes/*
/src/extensions/*
# XHPAST
/support/xhpast/*.a
/support/xhpast/*.o
/support/xhpast/parser.yacc.output
/support/xhpast/node_names.hpp
/support/xhpast/xhpast
/support/xhpast/xhpast.exe
/src/parser/xhpast/bin/xhpast
## NOTE: Don't .gitignore these files! Even though they're build artifacts, we
## want to check them in so users can build xhpast without flex/bison.
# /support/xhpast/parser.yacc.cpp
# /support/xhpast/parser.yacc.hpp
# /support/xhpast/scanner.lex.cpp
# /support/xhpast/scanner.lex.hpp
# This is an OS X build artifact.
/support/xhpast/xhpast.dSYM
# Generated shell completion rulesets.
/support/shell/rules/

27
bin/arc
View file

@ -1,21 +1,10 @@
#!/usr/bin/env bash
#!/usr/bin/env php
<?php
# NOTE: This file is a wrapper script instead of a symlink so it will work in
# the Git Bash environment in Windows.
if (function_exists('pcntl_async_signals')) {
pcntl_async_signals(true);
} else {
declare(ticks = 1);
}
# Do bash magic to resolve the real location of this script through aliases,
# symlinks, etc.
SOURCE="${BASH_SOURCE[0]}";
while [ -h "$SOURCE" ]; do
LINK="$(readlink "$SOURCE")";
if [ "${LINK:0:1}" == "/" ]; then
# absolute symlink
SOURCE="$LINK"
else
# relative symlink
SOURCE="$(cd -P "$(dirname "$SOURCE")" && pwd)/$LINK"
fi
done;
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
exec "$DIR/../scripts/arcanist.php" "$@"
require_once dirname(__DIR__).'/support/init/init-arcanist.php';

View file

@ -1,2 +1,2 @@
@echo off
php -f "%~dp0..\scripts\arcanist.php" -- %*
php -f "%~dp0..\bin\arc" -- %*

10
bin/phage Executable file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env php
<?php
if (function_exists('pcntl_async_signals')) {
pcntl_async_signals(true);
} else {
declare(ticks = 1);
}
require_once dirname(__DIR__).'/support/init/init-arcanist.php';

19
externals/jsonlint/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2011 Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,488 @@
<?php
/*
* This file is part of the JSON Lint package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Parser class
*
* Example:
*
* $parser = new JsonParser();
* // returns null if it's valid json, or an error object
* $parser->lint($json);
* // returns parsed json, like json_decode does, but slower, throws exceptions on failure.
* $parser->parse($json);
*
* Ported from https://github.com/zaach/jsonlint
*/
class JsonLintJsonParser
{
const DETECT_KEY_CONFLICTS = 1;
const ALLOW_DUPLICATE_KEYS = 2;
const PARSE_TO_ASSOC = 4;
private $lexer;
private $flags;
private $stack;
private $vstack; // semantic value stack
private $lstack; // location stack
private $symbols = array(
'error' => 2,
'JSONString' => 3,
'STRING' => 4,
'JSONNumber' => 5,
'NUMBER' => 6,
'JSONNullLiteral' => 7,
'NULL' => 8,
'JSONBooleanLiteral' => 9,
'TRUE' => 10,
'FALSE' => 11,
'JSONText' => 12,
'JSONValue' => 13,
'EOF' => 14,
'JSONObject' => 15,
'JSONArray' => 16,
'{' => 17,
'}' => 18,
'JSONMemberList' => 19,
'JSONMember' => 20,
':' => 21,
',' => 22,
'[' => 23,
']' => 24,
'JSONElementList' => 25,
'$accept' => 0,
'$end' => 1,
);
private $terminals_ = array(
2 => "error",
4 => "STRING",
6 => "NUMBER",
8 => "NULL",
10 => "TRUE",
11 => "FALSE",
14 => "EOF",
17 => "{",
18 => "}",
21 => ":",
22 => ",",
23 => "[",
24 => "]",
);
private $productions_ = array(
0,
array(3, 1),
array(5, 1),
array(7, 1),
array(9, 1),
array(9, 1),
array(12, 2),
array(13, 1),
array(13, 1),
array(13, 1),
array(13, 1),
array(13, 1),
array(13, 1),
array(15, 2),
array(15, 3),
array(20, 3),
array(19, 1),
array(19, 3),
array(16, 2),
array(16, 3),
array(25, 1),
array(25, 3)
);
private $table = array(array(3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 12 => 1, 13 => 2, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 1 => array(3)), array( 14 => array(1,16)), array( 14 => array(2,7), 18 => array(2,7), 22 => array(2,7), 24 => array(2,7)), array( 14 => array(2,8), 18 => array(2,8), 22 => array(2,8), 24 => array(2,8)), array( 14 => array(2,9), 18 => array(2,9), 22 => array(2,9), 24 => array(2,9)), array( 14 => array(2,10), 18 => array(2,10), 22 => array(2,10), 24 => array(2,10)), array( 14 => array(2,11), 18 => array(2,11), 22 => array(2,11), 24 => array(2,11)), array( 14 => array(2,12), 18 => array(2,12), 22 => array(2,12), 24 => array(2,12)), array( 14 => array(2,3), 18 => array(2,3), 22 => array(2,3), 24 => array(2,3)), array( 14 => array(2,4), 18 => array(2,4), 22 => array(2,4), 24 => array(2,4)), array( 14 => array(2,5), 18 => array(2,5), 22 => array(2,5), 24 => array(2,5)), array( 14 => array(2,1), 18 => array(2,1), 21 => array(2,1), 22 => array(2,1), 24 => array(2,1)), array( 14 => array(2,2), 18 => array(2,2), 22 => array(2,2), 24 => array(2,2)), array( 3 => 20, 4 => array(1,12), 18 => array(1,17), 19 => 18, 20 => 19 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 23, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15), 24 => array(1,21), 25 => 22 ), array( 1 => array(2,6)), array( 14 => array(2,13), 18 => array(2,13), 22 => array(2,13), 24 => array(2,13)), array( 18 => array(1,24), 22 => array(1,25)), array( 18 => array(2,16), 22 => array(2,16)), array( 21 => array(1,26)), array( 14 => array(2,18), 18 => array(2,18), 22 => array(2,18), 24 => array(2,18)), array( 22 => array(1,28), 24 => array(1,27)), array( 22 => array(2,20), 24 => array(2,20)), array( 14 => array(2,14), 18 => array(2,14), 22 => array(2,14), 24 => array(2,14)), array( 3 => 20, 4 => array(1,12), 20 => 29 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 30, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 14 => array(2,19), 18 => array(2,19), 22 => array(2,19), 24 => array(2,19)), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 31, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 18 => array(2,17), 22 => array(2,17)), array( 18 => array(2,15), 22 => array(2,15)), array( 22 => array(2,21), 24 => array(2,21)),
);
private $defaultActions = array(
16 => array(2, 6)
);
/**
* @param string $input JSON string
* @return null|JsonLintParsingException null if no error is found, a JsonLintParsingException containing all details otherwise
*/
public function lint($input)
{
try {
$this->parse($input);
} catch (JsonLintParsingException $e) {
return $e;
}
}
/**
* @param string $input JSON string
* @return mixed
* @throws JsonLintParsingException
*/
public function parse($input, $flags = 0)
{
$this->failOnBOM($input);
$this->flags = $flags;
$this->stack = array(0);
$this->vstack = array(null);
$this->lstack = array();
$yytext = '';
$yylineno = 0;
$yyleng = 0;
$recovering = 0;
$TERROR = 2;
$EOF = 1;
$this->lexer = new JsonLintLexer();
$this->lexer->setInput($input);
$yyloc = $this->lexer->yylloc;
$this->lstack[] = $yyloc;
$symbol = null;
$preErrorSymbol = null;
$state = null;
$action = null;
$a = null;
$r = null;
$yyval = new stdClass;
$p = null;
$len = null;
$newState = null;
$expected = null;
$errStr = null;
while (true) {
// retrieve state number from top of stack
$state = $this->stack[count($this->stack)-1];
// use default actions if available
if (isset($this->defaultActions[$state])) {
$action = $this->defaultActions[$state];
} else {
if ($symbol == null) {
$symbol = $this->lex();
}
// read action for current state and first input
$action = isset($this->table[$state][$symbol]) ? $this->table[$state][$symbol] : false;
}
// handle parse error
if (!$action || !$action[0]) {
if (!$recovering) {
// Report error
$expected = array();
foreach ($this->table[$state] as $p => $ignore) {
if (isset($this->terminals_[$p]) && $p > 2) {
$expected[] = "'" . $this->terminals_[$p] . "'";
}
}
$message = null;
if (in_array("'STRING'", $expected) && in_array(substr($this->lexer->match, 0, 1), array('"', "'"))) {
$message = "Invalid string";
if ("'" === substr($this->lexer->match, 0, 1)) {
$message .= ", it appears you used single quotes instead of double quotes";
} elseif (preg_match('{".+?(\\\\[^"bfnrt/\\\\u])}', $this->lexer->getUpcomingInput(), $match)) {
$message .= ", it appears you have an unescaped backslash at: ".$match[1];
} elseif (preg_match('{"(?:[^"]+|\\\\")*$}m', $this->lexer->getUpcomingInput())) {
$message .= ", it appears you forgot to terminate the string, or attempted to write a multiline string which is invalid";
}
}
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
$errStr .= $this->lexer->showPosition() . "\n";
if ($message) {
$errStr .= $message;
} else {
$errStr .= (count($expected) > 1) ? "Expected one of: " : "Expected: ";
$errStr .= implode(', ', $expected);
}
if (',' === substr(trim($this->lexer->getPastInput()), -1)) {
$errStr .= " - It appears you have an extra trailing comma";
}
$this->parseError($errStr, array(
'text' => $this->lexer->match,
'token' => !empty($this->terminals_[$symbol]) ? $this->terminals_[$symbol] : $symbol,
'line' => $this->lexer->yylineno,
'loc' => $yyloc,
'expected' => $expected,
));
}
// just recovered from another error
if ($recovering == 3) {
if ($symbol == $EOF) {
throw new JsonLintParsingException($errStr ? $errStr : 'Parsing halted.');
}
// discard current lookahead and grab another
$yyleng = $this->lexer->yyleng;
$yytext = $this->lexer->yytext;
$yylineno = $this->lexer->yylineno;
$yyloc = $this->lexer->yylloc;
$symbol = $this->lex();
}
// try to recover from error
while (true) {
// check for error recovery rule in this state
if (array_key_exists($TERROR, $this->table[$state])) {
break;
}
if ($state == 0) {
throw new JsonLintParsingException($errStr ? $errStr : 'Parsing halted.');
}
$this->popStack(1);
$state = $this->stack[count($this->stack)-1];
}
$preErrorSymbol = $symbol; // save the lookahead token
$symbol = $TERROR; // insert generic error symbol as new lookahead
$state = $this->stack[count($this->stack)-1];
$action = isset($this->table[$state][$TERROR]) ? $this->table[$state][$TERROR] : false;
$recovering = 3; // allow 3 real symbols to be shifted before reporting a new error
}
// this shouldn't happen, unless resolve defaults are off
if (is_array($action[0]) && count($action) > 1) {
throw new JsonLintParsingException('Parse Error: multiple actions possible at state: ' . $state . ', token: ' . $symbol);
}
switch ($action[0]) {
case 1: // shift
$this->stack[] = $symbol;
$this->vstack[] = $this->lexer->yytext;
$this->lstack[] = $this->lexer->yylloc;
$this->stack[] = $action[1]; // push state
$symbol = null;
if (!$preErrorSymbol) { // normal execution/no error
$yyleng = $this->lexer->yyleng;
$yytext = $this->lexer->yytext;
$yylineno = $this->lexer->yylineno;
$yyloc = $this->lexer->yylloc;
if ($recovering > 0) {
$recovering--;
}
} else { // error just occurred, resume old lookahead f/ before error
$symbol = $preErrorSymbol;
$preErrorSymbol = null;
}
break;
case 2: // reduce
$len = $this->productions_[$action[1]][1];
// perform semantic action
$yyval->token = $this->vstack[count($this->vstack) - $len]; // default to $$ = $1
// default location, uses first token for firsts, last for lasts
$yyval->store = array( // _$ = store
'first_line' => $this->lstack[count($this->lstack) - ($len ? $len : 1)]['first_line'],
'last_line' => $this->lstack[count($this->lstack) - 1]['last_line'],
'first_column' => $this->lstack[count($this->lstack) - ($len ? $len : 1)]['first_column'],
'last_column' => $this->lstack[count($this->lstack) - 1]['last_column'],
);
$r = $this->performAction($yyval, $yytext, $yyleng, $yylineno, $action[1], $this->vstack, $this->lstack);
if (!$r instanceof JsonLintUndefined) {
return $r;
}
if ($len) {
$this->popStack($len);
}
$this->stack[] = $this->productions_[$action[1]][0]; // push nonterminal (reduce)
$this->vstack[] = $yyval->token;
$this->lstack[] = $yyval->store;
$newState = $this->table[$this->stack[count($this->stack)-2]][$this->stack[count($this->stack)-1]];
$this->stack[] = $newState;
break;
case 3: // accept
return true;
}
}
return true;
}
protected function parseError($str, $hash)
{
throw new JsonLintParsingException($str, $hash);
}
// $$ = $tokens // needs to be passed by ref?
// $ = $token
// _$ removed, useless?
private function performAction(stdClass $yyval, $yytext, $yyleng, $yylineno, $yystate, &$tokens)
{
// $0 = $len
$len = count($tokens) - 1;
switch ($yystate) {
case 1:
$yytext = preg_replace_callback('{(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4})}', array($this, 'stringInterpolation'), $yytext);
$yyval->token = $yytext;
break;
case 2:
if (strpos($yytext, 'e') !== false || strpos($yytext, 'E') !== false) {
$yyval->token = floatval($yytext);
} else {
$yyval->token = strpos($yytext, '.') === false ? intval($yytext) : floatval($yytext);
}
break;
case 3:
$yyval->token = null;
break;
case 4:
$yyval->token = true;
break;
case 5:
$yyval->token = false;
break;
case 6:
return $yyval->token = $tokens[$len-1];
case 13:
if ($this->flags & self::PARSE_TO_ASSOC) {
$yyval->token = array();
} else {
$yyval->token = new stdClass;
}
break;
case 14:
$yyval->token = $tokens[$len-1];
break;
case 15:
$yyval->token = array($tokens[$len-2], $tokens[$len]);
break;
case 16:
$property = $tokens[$len][0];
if ($this->flags & self::PARSE_TO_ASSOC) {
$yyval->token = array();
$yyval->token[$property] = $tokens[$len][1];
} else {
$yyval->token = new stdClass;
$yyval->token->$property = $tokens[$len][1];
}
break;
case 17:
if ($this->flags & self::PARSE_TO_ASSOC) {
$yyval->token =& $tokens[$len-2];
$key = $tokens[$len][0];
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2][$key])) {
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
$errStr .= $this->lexer->showPosition() . "\n";
$errStr .= "Duplicate key: ".$tokens[$len][0];
throw new JsonLintParsingException($errStr);
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2][$key])) {
// Forget about it...
}
$tokens[$len-2][$key] = $tokens[$len][1];
} else {
$yyval->token = $tokens[$len-2];
$key = $tokens[$len][0];
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2]->{$key})) {
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
$errStr .= $this->lexer->showPosition() . "\n";
$errStr .= "Duplicate key: ".$tokens[$len][0];
throw new JsonLintParsingException($errStr);
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2]->{$key})) {
$duplicateCount = 1;
do {
$duplicateKey = $key . '.' . $duplicateCount++;
} while (isset($tokens[$len-2]->$duplicateKey));
$key = $duplicateKey;
}
$tokens[$len-2]->$key = $tokens[$len][1];
}
break;
case 18:
$yyval->token = array();
break;
case 19:
$yyval->token = $tokens[$len-1];
break;
case 20:
$yyval->token = array($tokens[$len]);
break;
case 21:
$tokens[$len-2][] = $tokens[$len];
$yyval->token = $tokens[$len-2];
break;
}
return new JsonLintUndefined();
}
private function stringInterpolation($match)
{
switch ($match[0]) {
case '\\\\':
return '\\';
case '\"':
return '"';
case '\b':
return chr(8);
case '\f':
return chr(12);
case '\n':
return "\n";
case '\r':
return "\r";
case '\t':
return "\t";
case '\/':
return "/";
default:
return html_entity_decode('&#x'.ltrim(substr($match[0], 2), '0').';', 0, 'UTF-8');
}
}
private function popStack($n)
{
$this->stack = array_slice($this->stack, 0, - (2 * $n));
$this->vstack = array_slice($this->vstack, 0, - $n);
$this->lstack = array_slice($this->lstack, 0, - $n);
}
private function lex()
{
$token = $this->lexer->lex();
if (!$token) {
$token = 1;
}
// if token isn't its numeric value, convert
if (!is_numeric($token)) {
$token = isset($this->symbols[$token]) ? $this->symbols[$token] : $token;
}
return $token;
}
private function failOnBOM($input)
{
// UTF-8 ByteOrderMark sequence
$bom = "\xEF\xBB\xBF";
if (substr($input, 0, 3) === $bom) {
$this->parseError("BOM detected, make sure your input does not include a Unicode Byte-Order-Mark", array());
}
}
}

View file

@ -0,0 +1,215 @@
<?php
/*
* This file is part of the JSON Lint package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Lexer class
*
* Ported from https://github.com/zaach/jsonlint
*/
class JsonLintLexer
{
private $EOF = 1;
private $rules = array(
0 => '/^\s+/',
1 => '/^-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][+-]?[0-9]+)?\b/',
2 => '{^"(?>\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x1f\\\\"]++)*+"}',
3 => '/^\{/',
4 => '/^\}/',
5 => '/^\[/',
6 => '/^\]/',
7 => '/^,/',
8 => '/^:/',
9 => '/^true\b/',
10 => '/^false\b/',
11 => '/^null\b/',
12 => '/^$/',
13 => '/^./',
);
private $conditions = array(
"INITIAL" => array(
"rules" => array(0,1,2,3,4,5,6,7,8,9,10,11,12,13),
"inclusive" => true,
),
);
private $conditionStack;
private $input;
private $more;
private $done;
private $matched;
public $match;
public $yylineno;
public $yyleng;
public $yytext;
public $yylloc;
public function lex()
{
$r = $this->next();
if (!$r instanceof JsonLintUndefined) {
return $r;
}
return $this->lex();
}
public function setInput($input)
{
$this->input = $input;
$this->more = false;
$this->done = false;
$this->yylineno = $this->yyleng = 0;
$this->yytext = $this->matched = $this->match = '';
$this->conditionStack = array('INITIAL');
$this->yylloc = array('first_line' => 1, 'first_column' => 0, 'last_line' => 1, 'last_column' => 0);
return $this;
}
public function showPosition()
{
$pre = str_replace("\n", '', $this->getPastInput());
$c = str_repeat('-', max(0, strlen($pre) - 1)); // new Array(pre.length + 1).join("-");
return $pre . str_replace("\n", '', $this->getUpcomingInput()) . "\n" . $c . "^";
}
public function getPastInput()
{
$past = substr($this->matched, 0, strlen($this->matched) - strlen($this->match));
return (strlen($past) > 20 ? '...' : '') . substr($past, -20);
}
public function getUpcomingInput()
{
$next = $this->match;
if (strlen($next) < 20) {
$next .= substr($this->input, 0, 20 - strlen($next));
}
return substr($next, 0, 20) . (strlen($next) > 20 ? '...' : '');
}
protected function parseError($str, $hash)
{
throw new Exception($str);
}
private function next()
{
if ($this->done) {
return $this->EOF;
}
if (!$this->input) {
$this->done = true;
}
$token = null;
$match = null;
$col = null;
$lines = null;
if (!$this->more) {
$this->yytext = '';
$this->match = '';
}
$rules = $this->getCurrentRules();
$rulesLen = count($rules);
for ($i=0; $i < $rulesLen; $i++) {
if (preg_match($this->rules[$rules[$i]], $this->input, $match)) {
preg_match_all('/\n.*/', $match[0], $lines);
$lines = $lines[0];
if ($lines) {
$this->yylineno += count($lines);
}
$this->yylloc = array(
'first_line' => $this->yylloc['last_line'],
'last_line' => $this->yylineno+1,
'first_column' => $this->yylloc['last_column'],
'last_column' => $lines ? strlen($lines[count($lines) - 1]) - 1 : $this->yylloc['last_column'] + strlen($match[0]),
);
$this->yytext .= $match[0];
$this->match .= $match[0];
$this->yyleng = strlen($this->yytext);
$this->more = false;
$this->input = substr($this->input, strlen($match[0]));
$this->matched .= $match[0];
$token = $this->performAction($rules[$i], $this->conditionStack[count($this->conditionStack)-1]);
if ($token) {
return $token;
}
return new JsonLintUndefined();
}
}
if ($this->input === "") {
return $this->EOF;
}
$this->parseError(
'Lexical error on line ' . ($this->yylineno+1) . ". Unrecognized text.\n" . $this->showPosition(),
array(
'text' => "",
'token' => null,
'line' => $this->yylineno,
)
);
}
private function getCurrentRules()
{
return $this->conditions[$this->conditionStack[count($this->conditionStack)-1]]['rules'];
}
private function performAction($avoiding_name_collisions, $YY_START)
{
switch ($avoiding_name_collisions) {
case 0:/* skip whitespace */
break;
case 1:
return 6;
break;
case 2:
$this->yytext = substr($this->yytext, 1, $this->yyleng-2);
return 4;
case 3:
return 17;
case 4:
return 18;
case 5:
return 23;
case 6:
return 24;
case 7:
return 22;
case 8:
return 21;
case 9:
return 10;
case 10:
return 11;
case 11:
return 8;
case 12:
return 14;
case 13:
return 'INVALID';
}
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the JSON Lint package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class JsonLintParsingException extends Exception
{
protected $details;
public function __construct($message, $details = array())
{
$this->details = $details;
parent::__construct($message);
}
public function getDetails()
{
return $this->details;
}
}

View file

@ -0,0 +1,14 @@
<?php
/*
* This file is part of the JSON Lint package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class JsonLintUndefined
{
}

File diff suppressed because it is too large Load diff

View file

@ -1,26 +0,0 @@
if [[ -n ${ZSH_VERSION-} ]]; then
autoload -U +X bashcompinit && bashcompinit
fi
_arc ()
{
CUR="${COMP_WORDS[COMP_CWORD]}"
COMPREPLY=()
OPTS=$(echo | arc shell-complete --current ${COMP_CWORD} -- ${COMP_WORDS[@]})
if [ $? -ne 0 ]; then
return $?
fi
if [ "$OPTS" = "FILE" ]; then
COMPREPLY=( $(compgen -f -- ${CUR}) )
return 0
fi
if [ "$OPTS" = "ARGUMENT" ]; then
return 0
fi
COMPREPLY=( $(compgen -W "${OPTS}" -- ${CUR}) )
}
complete -F _arc -o filenames arc

45
resources/ssl/README Normal file
View file

@ -0,0 +1,45 @@
This document describes how to set Certificate Authority information.
Usually, you need to do this only if you're using a self-signed certificate.
OSX after Yosemite
==================
If you're using a version of Mac OSX after Yosemite, you can not configure
certificates from the command line. All libphutil and arcanist options
related to CA configuration are ignored.
Instead, you need to add them to the system keychain. The easiest way to do this
is to visit the site in Safari and choose to permanently accept the certificate.
You can also use `security add-trusted-cert` from the command line.
All Other Systems
=================
If "curl.cainfo" is not set (or you are using PHP older than 5.3.7, where the
option was introduced), libphutil uses the "default.pem" certificate authority
bundle when making HTTPS requests with cURL. This bundle is extracted from
Mozilla's certificates by cURL:
http://curl.haxx.se/docs/caextract.html
If you want to use a different CA bundle (for example, because you use
self-signed certificates), set "curl.cainfo" if you're using PHP 5.3.7 or newer,
or create a file (or symlink) in this directory named "custom.pem".
If "custom.pem" is present, that file will be used instead of "default.pem".
If you receive errors using your "custom.pem" file, you can test it directly
with `curl` by running a command like this:
curl -v --cacert path/to/your/custom.pem https://phabricator.example.com/
Replace "path/to/your/custom.pem" with the path to your "custom.pem" file,
and replace "https://phabricator.example.com" with the real URL of your
Phabricator install.
The initial lines of output from `curl` should give you information about the
SSL handshake and certificate verification, which may be helpful in resolving
the issue.

3893
resources/ssl/default.pem Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,58 +1,3 @@
<?php
/**
* 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();
if (getenv('ARC_PHUTIL_PATH')) {
@include_once getenv('ARC_PHUTIL_PATH').'/scripts/__init_script__.php';
} else {
@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);
}
phutil_load_library(dirname(dirname(__FILE__)).'/src/');
PhutilTranslator::getInstance()
->setLocale(PhutilLocale::loadLocale('en_US'))
->setTranslations(PhutilTranslation::getTranslationMapForLocale('en_US'));
require_once dirname(dirname(__FILE__)).'/support/init/init-script.php';

View file

@ -5,6 +5,43 @@ sanity_check_environment();
require_once dirname(__FILE__).'/__init_script__.php';
/**
* Adjust 'include_path' to add locations where we'll search for libraries.
* We look in these places:
*
* - Next to 'arcanist/'.
* - Anywhere in the normal PHP 'include_path'.
* - Inside 'arcanist/externals/includes/'.
*/
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();
ini_set('memory_limit', -1);
$original_argv = $argv;
@ -18,9 +55,6 @@ $base_args->parsePartial(
'help' => pht('Load a libphutil library.'),
'repeat' => true,
),
array(
'name' => 'skip-arcconfig',
),
array(
'name' => 'arcrc-file',
'param' => 'filename',
@ -36,15 +70,8 @@ $base_args->parsePartial(
'help' => pht('Use a specific authentication token.'),
),
array(
'name' => 'conduit-version',
'param' => 'version',
'help' => pht(
'(Developers) Mock client version in protocol handshake.'),
),
array(
'name' => 'conduit-timeout',
'param' => 'timeout',
'help' => pht('Set Conduit timeout (in seconds).'),
'name' => 'anonymous',
'help' => pht('Run workflow as a public user, without authenticating.'),
),
array(
'name' => 'config',
@ -60,10 +87,8 @@ $config_trace_mode = $base_args->getArg('trace');
$force_conduit = $base_args->getArg('conduit-uri');
$force_token = $base_args->getArg('conduit-token');
$force_conduit_version = $base_args->getArg('conduit-version');
$conduit_timeout = $base_args->getArg('conduit-timeout');
$skip_arcconfig = $base_args->getArg('skip-arcconfig');
$custom_arcrc = $base_args->getArg('arcrc-file');
$is_anonymous = $base_args->getArg('anonymous');
$load = $base_args->getArg('load-phutil-library');
$help = $base_args->getArg('help');
$args = array_values($base_args->getUnconsumedArgumentVector());
@ -81,7 +106,6 @@ try {
csprintf('%Ls', $original_argv));
$libraries = array(
'phutil',
'arcanist',
);
@ -116,12 +140,8 @@ try {
$system_config = $configuration_manager->readSystemArcConfig();
$runtime_config = $configuration_manager->applyRuntimeArcConfig($base_args);
if ($skip_arcconfig) {
$working_copy = ArcanistWorkingCopyIdentity::newDummyWorkingCopy();
} else {
$working_copy =
ArcanistWorkingCopyIdentity::newFromPath($working_directory);
}
$working_copy =
ArcanistWorkingCopyIdentity::newFromPath($working_directory);
$configuration_manager->setWorkingCopyIdentity($working_copy);
// Load additional libraries, which can provide new classes like configuration
@ -180,13 +200,7 @@ try {
}
$user_config = $configuration_manager->readUserConfigurationFile();
$config_class = $working_copy->getProjectConfig('arcanist_configuration');
if ($config_class) {
$config = new $config_class();
} else {
$config = new ArcanistConfiguration();
}
$config = new ArcanistConfiguration();
$command = strtolower($args[0]);
$args = array_slice($args, 1);
@ -206,13 +220,6 @@ try {
// their behaviors.
putenv('ARCANIST='.$command);
if ($force_conduit_version) {
$workflow->forceConduitVersion($force_conduit_version);
}
if ($conduit_timeout) {
$workflow->setConduitTimeout($conduit_timeout);
}
$need_working_copy = $workflow->requiresWorkingCopy();
$supported_vcs_types = $workflow->getSupportedRevisionControlSystems();
@ -324,6 +331,10 @@ try {
$conduit_token = $force_token;
}
if ($is_anonymous) {
$conduit_token = null;
}
$description = implode(' ', $original_argv);
$credentials = array(
'user' => $user_name,
@ -333,6 +344,12 @@ try {
);
$workflow->setConduitCredentials($credentials);
$engine = id(new ArcanistConduitEngine())
->setConduitURI($conduit_uri)
->setConduitToken($conduit_token);
$workflow->setConduitEngine($engine);
if ($need_auth) {
if ((!$user_name || !$certificate) && (!$conduit_token)) {
$arc = 'arc';
@ -409,7 +426,7 @@ try {
fwrite(STDERR, phutil_console_format(
"**%s** %s\n",
pht('Usage Exception:'),
$ex->getMessage()));
rtrim($ex->getMessage())));
}
if ($config) {

View file

@ -1,3 +1,3 @@
<?php
phutil_register_library('arcanist', __FILE__);
require_once dirname(__FILE__).'/init/init-library.php';

File diff suppressed because it is too large Load diff

View file

@ -1,3 +0,0 @@
<?php
final class ArcanistLibraryTestCase extends PhutilLibraryTestCase {}

View file

@ -0,0 +1,191 @@
<?php
/**
* @concrete-extensible
*/
class PhutilLibraryTestCase extends PhutilTestCase {
/**
* This is more of an acceptance test case instead of a unit test. It verifies
* that all symbols can be loaded correctly. It can catch problems like
* missing methods in descendants of abstract base classes.
*/
public function testEverythingImplemented() {
id(new PhutilSymbolLoader())
->setLibrary($this->getLibraryName())
->selectAndLoadSymbols();
$this->assertTrue(true);
}
/**
* This is more of an acceptance test case instead of a unit test. It verifies
* that all the library map is up-to-date.
*/
public function testLibraryMap() {
$root = $this->getLibraryRoot();
$library = phutil_get_library_name_for_root($root);
$new_library_map = id(new PhutilLibraryMapBuilder($root))
->buildMap();
$bootloader = PhutilBootloader::getInstance();
$old_library_map = $bootloader->getLibraryMapWithoutExtensions($library);
unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]);
$identical = ($new_library_map === $old_library_map);
if (!$identical) {
$differences = $this->getMapDifferences(
$old_library_map,
$new_library_map);
sort($differences);
} else {
$differences = array();
}
$this->assertTrue(
$identical,
pht(
"The library map is out of date. Rebuild it with `%s`.\n".
"These entries differ: %s.",
'arc liberate',
implode(', ', $differences)));
}
private function getMapDifferences($old, $new) {
$changed = array();
$all = $old + $new;
foreach ($all as $key => $value) {
$old_exists = array_key_exists($key, $old);
$new_exists = array_key_exists($key, $new);
// One map has it and the other does not, so mark it as changed.
if ($old_exists != $new_exists) {
$changed[] = $key;
continue;
}
$oldv = idx($old, $key);
$newv = idx($new, $key);
if ($oldv === $newv) {
continue;
}
if (is_array($oldv) && is_array($newv)) {
$child_changed = $this->getMapDifferences($oldv, $newv);
foreach ($child_changed as $child) {
$changed[] = $key.'.'.$child;
}
} else {
$changed[] = $key;
}
}
return $changed;
}
/**
* This is more of an acceptance test case instead of a unit test. It verifies
* that methods in subclasses have the same visibility as the method in the
* parent class.
*/
public function testMethodVisibility() {
$symbols = id(new PhutilSymbolLoader())
->setLibrary($this->getLibraryName())
->selectSymbolsWithoutLoading();
$classes = array();
foreach ($symbols as $symbol) {
if ($symbol['type'] == 'class') {
$classes[$symbol['name']] = new ReflectionClass($symbol['name']);
}
}
$failures = array();
foreach ($classes as $class_name => $class) {
$parents = array();
$parent = $class;
while ($parent = $parent->getParentClass()) {
$parents[] = $parent;
}
$interfaces = $class->getInterfaces();
foreach ($class->getMethods() as $method) {
$method_name = $method->getName();
foreach (array_merge($parents, $interfaces) as $extends) {
if ($extends->hasMethod($method_name)) {
$xmethod = $extends->getMethod($method_name);
if (!$this->compareVisibility($xmethod, $method)) {
$failures[] = pht(
'Class "%s" implements method "%s" with the wrong visibility. '.
'The method has visibility "%s", but it is defined in parent '.
'"%s" with visibility "%s". In Phabricator, a method which '.
'overrides another must always have the same visibility.',
$class_name,
$method_name,
$this->getVisibility($method),
$extends->getName(),
$this->getVisibility($xmethod));
}
// We found a declaration somewhere, so stop looking.
break;
}
}
}
}
$this->assertTrue(
empty($failures),
"\n\n".implode("\n\n", $failures));
}
/**
* Get the name of the library currently being tested.
*/
protected function getLibraryName() {
return phutil_get_library_name_for_root($this->getLibraryRoot());
}
/**
* Get the root directory for the library currently being tested.
*/
protected function getLibraryRoot() {
$caller = id(new ReflectionClass($this))->getFileName();
return phutil_get_library_root_for_path($caller);
}
private function compareVisibility(
ReflectionMethod $parent_method,
ReflectionMethod $method) {
static $bitmask;
if ($bitmask === null) {
$bitmask = ReflectionMethod::IS_PUBLIC;
$bitmask += ReflectionMethod::IS_PROTECTED;
$bitmask += ReflectionMethod::IS_PRIVATE;
}
$parent_modifiers = $parent_method->getModifiers();
$modifiers = $method->getModifiers();
return !(($parent_modifiers ^ $modifiers) & $bitmask);
}
private function getVisibility(ReflectionMethod $method) {
if ($method->isPrivate()) {
return 'private';
} else if ($method->isProtected()) {
return 'protected';
} else {
return 'public';
}
}
}

View file

@ -0,0 +1,65 @@
<?php
final class ArcanistBrowseCommitHardpointQuery
extends ArcanistRuntimeHardpointQuery {
public function getHardpoints() {
return array(
ArcanistBrowseRef::HARDPOINT_COMMITREFS,
);
}
protected function canLoadRef(ArcanistRef $ref) {
return ($ref instanceof ArcanistBrowseRef);
}
public function loadHardpoint(array $refs, $hardpoint) {
$api = $this->getRepositoryAPI();
$commit_map = array();
foreach ($refs as $key => $ref) {
$token = $ref->getToken();
if ($token === '.') {
// Git resolves "." like HEAD, but we want to treat it as "browse the
// current directory" instead in all cases.
continue;
}
// Always resolve the empty token; top-level loaders filter out
// irrelevant tokens before this stage.
if ($token === null) {
$token = $api->getHeadCommit();
}
// TODO: We should pull a full commit ref out of the API as soon as it
// is able to provide them. In particular, we currently miss Git tree
// hashes which reduces the accuracy of lookups.
try {
$commit = $api->getCanonicalRevisionName($token);
if ($commit) {
$commit_map[$commit][] = $key;
}
} catch (Exception $ex) {
// Ignore anything we can't resolve.
}
}
if (!$commit_map) {
yield $this->yieldMap(array());
}
$results = array();
foreach ($commit_map as $commit_identifier => $ref_keys) {
foreach ($ref_keys as $key) {
$commit_ref = id(new ArcanistCommitRef())
->setCommitHash($commit_identifier);
$results[$key][] = $commit_ref;
}
}
yield $this->yieldMap($results);
}
}

View file

@ -0,0 +1,49 @@
<?php
final class ArcanistBrowseCommitURIHardpointQuery
extends ArcanistBrowseURIHardpointQuery {
const BROWSETYPE = 'commit';
public function loadHardpoint(array $refs, $hardpoint) {
$refs = $this->getRefsWithSupportedTypes($refs);
if (!$refs) {
yield $this->yieldMap(array());
}
yield $this->yieldRequests(
$refs,
array(
ArcanistBrowseRef::HARDPOINT_COMMITREFS,
));
$commit_refs = array();
foreach ($refs as $key => $ref) {
foreach ($ref->getCommitRefs() as $commit_ref) {
$commit_refs[] = $commit_ref;
}
}
yield $this->yieldRequests(
$commit_refs,
array(
ArcanistCommitRef::HARDPOINT_UPSTREAM,
));
$results = array();
foreach ($refs as $key => $ref) {
$commit_refs = $ref->getCommitRefs();
foreach ($commit_refs as $commit_ref) {
$uri = $commit_ref->getURI();
if ($uri !== null) {
$results[$key][] = $this->newBrowseURIRef()
->setURI($uri);
}
}
}
yield $this->yieldMap($results);
}
}

View file

@ -0,0 +1,56 @@
<?php
final class ArcanistBrowseObjectNameURIHardpointQuery
extends ArcanistBrowseURIHardpointQuery {
const BROWSETYPE = 'object';
public function loadHardpoint(array $refs, $hardpoint) {
$refs = $this->getRefsWithSupportedTypes($refs);
if (!$refs) {
yield $this->yieldMap(array());
}
$name_map = array();
$token_set = array();
foreach ($refs as $key => $ref) {
$token = $ref->getToken();
if (!strlen($token)) {
continue;
}
$name_map[$key] = $token;
$token_set[$token] = $token;
}
if (!$token_set) {
yield $this->yieldMap(array());
}
$objects = (yield $this->yieldConduit(
'phid.lookup',
array(
'names' => $token_set,
)));
$result = array();
foreach ($name_map as $ref_key => $token) {
$object = idx($objects, $token);
if (!$object) {
continue;
}
$uri = idx($object, 'uri');
if (!strlen($uri)) {
continue;
}
$result[$ref_key][] = $this->newBrowseURIRef()
->setURI($object['uri']);
}
yield $this->yieldMap($result);
}
}

View file

@ -0,0 +1,74 @@
<?php
final class ArcanistBrowsePathURIHardpointQuery
extends ArcanistBrowseURIHardpointQuery {
const BROWSETYPE = 'path';
public function loadHardpoint(array $refs, $hardpoint) {
$refs = $this->getRefsWithSupportedTypes($refs);
if (!$refs) {
yield $this->yieldMap(array());
}
$repository_ref = (yield $this->yieldRepositoryRef());
if (!$repository_ref) {
yield $this->yieldMap(array());
}
$working_copy = $this->getWorkingCopy();
$working_root = $working_copy->getPath();
$results = array();
foreach ($refs as $key => $ref) {
$is_path = $ref->hasType(self::BROWSETYPE);
$path = $ref->getToken();
if ($path === null) {
// If we're explicitly resolving no arguments as a path, treat it
// as the current working directory.
if ($is_path) {
$path = '.';
} else {
continue;
}
}
$lines = null;
$parts = explode(':', $path);
if (count($parts) > 1) {
$lines = array_pop($parts);
}
$path = implode(':', $parts);
$full_path = Filesystem::resolvePath($path);
if (!Filesystem::pathExists($full_path)) {
if (!$is_path) {
continue;
}
}
if ($full_path == $working_root) {
$path = '';
} else {
$path = Filesystem::readablePath($full_path, $working_root);
}
$params = array(
'path' => $path,
'lines' => $lines,
'branch' => $ref->getBranch(),
);
$uri = $repository_ref->newBrowseURI($params);
$results[$key][] = $this->newBrowseURIRef()
->setURI($uri);
}
yield $this->yieldMap($results);
}
}

View file

@ -0,0 +1,62 @@
<?php
final class ArcanistBrowseRevisionURIHardpointQuery
extends ArcanistBrowseURIHardpointQuery {
const BROWSETYPE = 'revision';
public function loadHardpoint(array $refs, $hardpoint) {
$refs = $this->getRefsWithSupportedTypes($refs);
if (!$refs) {
yield $this->yieldMap(array());
}
yield $this->yieldRequests(
$refs,
array(
ArcanistBrowseRef::HARDPOINT_COMMITREFS,
));
$states = array();
$map = array();
foreach ($refs as $key => $ref) {
foreach ($ref->getCommitRefs() as $commit_ref) {
$hash = $commit_ref->getCommitHash();
$states[$hash] = id(new ArcanistWorkingCopyStateRef())
->setCommitRef($commit_ref);
$map[$hash][] = $key;
}
}
if (!$states) {
yield $this->yieldMap(array());
}
yield $this->yieldRequests(
$states,
array(
'revisionRefs',
));
$results = array();
foreach ($states as $hash => $state) {
foreach ($state->getRevisionRefs() as $revision) {
if ($revision->isClosed()) {
// Don't resolve closed revisions.
continue;
}
$uri = $revision->getURI();
foreach ($map[$hash] as $key) {
$results[$key][] = $this->newBrowseURIRef()
->setURI($uri);
}
}
}
yield $this->yieldMap($results);
}
}

View file

@ -0,0 +1,49 @@
<?php
abstract class ArcanistBrowseURIHardpointQuery
extends ArcanistRuntimeHardpointQuery {
public function getSupportedBrowseType() {
return $this->getPhobjectClassConstant('BROWSETYPE', 32);
}
public function getHardpoints() {
return array(
ArcanistBrowseRef::HARDPOINT_URIS,
);
}
protected function canLoadRef(ArcanistRef $ref) {
return ($ref instanceof ArcanistBrowseRef);
}
public function getRefsWithSupportedTypes(array $refs) {
$type = $this->getSupportedBrowseType();
foreach ($refs as $key => $ref) {
if ($ref->isUntyped()) {
continue;
}
if ($ref->hasType($type)) {
continue;
}
unset($refs[$key]);
}
return $refs;
}
public static function getAllBrowseQueries() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
final protected function newBrowseURIRef() {
return id(new ArcanistBrowseURIRef())
->setType($this->getSupportedBrowseType());
}
}

View file

@ -0,0 +1,69 @@
<?php
final class ArcanistBrowseRef
extends ArcanistRef {
const HARDPOINT_URIS = 'uris';
const HARDPOINT_COMMITREFS = 'commitRefs';
private $token;
private $types = array();
private $branch;
public function getRefDisplayName() {
return pht('Browse Query "%s"', $this->getToken());
}
protected function newHardpoints() {
return array(
$this->newVectorHardpoint(self::HARDPOINT_COMMITREFS),
$this->newVectorHardpoint(self::HARDPOINT_URIS),
);
}
public function setToken($token) {
$this->token = $token;
return $this;
}
public function getToken() {
return $this->token;
}
public function setTypes(array $types) {
$this->types = $types;
return $this;
}
public function getTypes() {
return $this->types;
}
public function hasType($type) {
$map = $this->getTypes();
$map = array_fuse($map);
return isset($map[$type]);
}
public function isUntyped() {
return !$this->types;
}
public function setBranch($branch) {
$this->branch = $branch;
return $this;
}
public function getBranch() {
return $this->branch;
}
public function getURIs() {
return $this->getHardpoint(self::HARDPOINT_URIS);
}
public function getCommitRefs() {
return $this->getHardpoint(self::HARDPOINT_COMMITREFS);
}
}

View file

@ -0,0 +1,31 @@
<?php
final class ArcanistBrowseURIRef
extends ArcanistRef {
private $uri;
private $type;
public function getRefDisplayName() {
return pht('Browse URI "%s"', $this->getURI());
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
return $this->uri;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
}

View file

@ -0,0 +1,261 @@
<?php
/**
* Browse files or objects in the Phabricator web interface.
*/
final class ArcanistBrowseWorkflow
extends ArcanistArcWorkflow {
public function getWorkflowName() {
return 'browse';
}
public function getWorkflowInformation() {
$help = pht(<<<EOTEXT
Open a file or object (like a task or revision) in a local web browser.
$ arc browse README # Open a file in Diffusion.
$ arc browse T123 # View a task.
$ arc browse HEAD # View a symbolic commit.
To choose a browser binary to invoke, use:
$ arc set-config browser __browser-binary__
If no browser is set, the command will try to guess which browser to use.
EOTEXT
);
return $this->newWorkflowInformation()
->setSynopsis(pht('Open a file or object in a local web browser.'))
->addExample('**browse** [options] -- __target__ ...')
->addExample('**browse** -- __file-name__')
->addExample('**browse** -- __object-name__')
->setHelp($help);
}
public function getWorkflowArguments() {
return array(
$this->newWorkflowArgument('branch')
->setParameter('branch-name')
->setHelp(
pht(
'Default branch name to view on server. Defaults to "%s".',
'master')),
$this->newWorkflowArgument('types')
->setParameter('type-list')
->setHelp(
pht(
'Force targets to be interpreted as naming particular types of '.
'resources.')),
$this->newWorkflowArgument('force')
->setHelp(
pht(
'(DEPRECATED) Obsolete, use "--types path" instead.')),
$this->newWorkflowArgument('targets')
->setIsPathArgument(true)
->setWildcard(true),
);
}
public function runWorkflow() {
$targets = $this->getArgument('targets');
$targets = array_fuse($targets);
if (!$targets) {
$refs = array(
new ArcanistBrowseRef(),
);
} else {
$refs = array();
foreach ($targets as $target) {
$refs[] = id(new ArcanistBrowseRef())
->setToken($target);
}
}
$is_force = $this->getArgument('force');
if ($is_force) {
// TODO: Remove this completely.
$this->writeWarn(
pht('DEPRECATED'),
pht(
'Argument "--force" for "arc browse" is deprecated. Use '.
'"--type %s" instead.',
ArcanistBrowsePathURIHardpointQuery::BROWSETYPE));
}
$types = $this->getArgument('types');
if ($types !== null) {
$types = preg_split('/[\s,]+/', $types);
} else {
if ($is_force) {
$types = array(ArcanistBrowsePathURIHardpointQuery::BROWSETYPE);
} else {
$types = array();
}
}
foreach ($refs as $ref) {
$ref->setTypes($types);
}
$branch = $this->getArgument('branch');
if ($branch) {
foreach ($refs as $ref) {
$ref->setBranch($branch);
}
}
// TODO: The "Path" and "Commit" queries should regain the ability to warn
// when this command is not run in a working copy that belongs to a
// recognized repository, so they won't ever be able to resolve things.
// TODO: When you run "arc browse" with no arguments, we should either
// take you to the repository home page or show help.
// TODO: When you "arc browse something/like/a/path.c" but it does not
// exist on disk, it is not resolved unless you explicitly use "--type
// path". This should be explained more clearly again.
$this->loadHardpoints(
$refs,
ArcanistBrowseRef::HARDPOINT_URIS);
$zero_hits = array();
$open_uris = array();
$many_hits = array();
foreach ($refs as $ref) {
$uris = $ref->getURIs();
if (!$uris) {
$zero_hits[] = $ref;
} else if (count($uris) == 1) {
$open_uris[] = $ref;
} else {
$many_hits[] = $ref;
}
}
$pick_map = array();
$pick_selection = null;
$pick_id = 0;
if ($many_hits) {
foreach ($many_hits as $ref) {
$token = $ref->getToken();
if (strlen($token)) {
$message = pht('Argument "%s" is ambiguous.', $token);
} else {
$message = pht('Default behavior is ambiguous.');
}
$this->writeWarn(pht('AMBIGUOUS'), $message);
}
$is_single_ref = (count($refs) == 1);
$table = id(new PhutilConsoleTable());
if ($is_single_ref) {
$table->addColumn('pick', array('title' => pht('Pick')));
} else {
$table->addColumn('argument', array('title' => pht('Argument')));
}
$table
->addColumn('type', array('title' => pht('Type')))
->addColumn('uri', array('title' => pht('URI')));
foreach ($many_hits as $ref) {
$token_display = $ref->getToken();
if (!strlen($token)) {
$token_display = pht('<default>');
}
foreach ($ref->getURIs() as $uri) {
++$pick_id;
$pick_map[$pick_id] = $uri;
$row = array(
'pick' => $pick_id,
'argument' => $token_display,
'type' => $uri->getType(),
'uri' => $uri->getURI(),
);
$table->addRow($row);
}
}
$table->draw();
if ($is_single_ref) {
$pick_selection = phutil_console_select(
pht('Which URI do you want to open?'),
1,
$pick_id);
$open_uris[] = $ref;
} else {
$this->writeInfo(
pht('CHOOSE'),
pht('Use "--types" to select between alternatives.'));
}
}
// If anything failed to resolve, this is also an error.
if ($zero_hits) {
foreach ($zero_hits as $ref) {
$token = $ref->getToken();
if ($token === null) {
echo tsprintf(
"%s\n",
pht(
'Unable to resolve default browse target.'));
} else {
echo tsprintf(
"%s\n",
pht(
'Unable to resolve argument "%s".',
$ref->getToken()));
}
}
}
$uris = array();
foreach ($open_uris as $ref) {
$ref_uris = $ref->getURIs();
if (count($ref_uris) > 1) {
foreach ($ref_uris as $uri_key => $uri) {
if ($pick_map[$pick_selection] !== $uri) {
unset($ref_uris[$uri_key]);
}
}
}
$ref_uri = head($ref_uris);
// TODO: "ArcanistRevisionRef", at least, may return a relative URI.
// If we get a relative URI, guess the correct absolute URI based on
// the Conduit URI. This might not be correct for Conduit over SSH.
$raw_uri = $ref_uri->getURI();
$raw_uri = new PhutilURI($raw_uri);
if (!strlen($raw_uri->getDomain())) {
$base_uri = $this->getConduitEngine()
->getConduitURI();
$raw_uri = id(new PhutilURI($base_uri))
->setPath($raw_uri->getPath());
}
$raw_uri = phutil_string_cast($raw_uri);
$uris[] = $raw_uri;
}
$this->openURIsInBrowser($uris);
return 0;
}
}

View file

@ -0,0 +1,426 @@
<?php
/**
* Wrapper around streams, pipes, and other things that have basic read/write
* I/O characteristics.
*
* Channels include buffering, so you can do fire-and-forget writes and reads
* without worrying about network/pipe buffers. Use @{method:read} and
* @{method:write} to read and write.
*
* Channels are nonblocking and provide a select()-oriented interface so you
* can reasonably write server-like and daemon-like things with them. Use
* @{method:waitForAny} to select channels.
*
* Channel operations other than @{method:update} generally operate on buffers.
* Writes and reads affect buffers, while @{method:update} flushes output
* buffers and fills input buffers.
*
* Channels are either "open" or "closed". You can detect that a channel has
* closed by calling @{method:isOpen} or examining the return value of
* @{method:update}.
*
* NOTE: Channels are new (as of June 2012) and subject to interface changes.
*
* @task io Reading and Writing
* @task wait Waiting for Activity
* @task update Responding to Activity
* @task impl Channel Implementation
*/
abstract class PhutilChannel extends Phobject {
private $ibuf = '';
private $obuf;
private $name;
private $readBufferSize;
public function __construct() {
$this->obuf = new PhutilRope();
}
/* -( Reading and Writing )------------------------------------------------ */
/**
* Read from the channel. A channel defines the format of data that is read
* from it, so this method may return strings, objects, or anything else.
*
* The default implementation returns bytes.
*
* @return wild Data from the channel, normally bytes.
*
* @task io
*/
public function read() {
$result = $this->ibuf;
$this->ibuf = '';
return $result;
}
/**
* Write to the channel. A channel defines what data format it accepts,
* so this method may take strings, objects, or anything else.
*
* The default implementation accepts bytes.
*
* @param wild Data to write to the channel, normally bytes.
* @return this
*
* @task io
*/
public function write($bytes) {
if (!is_scalar($bytes)) {
throw new Exception(
pht(
'%s may only write strings!',
__METHOD__.'()'));
}
$this->obuf->append($bytes);
return $this;
}
/* -( Waiting for Activity )----------------------------------------------- */
/**
* Wait for any activity on a list of channels. Convenience wrapper around
* @{method:waitForActivity}.
*
* @param list<PhutilChannel> A list of channels to wait for.
* @param dict Options, see above.
* @return void
*
* @task wait
*/
public static function waitForAny(array $channels, array $options = array()) {
return self::waitForActivity($channels, $channels, $options);
}
/**
* Wait (using select()) for channels to become ready for reads or writes.
* This method blocks until some channel is ready to be updated.
*
* It does not provide a way to determine which channels are ready to be
* updated. The expectation is that you'll just update every channel. This
* might change eventually.
*
* Available options are:
*
* - 'read' (list<stream>) Additional streams to select for read.
* - 'write' (list<stream>) Additional streams to select for write.
* - 'except' (list<stream>) Additional streams to select for except.
* - 'timeout' (float) Select timeout, defaults to 1.
*
* NOTE: Extra streams must be //streams//, not //sockets//, because this
* method uses `stream_select()`, not `socket_select()`.
*
* @param list<PhutilChannel> List of channels to wait for reads on.
* @param list<PhutilChannel> List of channels to wait for writes on.
* @return void
*
* @task wait
*/
public static function waitForActivity(
array $reads,
array $writes,
array $options = array()) {
assert_instances_of($reads, __CLASS__);
assert_instances_of($writes, __CLASS__);
$read = idx($options, 'read', array());
$write = idx($options, 'write', array());
$except = idx($options, 'except', array());
$wait = idx($options, 'timeout', 1);
// TODO: It would be nice to just be able to categorically reject these as
// unselectable.
foreach (array($reads, $writes) as $channels) {
foreach ($channels as $channel) {
$r_sockets = $channel->getReadSockets();
$w_sockets = $channel->getWriteSockets();
// If any channel has no read sockets and no write sockets, assume it
// isn't selectable and return immediately (effectively degrading to a
// busy wait).
if (!$r_sockets && !$w_sockets) {
return false;
}
}
}
foreach ($reads as $channel) {
// If any of the read channels have data in read buffers, return
// immediately. If we don't, we risk running select() on a bunch of
// sockets which won't become readable because the data the application
// expects is already in a read buffer.
if (!$channel->isReadBufferEmpty()) {
return;
}
$r_sockets = $channel->getReadSockets();
foreach ($r_sockets as $socket) {
$read[] = $socket;
$except[] = $socket;
}
}
foreach ($writes as $channel) {
if ($channel->isWriteBufferEmpty()) {
// If the channel's write buffer is empty, don't select the write
// sockets, since they're writable immediately.
$w_sockets = array();
} else {
$w_sockets = $channel->getWriteSockets();
}
foreach ($w_sockets as $socket) {
$write[] = $socket;
$except[] = $socket;
}
}
if (!$read && !$write && !$except) {
return false;
}
$wait_sec = (int)$wait;
$wait_usec = 1000000 * ($wait - $wait_sec);
@stream_select($read, $write, $except, $wait_sec, $wait_usec);
}
/* -( Responding to Activity )--------------------------------------------- */
/**
* Updates the channel, filling input buffers and flushing output buffers.
* Returns false if the channel has closed.
*
* @return bool True if the channel is still open.
*
* @task update
*/
public function update() {
$maximum_read = PHP_INT_MAX;
if ($this->readBufferSize !== null) {
$maximum_read = ($this->readBufferSize - strlen($this->ibuf));
}
while ($maximum_read > 0) {
$in = $this->readBytes($maximum_read);
if (!strlen($in)) {
// Reading is blocked for now.
break;
}
$this->ibuf .= $in;
$maximum_read -= strlen($in);
}
while ($this->obuf->getByteLength()) {
$len = $this->writeBytes($this->obuf->getAnyPrefix());
if (!$len) {
// Writing is blocked for now.
break;
}
$this->obuf->removeBytesFromHead($len);
}
return $this->isOpen();
}
/* -( Channel Implementation )--------------------------------------------- */
/**
* Set a channel name. This is primarily intended to allow you to debug
* channel code more easily, by naming channels something meaningful.
*
* @param string Channel name.
* @return this
*
* @task impl
*/
public function setName($name) {
$this->name = $name;
return $this;
}
/**
* Get the channel name, as set by @{method:setName}.
*
* @return string Name of the channel.
*
* @task impl
*/
public function getName() {
return coalesce($this->name, get_class($this));
}
/**
* Test if the channel is open: active, can be read from and written to, etc.
*
* @return bool True if the channel is open.
*
* @task impl
*/
abstract public function isOpen();
/**
* Close the channel for writing.
*
* @return void
* @task impl
*/
abstract public function closeWriteChannel();
/**
* Test if the channel is open for reading.
*
* @return bool True if the channel is open for reading.
*
* @task impl
*/
public function isOpenForReading() {
return $this->isOpen();
}
/**
* Test if the channel is open for writing.
*
* @return bool True if the channel is open for writing.
*
* @task impl
*/
public function isOpenForWriting() {
return $this->isOpen();
}
/**
* Read from the channel's underlying I/O.
*
* @param int Maximum number of bytes to read.
* @return string Bytes, if available.
*
* @task impl
*/
abstract protected function readBytes($length);
/**
* Write to the channel's underlying I/O.
*
* @param string Bytes to write.
* @return int Number of bytes written.
*
* @task impl
*/
abstract protected function writeBytes($bytes);
/**
* Get sockets to select for reading.
*
* @return list<stream> Read sockets.
*
* @task impl
*/
protected function getReadSockets() {
return array();
}
/**
* Get sockets to select for writing.
*
* @return list<stream> Write sockets.
*
* @task impl
*/
protected function getWriteSockets() {
return array();
}
/**
* Set the maximum size of the channel's read buffer. Reads will artificially
* block once the buffer reaches this size until the in-process buffer is
* consumed.
*
* @param int|null Maximum read buffer size, or `null` for a limitless buffer.
* @return this
* @task impl
*/
public function setReadBufferSize($size) {
$this->readBufferSize = $size;
return $this;
}
/**
* Test state of the read buffer.
*
* @return bool True if the read buffer is empty.
*
* @task impl
*/
public function isReadBufferEmpty() {
return (strlen($this->ibuf) == 0);
}
/**
* Test state of the write buffer.
*
* @return bool True if the write buffer is empty.
*
* @task impl
*/
public function isWriteBufferEmpty() {
return !$this->getWriteBufferSize();
}
/**
* Get the number of bytes we're currently waiting to write.
*
* @return int Number of waiting bytes.
*
* @task impl
*/
public function getWriteBufferSize() {
return $this->obuf->getByteLength();
}
/**
* Wait for any buffered writes to complete. This is a blocking call. When
* the call returns, the write buffer will be empty.
*
* @task impl
*/
public function flush() {
while (!$this->isWriteBufferEmpty()) {
self::waitForAny(array($this));
if (!$this->update()) {
throw new Exception(pht('Channel closed while flushing output!'));
}
}
return $this;
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* Channel that wraps some other channel. This class is not interesting on its
* own and just serves as a no-op proxy, but extending it allows you to compose
* channels to mutate their characteristics (for instance, to add protocol
* semantics with @{class:PhutilProtocolChannel}).
*
* The implementation of this class is entirely uninteresting.
*/
abstract class PhutilChannelChannel extends PhutilChannel {
private $channel;
public function __construct(PhutilChannel $channel) {
parent::__construct();
$this->channel = $channel;
$this->didConstruct();
}
protected function didConstruct() {
// Hook for subclasses.
}
public function read() {
return $this->channel->read();
}
public function write($message) {
$this->channel->write($message);
return $this;
}
public function update() {
return $this->channel->update();
}
public function isOpen() {
return $this->channel->isOpen();
}
public function closeWriteChannel() {
return $this->channel->closeWriteChannel();
}
public function isOpenForReading() {
return $this->channel->isOpenForReading();
}
public function isOpenForWriting() {
return $this->channel->isOpenForWriting();
}
protected function readBytes($length) {
$this->throwOnRawByteOperations();
}
protected function writeBytes($bytes) {
$this->throwOnRawByteOperations();
}
protected function getReadSockets() {
return $this->channel->getReadSockets();
}
protected function getWriteSockets() {
return $this->channel->getWriteSockets();
}
public function setReadBufferSize($size) {
$this->channel->setReadBufferSize($size);
return $this;
}
public function isReadBufferEmpty() {
return $this->channel->isReadBufferEmpty();
}
public function isWriteBufferEmpty() {
return $this->channel->isWriteBufferEmpty();
}
public function getWriteBufferSize() {
return $this->channel->getWriteBufferSize();
}
public function flush() {
$this->channel->flush();
return $this;
}
protected function getUnderlyingChannel() {
return $this->channel;
}
private function throwOnRawByteOperations() {
// NOTE: You should only be able to end up here if you subclass this class
// and implement your subclass incorrectly, since the byte methods are
// protected.
throw new Exception(
pht(
'Do not call %s or %s directly on a %s. Instead, call %s or %s.',
'readBytes()',
'writeBytes()',
__CLASS__,
'read()',
'write()'));
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* Channel on an underlying @{class:ExecFuture}. For a description of channels,
* see @{class:PhutilChannel}.
*
* For example, you can open a channel on `nc` like this:
*
* $future = new ExecFuture('nc example.com 80');
* $channel = new PhutilExecChannel($future);
*
* $channel->write("GET / HTTP/1.0\n\n");
* while (true) {
* echo $channel->read();
*
* PhutilChannel::waitForAny(array($channel));
* if (!$channel->update()) {
* // Break out of the loop when the channel closes.
* break;
* }
* }
*
* This script makes an HTTP request to "example.com". This example is heavily
* contrived. In most cases, @{class:ExecFuture} and other futures constructs
* offer a much easier way to solve problems which involve system commands, and
* @{class:HTTPFuture} and other HTTP constructs offer a much easier way to
* solve problems which involve HTTP.
*
* @{class:PhutilExecChannel} is generally useful only when a program acts like
* a server but performs I/O on stdin/stdout, and you need to act like a client
* or interact with the program at the same time as you manage traditional
* socket connections. Examples are Mercurial operating in "cmdserve" mode, git
* operating in "receive-pack" mode, etc. It is unlikely that any reasonable
* use of this class is concise enough to make a short example out of, so you
* get a contrived one instead.
*
* See also @{class:PhutilSocketChannel}, for a similar channel that uses
* sockets for I/O.
*
* Since @{class:ExecFuture} already supports buffered I/O and socket selection,
* the implementation of this class is fairly straightforward.
*
* @task construct Construction
*/
final class PhutilExecChannel extends PhutilChannel {
private $future;
private $stderrHandler;
/* -( Construction )------------------------------------------------------- */
/**
* Construct an exec channel from a @{class:ExecFuture}. The future should
* **NOT** have been started yet (e.g., with `isReady()` or `start()`),
* because @{class:ExecFuture} closes stdin by default when futures start.
* If stdin has been closed, you will be unable to write on the channel.
*
* @param ExecFuture Future to use as an underlying I/O source.
* @task construct
*/
public function __construct(ExecFuture $future) {
parent::__construct();
// Make an empty write to keep the stdin pipe open. By default, futures
// close this pipe when they start.
$future->write('', $keep_pipe = true);
// Start the future so that reads and writes work immediately.
$future->isReady();
$this->future = $future;
}
public function __destruct() {
if (!$this->future->isReady()) {
$this->future->resolveKill();
}
}
public function update() {
$this->future->isReady();
return parent::update();
}
public function isOpen() {
return !$this->future->isReady();
}
protected function readBytes($length) {
list($stdout, $stderr) = $this->future->read();
$this->future->discardBuffers();
if (strlen($stderr)) {
if ($this->stderrHandler) {
call_user_func($this->stderrHandler, $this, $stderr);
} else {
throw new Exception(
pht('Unexpected output to stderr on exec channel: %s', $stderr));
}
}
return $stdout;
}
public function write($bytes) {
$this->future->write($bytes, $keep_pipe = true);
}
public function closeWriteChannel() {
$this->future->write('', $keep_pipe = false);
}
protected function writeBytes($bytes) {
throw new Exception(pht('%s can not write bytes directly!', 'ExecFuture'));
}
protected function getReadSockets() {
return $this->future->getReadSockets();
}
protected function getWriteSockets() {
return $this->future->getWriteSockets();
}
public function isReadBufferEmpty() {
// Check both the channel and future read buffers, since either could have
// data.
return parent::isReadBufferEmpty() && $this->future->isReadBufferEmpty();
}
public function setReadBufferSize($size) {
// NOTE: We may end up using 2x the buffer size here, one inside
// ExecFuture and one inside the Channel. We could tune this eventually, but
// it should be fine for now.
parent::setReadBufferSize($size);
$this->future->setReadBufferSize($size);
return $this;
}
public function isWriteBufferEmpty() {
return $this->future->isWriteBufferEmpty();
}
public function getWriteBufferSize() {
return $this->future->getWriteBufferSize();
}
/**
* If the wrapped @{class:ExecFuture} outputs data to stderr, we normally
* throw an exception. Instead, you can provide a callback handler that will
* be invoked and passed the data. It should have this signature:
*
* function f(PhutilExecChannel $channel, $stderr) {
* // ...
* }
*
* The `$channel` will be this channel object, and `$stderr` will be a string
* with bytes received over stderr.
*
* You can set a handler which does nothing to effectively ignore and discard
* any output on stderr.
*
* @param callable Handler to invoke when stderr data is received.
* @return this
*/
public function setStderrHandler($handler) {
$this->stderrHandler = $handler;
return $this;
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
* Channel that transmits dictionaries of primitives using JSON serialization.
* This channel is not binary safe.
*
* This protocol is implemented by the Phabricator Aphlict realtime notification
* server.
*
* @task protocol Protocol Implementation
*/
final class PhutilJSONProtocolChannel extends PhutilProtocolChannel {
const MODE_LENGTH = 'length';
const MODE_OBJECT = 'object';
/**
* Size of the "length" frame of the protocol in bytes.
*/
const SIZE_LENGTH = 8;
private $mode = self::MODE_LENGTH;
private $byteLengthOfNextChunk = self::SIZE_LENGTH;
private $buf = '';
/* -( Protocol Implementation )-------------------------------------------- */
/**
* Encode a message for transmission over the channel. The message should
* be any serializable as JSON.
*
* Objects are transmitted as:
*
* <len><json>
*
* ...where <len> is an 8-character, zero-padded integer written as a string.
* For example, this is a valid message:
*
* 00000015{"key":"value"}
*
* @task protocol
*/
protected function encodeMessage($message) {
if (!is_array($message)) {
throw new Exception(
pht(
'JSON protocol message must be an array, got some other '.
'type ("%s").',
phutil_describe_type($message)));
}
$message = phutil_json_encode($message);
$len = sprintf(
'%0'.self::SIZE_LENGTH.'.'.self::SIZE_LENGTH.'d',
strlen($message));
return "{$len}{$message}";
}
/**
* Decode a message received from the other end of the channel. Messages are
* decoded as associative arrays.
*
* @task protocol
*/
protected function decodeStream($data) {
$this->buf .= $data;
$objects = array();
while (strlen($this->buf) >= $this->byteLengthOfNextChunk) {
switch ($this->mode) {
case self::MODE_LENGTH:
$len = substr($this->buf, 0, self::SIZE_LENGTH);
$this->buf = substr($this->buf, self::SIZE_LENGTH);
if (!preg_match('/^\d+\z/', $len)) {
$full_buffer = $len.$this->buf;
$full_length = strlen($full_buffer);
throw new Exception(
pht(
'Protocol channel expected %s-character, zero-padded '.
'numeric frame length, got something else ("%s"). Full '.
'buffer (of length %s) begins: %s',
new PhutilNumber(self::SIZE_LENGTH),
phutil_encode_log($len),
new PhutilNumber($full_length),
phutil_encode_log(substr($len.$this->buf, 0, 128))));
}
$this->mode = self::MODE_OBJECT;
$this->byteLengthOfNextChunk = (int)$len;
break;
case self::MODE_OBJECT:
$data = substr($this->buf, 0, $this->byteLengthOfNextChunk);
$this->buf = substr($this->buf, $this->byteLengthOfNextChunk);
try {
$objects[] = phutil_json_decode($data);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to decode JSON object.'),
$ex);
}
$this->mode = self::MODE_LENGTH;
$this->byteLengthOfNextChunk = self::SIZE_LENGTH;
break;
}
}
return $objects;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* A @{class:PhutilChannelChannel} which wraps some other channel and writes
* data passed over it to a log file.
*/
final class PhutilLogFileChannel extends PhutilChannelChannel {
private $logfile;
public function setLogfile($path) {
$this->logfile = fopen($path, 'a');
$this->log('--- '.getmypid().' ---');
return $this;
}
public function read() {
$buffer = parent::read();
if (strlen($buffer)) {
$this->log('>>> '.phutil_loggable_string($buffer));
}
return $buffer;
}
public function write($message) {
if (strlen($message)) {
$this->log('<<< '.phutil_loggable_string($message));
}
return parent::write($message);
}
private function log($message) {
if ($this->logfile) {
fwrite($this->logfile, $message."\n");
}
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* A @{class:PhutilChannelChannel} which wraps some other channel and provides
* metrics about its use (e.g., bytes read and bytes written).
*
* @task metrics Channel Metrics
* @task impl Implementation
*/
final class PhutilMetricsChannel extends PhutilChannelChannel {
private $bytesRead = 0;
private $bytesWritten = 0;
private $startTime;
/* -( Channel Metrics )---------------------------------------------------- */
/**
* Get the number of bytes that have been written to the channel. This
* includes any bytes which have been buffered but not actually transmitted,
* and thus may overreport compared to actual activity on the wire.
*
* @return int Bytes written.
* @task metrics
*/
public function getBytesWritten() {
return $this->bytesWritten;
}
/**
* Get the number of bytes that have been read from the channel. This excludes
* any bytes which have been received but not actually read by anything, and
* thus may underreport compared to actual activity on the wire.
*
* @return int Bytes read.
* @task metrics
*/
public function getBytesRead() {
return $this->bytesRead;
}
/**
* Get the elapsed wall time since this channel opened.
*
* @return float Wall time, in seconds.
* @task metrics
*/
public function getWallTime() {
return microtime(true) - $this->startTime;
}
/* -( Implementation )----------------------------------------------------- */
/**
* @task impl
*/
protected function didConstruct() {
$this->startTime = microtime(true);
}
/**
* @task impl
*/
public function read() {
$buffer = parent::read();
$this->bytesRead += strlen($buffer);
return $buffer;
}
/**
* @task impl
*/
public function write($message) {
$this->bytesWritten += strlen($message);
return parent::write($message);
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* Channel that transmits PHP objects using PHP serialization. This channel
* is binary safe.
*
* @task protocol Protocol Implementation
*/
final class PhutilPHPObjectProtocolChannel extends PhutilProtocolChannel {
const MODE_LENGTH = 'length';
const MODE_OBJECT = 'object';
/**
* Size of the "length" frame of the protocol in bytes.
*/
const SIZE_LENGTH = 4;
private $mode = self::MODE_LENGTH;
private $byteLengthOfNextChunk = self::SIZE_LENGTH;
private $buf = '';
/* -( Protocol Implementation )-------------------------------------------- */
/**
* Encode a message for transmission over the channel. The message should
* be any serializable PHP object. The entire object will be serialized, so
* avoid transmitting objects which connect to large graphs of other objects,
* etc.
*
* This channel can transmit class instances, but the receiving end must be
* running the same version of the code. There are no builtin safeguards
* to protect against versioning problems in object serialization.
*
* Objects are transmitted as:
*
* <len><serialized PHP object>
*
* ...where <len> is a 4-byte unsigned big-endian integer.
*
* @task protocol
*/
protected function encodeMessage($message) {
$message = serialize($message);
$len = pack('N', strlen($message));
return "{$len}{$message}";
}
/**
* Decode a message received from the other end of the channel.
*
* @task protocol
*/
protected function decodeStream($data) {
$this->buf .= $data;
$objects = array();
while (strlen($this->buf) >= $this->byteLengthOfNextChunk) {
switch ($this->mode) {
case self::MODE_LENGTH:
$len = substr($this->buf, 0, self::SIZE_LENGTH);
$this->buf = substr($this->buf, self::SIZE_LENGTH);
$this->mode = self::MODE_OBJECT;
$this->byteLengthOfNextChunk = head(unpack('N', $len));
break;
case self::MODE_OBJECT:
$data = substr($this->buf, 0, $this->byteLengthOfNextChunk);
$this->buf = substr($this->buf, $this->byteLengthOfNextChunk);
$obj = @unserialize($data);
if ($obj === false) {
throw new Exception(pht('Failed to unserialize object: %s', $data));
} else {
$objects[] = $obj;
}
$this->mode = self::MODE_LENGTH;
$this->byteLengthOfNextChunk = self::SIZE_LENGTH;
break;
}
}
return $objects;
}
}

View file

@ -0,0 +1,139 @@
<?php
/**
* Wraps a @{class:PhutilChannel} and implements a message-oriented protocol
* on top of it. A protocol channel behaves like a normal channel, except that
* @{method:read} and @{method:write} are message-oriented and the protocol
* channel handles encoding and parsing messages for transmission.
*
* @task io Reading and Writing
* @task protocol Protocol Implementation
* @task wait Waiting for Activity
*/
abstract class PhutilProtocolChannel extends PhutilChannelChannel {
private $messages = array();
/* -( Reading and Writing )------------------------------------------------ */
/**
* Read a message from the channel, if a message is available.
*
* @return wild A message, or null if no message is available.
*
* @task io
*/
public function read() {
$data = parent::read();
if (strlen($data)) {
$messages = $this->decodeStream($data);
foreach ($messages as $message) {
$this->addMessage($message);
}
}
if (!$this->messages) {
return null;
}
return array_shift($this->messages);
}
/**
* Write a message to the channel.
*
* @param wild Some message.
* @return this
*
* @task io
*/
public function write($message) {
$bytes = $this->encodeMessage($message);
return parent::write($bytes);
}
/**
* Add a message to the queue. While you normally do not need to do this,
* you can use it to inject out-of-band messages.
*
* @param wild Some message.
* @return this
*
* @task io
*/
public function addMessage($message) {
$this->messages[] = $message;
return $this;
}
/* -( Protocol Implementation )-------------------------------------------- */
/**
* Encode a message for transmission.
*
* @param wild Some message.
* @return string The message serialized into a wire format for
* transmission.
*
* @task protocol
*/
abstract protected function encodeMessage($message);
/**
* Decode bytes from the underlying channel into zero or more complete
* messages. The messages should be returned.
*
* This method is called as data is available. It will receive incoming
* data only once, and must buffer any data which represents only part of
* a message. Once a complete message is received, it can return the message
* and discard that part of the buffer.
*
* Generally, a protocol channel should maintain a read buffer, implement
* a parser in this method, and store parser state on the object to be able
* to process incoming data in small chunks.
*
* @param string One or more bytes from the underlying channel.
* @return list<wild> Zero or more parsed messages.
*
* @task protocol
*/
abstract protected function decodeStream($data);
/* -( Waiting for Activity )----------------------------------------------- */
/**
* Wait for a message, blocking until one is available.
*
* @return wild A message.
*
* @task wait
*/
public function waitForMessage() {
while (true) {
$is_open = $this->update();
$message = $this->read();
if ($message !== null) {
return $message;
}
if (!$is_open) {
break;
}
self::waitForAny(array($this));
}
throw new Exception(pht('Channel closed while waiting for message!'));
}
}

View file

@ -0,0 +1,192 @@
<?php
/**
* Channel on an underlying stream socket or socket pair. For a description of
* channels, see @{class:PhutilChannel}.
*
* Using a network socket:
*
* $socket = stream_socket_client(...);
* $channel = new PhutilSocketChannel($socket);
*
* Using stdin/stdout:
*
* $channel = new PhutilSocketChannel(
* fopen('php://stdin', 'r'),
* fopen('php://stdout', 'w'));
*
* @task construct Construction
*/
final class PhutilSocketChannel extends PhutilChannel {
private $readSocket;
private $writeSocket;
private $isSingleSocket;
/* -( Construction )------------------------------------------------------- */
/**
* Construct a socket channel from a socket or a socket pair.
*
* NOTE: This must be a //stream// socket from `stream_socket_client()` or
* `stream_socket_server()` or similar, not a //plain// socket from
* `socket_create()` or similar.
*
* @param socket Socket (stream socket, not plain socket). If only one
* socket is provided, it is used for reading and writing.
* @param socket? Optional write socket.
*
* @task construct
*/
public function __construct($read_socket, $write_socket = null) {
parent::__construct();
foreach (array($read_socket, $write_socket) as $socket) {
if (!$socket) {
continue;
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
throw new Exception(pht('Failed to set socket nonblocking!'));
}
}
$this->readSocket = $read_socket;
if ($write_socket) {
$this->writeSocket = $write_socket;
} else {
$this->writeSocket = $read_socket;
$this->isSingleSocket = true;
}
}
public function __destruct() {
$this->closeSockets();
}
/**
* Creates a pair of socket channels that are connected to each other. This
* is mostly useful for writing unit tests of, e.g., protocol channels.
*
* list($x, $y) = PhutilSocketChannel::newChannelPair();
*
* @task construct
*/
public static function newChannelPair() {
$sockets = null;
$domain = phutil_is_windows() ? STREAM_PF_INET : STREAM_PF_UNIX;
$pair = stream_socket_pair($domain, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
if (!$pair) {
throw new Exception(pht('%s failed!', 'stream_socket_pair()'));
}
$x = new PhutilSocketChannel($pair[0]);
$y = new PhutilSocketChannel($pair[1]);
return array($x, $y);
}
public function isOpen() {
return ($this->isOpenForReading() || $this->isOpenForWriting());
}
public function isOpenForReading() {
return (bool)$this->readSocket;
}
public function isOpenForWriting() {
return (bool)$this->writeSocket;
}
protected function readBytes($length) {
$socket = $this->readSocket;
if (!$socket) {
return '';
}
$data = @fread($socket, min($length, 64 * 1024));
if ($data === false) {
$this->closeReadSocket();
$data = '';
}
// NOTE: fread() continues returning empty string after the socket is
// closed, we need to check for EOF explicitly.
if ($data === '') {
if (feof($socket)) {
$this->closeReadSocket();
}
}
return $data;
}
protected function writeBytes($bytes) {
$socket = $this->writeSocket;
if (!$socket) {
return 0;
}
$len = phutil_fwrite_nonblocking_stream($socket, $bytes);
if ($len === false) {
$this->closeWriteSocket();
return 0;
}
return $len;
}
protected function getReadSockets() {
if ($this->readSocket) {
return array($this->readSocket);
}
return array();
}
protected function getWriteSockets() {
if ($this->writeSocket) {
return array($this->writeSocket);
} else {
return array();
}
}
private function closeReadSocket() {
$this->closeOneSocket($this->readSocket);
$this->readSocket = null;
if ($this->isSingleSocket) {
$this->writeSocket = null;
}
}
private function closeWriteSocket() {
$this->closeOneSocket($this->writeSocket);
$this->writeSocket = null;
if ($this->isSingleSocket) {
$this->readSocket = null;
}
}
public function closeWriteChannel() {
$this->closeWriteSocket();
}
private function closeOneSocket($socket) {
if (!$socket) {
return;
}
// We should also stream_socket_shutdown() here but HHVM throws errors
// with it (for example 'Unexpected object type PlainFile'). We depend
// just on fclose() until it is fixed.
@fclose($socket);
}
private function closeSockets() {
$this->closeReadSocket();
$this->closeWriteSocket();
}
}

View file

@ -0,0 +1,45 @@
<?php
final class PhutilChannelTestCase extends PhutilTestCase {
public function testChannelBasics() {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$str_len_8 = 'abcdefgh';
$str_len_4 = 'abcd';
// Do a write with no buffer limit.
$x->write($str_len_8);
while (true) {
$x->update();
$y->update();
$read = $y->read();
if (strlen($read)) {
break;
}
}
// We expect to read the entire message.
$this->assertEqual($str_len_8, $read);
// Again, with a read buffer limit.
$y->setReadBufferSize(4);
$x->write($str_len_8);
while (true) {
$x->update();
$y->update();
$read = $y->read();
if (strlen($read)) {
break;
}
}
// We expect to see only the first 4 bytes of the message.
$this->assertEqual($str_len_4, $read);
}
}

View file

@ -0,0 +1,26 @@
<?php
final class PhutilJSONProtocolChannelTestCase extends PhutilTestCase {
public function testJSONChannelBasics() {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$xp = new PhutilJSONProtocolChannel($x);
$yp = new PhutilJSONProtocolChannel($y);
$dict = array(
'rand' => mt_rand(),
'list' => array(1, 2, 3),
'null' => null,
);
$xp->write($dict);
$xp->flush();
$result = $yp->waitForMessage();
$this->assertEqual(
$dict,
$result,
pht('Values are identical.'));
}
}

View file

@ -0,0 +1,67 @@
<?php
final class PhutilPHPObjectProtocolChannelTestCase extends PhutilTestCase {
public function testPHPObjectChannelBasics() {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$xp = new PhutilPHPObjectProtocolChannel($x);
$yp = new PhutilPHPObjectProtocolChannel($y);
$object = (object)array(
'key' => mt_rand(),
);
$xp->write($object);
$xp->flush();
$result = $yp->waitForMessage();
$this->assertTrue(
(array)$object === (array)$result,
pht('Values are identical.'));
$this->assertFalse(
$object === $result,
pht('Objects are not the same.'));
}
public function testCloseSocketWriteChannel() {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$xp = new PhutilPHPObjectProtocolChannel($x);
$yp = new PhutilPHPObjectProtocolChannel($y);
$yp->closeWriteChannel();
$yp->update();
// NOTE: This test is more broad than the implementation needs to be. A
// better test would be to verify that this throws an exception:
//
// $xp->waitForMessage();
//
// However, if the test breaks, that method will hang forever instead of
// returning, which would be hard to diagnose. Since the current
// implementation shuts down the entire channel, just test for that.
$this->assertFalse($xp->update(), pht('Expected channel to close.'));
}
public function testCloseExecWriteChannel() {
$bin = $this->getSupportExecutable('cat');
$future = new ExecFuture('php -f %R', $bin);
// If this test breaks, we want to explode, not hang forever.
$future->setTimeout(5);
$exec_channel = new PhutilExecChannel($future);
$exec_channel->write('quack');
$exec_channel->closeWriteChannel();
// If `closeWriteChannel()` did what it is supposed to, this will just
// echo "quack" and exit with no error code. If the channel did not close,
// this will time out after 5 seconds and throw.
$future->resolvex();
$this->assertTrue(true);
}
}

View file

@ -0,0 +1,153 @@
<?php
final class ArcanistConduitCall
extends Phobject {
private $key;
private $engine;
private $method;
private $parameters;
private $future;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setEngine(ArcanistConduitEngine $engine) {
$this->engine = $engine;
return $this;
}
public function getEngine() {
return $this->engine;
}
public function setMethod($method) {
$this->method = $method;
return $this;
}
public function getMethod() {
return $this->method;
}
public function setParameters(array $parameters) {
$this->parameters = $parameters;
return $this;
}
public function getParameters() {
return $this->parameters;
}
private function newFuture() {
if ($this->future) {
throw new Exception(
pht(
'Call has previously generated a future. Create a '.
'new call object for each API method invocation.'));
}
$method = $this->getMethod();
$parameters = $this->getParameters();
$future = $this->getEngine()->newFuture($this);
$this->future = $future;
return $this->future;
}
public function resolve() {
if (!$this->future) {
$this->newFuture();
}
return $this->resolveFuture();
}
private function resolveFuture() {
$future = $this->future;
try {
$result = $future->resolve();
} catch (ConduitClientException $ex) {
switch ($ex->getErrorCode()) {
case 'ERR-INVALID-SESSION':
if (!$this->getEngine()->getConduitToken()) {
$this->raiseLoginRequired();
}
break;
case 'ERR-INVALID-AUTH':
$this->raiseInvalidAuth();
break;
}
throw $ex;
}
return $result;
}
private function raiseLoginRequired() {
$conduit_uri = $this->getEngine()->getConduitURI();
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_uri->setPath('/');
$conduit_domain = $conduit_uri->getDomain();
$block = id(new PhutilConsoleBlock())
->addParagraph(
tsprintf(
'**<bg:red> %s </bg>**',
pht('LOGIN REQUIRED')))
->addParagraph(
pht(
'You are trying to connect to a server ("%s") that you do not '.
'have any stored credentials for, but the command you are '.
'running requires authentication.',
$conduit_domain))
->addParagraph(
pht(
'To login and save credentials for this server, run this '.
'command:'))
->addParagraph(
tsprintf(
" $ arc install-certificate %s\n",
$conduit_uri));
throw new ArcanistUsageException($block->drawConsoleString());
}
private function raiseInvalidAuth() {
$conduit_uri = $this->getEngine()->getConduitURI();
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_uri->setPath('/');
$conduit_domain = $conduit_uri->getDomain();
$block = id(new PhutilConsoleBlock())
->addParagraph(
tsprintf(
'**<bg:red> %s </bg>**',
pht('INVALID CREDENTIALS')))
->addParagraph(
pht(
'Your stored credentials for this server ("%s") are not valid.',
$conduit_domain))
->addParagraph(
pht(
'To login and save valid credentials for this server, run this '.
'command:'))
->addParagraph(
tsprintf(
" $ arc install-certificate %s\n",
$conduit_uri));
throw new ArcanistUsageException($block->drawConsoleString());
}
}

View file

@ -0,0 +1,108 @@
<?php
final class ArcanistConduitEngine
extends Phobject {
private $conduitURI;
private $conduitToken;
private $conduitTimeout;
private $client;
public function isCallable() {
return ($this->conduitURI !== null);
}
public function setConduitURI($conduit_uri) {
$this->conduitURI = $conduit_uri;
return $this;
}
public function getConduitURI() {
return $this->conduitURI;
}
public function setConduitToken($conduit_token) {
$this->conduitToken = $conduit_token;
return $this;
}
public function getConduitToken() {
return $this->conduitToken;
}
public function setConduitTimeout($conduit_timeout) {
$this->conduitTimeout = $conduit_timeout;
return $this;
}
public function getConduitTimeout() {
return $this->conduitTimeout;
}
public function newCall($method, array $parameters) {
if ($this->conduitURI == null) {
$this->raiseURIException();
}
return id(new ArcanistConduitCall())
->setEngine($this)
->setMethod($method)
->setParameters($parameters);
}
public function resolveCall($method, array $parameters) {
return $this->newCall($method, $parameters)->resolve();
}
public function newFuture(ArcanistConduitCall $call) {
$method = $call->getMethod();
$parameters = $call->getParameters();
$future = $this->getClient()->callMethod($method, $parameters);
return $future;
}
private function getClient() {
if (!$this->client) {
$conduit_uri = $this->getConduitURI();
$client = new ConduitClient($conduit_uri);
$timeout = $this->getConduitTimeout();
if ($timeout) {
$client->setTimeout($timeout);
}
$token = $this->getConduitToken();
if ($token) {
$client->setConduitToken($this->getConduitToken());
}
}
return $client;
}
private function raiseURIException() {
$list = id(new PhutilConsoleList())
->addItem(
pht(
'Run in a working copy with "phabricator.uri" set in ".arcconfig".'))
->addItem(
pht(
'Set a default URI with `arc set-config phabricator.uri <uri>`.'))
->addItem(
pht(
'Specify a URI explicitly with `--config phabricator.uri=<uri>`.'));
$block = id(new PhutilConsoleBlock())
->addParagraph(
pht(
'This command needs to communicate with Phabricator, but no '.
'Phabricator URI is configured.'))
->addList($list);
throw new ArcanistUsageException($block->drawConsoleString());
}
}

View file

@ -0,0 +1,3 @@
<?php
abstract class ArcanistConduitException extends Exception {}

View file

@ -0,0 +1,4 @@
<?php
final class ArcanistNoURIConduitException
extends ArcanistConduitException {}

View file

@ -0,0 +1,417 @@
<?php
final class ConduitClient extends Phobject {
private $uri;
private $host;
private $connectionID;
private $sessionKey;
private $timeout = 300.0;
private $username;
private $password;
private $publicKey;
private $privateKey;
private $conduitToken;
private $oauthToken;
private $capabilities = array();
const AUTH_ASYMMETRIC = 'asymmetric';
const SIGNATURE_CONSIGN_1 = 'Consign1.0/';
public function getConnectionID() {
return $this->connectionID;
}
public function __construct($uri) {
$this->uri = new PhutilURI($uri);
if (!strlen($this->uri->getDomain())) {
throw new Exception(
pht("Conduit URI '%s' must include a valid host.", $uri));
}
$this->host = $this->uri->getDomain();
}
/**
* Override the domain specified in the service URI and provide a specific
* host identity.
*
* This can be used to connect to a specific node in a cluster environment.
*/
public function setHost($host) {
$this->host = $host;
return $this;
}
public function getHost() {
return $this->host;
}
public function setConduitToken($conduit_token) {
$this->conduitToken = $conduit_token;
return $this;
}
public function getConduitToken() {
return $this->conduitToken;
}
public function setOAuthToken($oauth_token) {
$this->oauthToken = $oauth_token;
return $this;
}
public function callMethodSynchronous($method, array $params) {
return $this->callMethod($method, $params)->resolve();
}
public function didReceiveResponse($method, $data) {
if ($method == 'conduit.connect') {
$this->sessionKey = idx($data, 'sessionKey');
$this->connectionID = idx($data, 'connectionID');
}
return $data;
}
public function setTimeout($timeout) {
$this->timeout = $timeout;
return $this;
}
public function setSigningKeys(
$public_key,
PhutilOpaqueEnvelope $private_key) {
$this->publicKey = $public_key;
$this->privateKey = $private_key;
return $this;
}
public function enableCapabilities(array $capabilities) {
$this->capabilities += array_fuse($capabilities);
return $this;
}
public function callMethod($method, array $params) {
$meta = array();
if ($this->sessionKey) {
$meta['sessionKey'] = $this->sessionKey;
}
if ($this->connectionID) {
$meta['connectionID'] = $this->connectionID;
}
if ($method == 'conduit.connect') {
$certificate = idx($params, 'certificate');
if ($certificate) {
$token = time();
$params['authToken'] = $token;
$params['authSignature'] = sha1($token.$certificate);
}
unset($params['certificate']);
}
if ($this->privateKey && $this->publicKey) {
$meta['auth.type'] = self::AUTH_ASYMMETRIC;
$meta['auth.key'] = $this->publicKey;
$meta['auth.host'] = $this->getHostStringForSignature();
$signature = $this->signRequest($method, $params, $meta);
$meta['auth.signature'] = $signature;
}
if ($this->conduitToken) {
$meta['token'] = $this->conduitToken;
}
if ($this->oauthToken) {
$meta['access_token'] = $this->oauthToken;
}
if ($meta) {
$params['__conduit__'] = $meta;
}
$uri = id(clone $this->uri)->setPath('/api/'.$method);
$data = array(
'params' => json_encode($params),
'output' => 'json',
// This is a hint to Phabricator that the client expects a Conduit
// response. It is not necessary, but provides better error messages in
// some cases.
'__conduit__' => true,
);
// Always use the cURL-based HTTPSFuture, for proxy support and other
// protocol edge cases that HTTPFuture does not support.
$core_future = new HTTPSFuture($uri);
$core_future->addHeader('Host', $this->getHostStringForHeader());
$core_future->setMethod('POST');
$core_future->setTimeout($this->timeout);
// See T13507. If possible, try to compress requests. To compress requests,
// we must have "gzencode()" available and the server needs to have
// asserted it has the "gzip" capability.
$can_gzip =
(function_exists('gzencode')) &&
(isset($this->capabilities['gzip']));
if ($can_gzip) {
$gzip_data = phutil_build_http_querystring($data);
$gzip_data = gzencode($gzip_data);
$core_future->addHeader('Content-Encoding', 'gzip');
$core_future->setData($gzip_data);
} else {
$core_future->setData($data);
}
if ($this->username !== null) {
$core_future->setHTTPBasicAuthCredentials(
$this->username,
$this->password);
}
return id(new ConduitFuture($core_future))
->setClient($this, $method);
}
public function setBasicAuthCredentials($username, $password) {
$this->username = $username;
$this->password = new PhutilOpaqueEnvelope($password);
return $this;
}
private function getHostStringForHeader() {
return $this->newHostString(false);
}
private function getHostStringForSignature() {
return $this->newHostString(true);
}
/**
* Build a string describing the host for this request.
*
* This method builds strings in two modes: with explicit ports for request
* signing (which always include the port number) and with implicit ports
* for use in the "Host:" header of requests (which omit the port number if
* the port is the same as the default port for the protocol).
*
* This implicit port behavior is similar to what browsers do, so it is less
* likely to get us into trouble with webserver configurations.
*
* @param bool True to include the port explicitly.
* @return string String describing the host for the request.
*/
private function newHostString($with_explicit_port) {
$host = $this->getHost();
$uri = new PhutilURI($this->uri);
$protocol = $uri->getProtocol();
$port = $uri->getPort();
$implicit_ports = array(
'https' => 443,
);
$default_port = 80;
$implicit_port = idx($implicit_ports, $protocol, $default_port);
if ($with_explicit_port) {
if (!$port) {
$port = $implicit_port;
}
} else {
if ($port == $implicit_port) {
$port = null;
}
}
if (!$port) {
$result = $host;
} else {
$result = $host.':'.$port;
}
return $result;
}
private function signRequest(
$method,
array $params,
array $meta) {
$input = self::encodeRequestDataForSignature(
$method,
$params,
$meta);
$signature = null;
$result = openssl_sign(
$input,
$signature,
$this->privateKey->openEnvelope());
if (!$result) {
throw new Exception(
pht('Unable to sign Conduit request with signing key.'));
}
return self::SIGNATURE_CONSIGN_1.base64_encode($signature);
}
public static function verifySignature(
$method,
array $params,
array $meta,
$openssl_public_key) {
$auth_type = idx($meta, 'auth.type');
switch ($auth_type) {
case self::AUTH_ASYMMETRIC:
break;
default:
throw new Exception(
pht(
'Unable to verify request signature, specified "%s" '.
'("%s") is unknown.',
'auth.type',
$auth_type));
}
$public_key = idx($meta, 'auth.key');
if (!strlen($public_key)) {
throw new Exception(
pht(
'Unable to verify request signature, no "%s" present in '.
'request protocol information.',
'auth.key'));
}
$signature = idx($meta, 'auth.signature');
if (!strlen($signature)) {
throw new Exception(
pht(
'Unable to verify request signature, no "%s" present '.
'in request protocol information.',
'auth.signature'));
}
$prefix = self::SIGNATURE_CONSIGN_1;
if (strncmp($signature, $prefix, strlen($prefix)) !== 0) {
throw new Exception(
pht(
'Unable to verify request signature, signature format is not '.
'known.'));
}
$signature = substr($signature, strlen($prefix));
$input = self::encodeRequestDataForSignature(
$method,
$params,
$meta);
$signature = base64_decode($signature);
$trap = new PhutilErrorTrap();
$result = @openssl_verify(
$input,
$signature,
$openssl_public_key);
$err = $trap->getErrorsAsString();
$trap->destroy();
if ($result === 1) {
// Signature is good.
return true;
} else if ($result === 0) {
// Signature is bad.
throw new Exception(
pht(
'Request signature verification failed: signature is not correct.'));
} else {
// Some kind of error.
if (strlen($err)) {
throw new Exception(
pht(
'OpenSSL encountered an error verifying the request signature: %s',
$err));
} else {
throw new Exception(
pht(
'OpenSSL encountered an unknown error verifying the request: %s',
$err));
}
}
}
private static function encodeRequestDataForSignature(
$method,
array $params,
array $meta) {
unset($meta['auth.signature']);
$structure = array(
'method' => $method,
'protocol' => $meta,
'parameters' => $params,
);
return self::encodeRawDataForSignature($structure);
}
public static function encodeRawDataForSignature($data) {
$out = array();
if (is_array($data)) {
if (phutil_is_natural_list($data)) {
$out[] = 'A';
$out[] = count($data);
$out[] = ':';
foreach ($data as $value) {
$out[] = self::encodeRawDataForSignature($value);
}
} else {
ksort($data);
$out[] = 'O';
$out[] = count($data);
$out[] = ':';
foreach ($data as $key => $value) {
$out[] = self::encodeRawDataForSignature($key);
$out[] = self::encodeRawDataForSignature($value);
}
}
} else if (is_string($data)) {
$out[] = 'S';
$out[] = strlen($data);
$out[] = ':';
$out[] = $data;
} else if (is_int($data)) {
$out[] = 'I';
$out[] = strlen((string)$data);
$out[] = ':';
$out[] = (string)$data;
} else if (is_null($data)) {
$out[] = 'N';
$out[] = ':';
} else if ($data === true) {
$out[] = 'B1:';
} else if ($data === false) {
$out[] = 'B0:';
} else {
throw new Exception(
pht(
'Unexpected data type in request data: %s.',
gettype($data)));
}
return implode('', $out);
}
}

View file

@ -0,0 +1,16 @@
<?php
final class ConduitClientException extends Exception {
protected $errorCode;
public function __construct($code, $info) {
parent::__construct("{$code}: {$info}");
$this->errorCode = $code;
}
public function getErrorCode() {
return $this->errorCode;
}
}

View file

@ -0,0 +1,90 @@
<?php
final class ConduitFuture extends FutureProxy {
private $client;
private $engine;
private $conduitMethod;
private $profilerCallID;
public function setClient(ConduitClient $client, $method) {
$this->client = $client;
$this->conduitMethod = $method;
return $this;
}
public function isReady() {
if ($this->profilerCallID === null) {
$profiler = PhutilServiceProfiler::getInstance();
$this->profilerCallID = $profiler->beginServiceCall(
array(
'type' => 'conduit',
'method' => $this->conduitMethod,
'size' => $this->getProxiedFuture()->getHTTPRequestByteLength(),
));
}
return parent::isReady();
}
protected function didReceiveResult($result) {
if ($this->profilerCallID !== null) {
$profiler = PhutilServiceProfiler::getInstance();
$profiler->endServiceCall(
$this->profilerCallID,
array());
}
list($status, $body, $headers) = $result;
if ($status->isError()) {
throw $status;
}
$capabilities = array();
foreach ($headers as $header) {
list($name, $value) = $header;
if (!strcasecmp($name, 'X-Conduit-Capabilities')) {
$capabilities = explode(' ', $value);
break;
}
}
if ($capabilities) {
$this->client->enableCapabilities($capabilities);
}
$raw = $body;
$shield = 'for(;;);';
if (!strncmp($raw, $shield, strlen($shield))) {
$raw = substr($raw, strlen($shield));
}
$data = null;
try {
$data = phutil_json_decode($raw);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht(
'Host returned HTTP/200, but invalid JSON data in response to '.
'a Conduit method call.'),
$ex);
}
if ($data['error_code']) {
throw new ConduitClientException(
$data['error_code'],
$data['error_info']);
}
$result = $data['result'];
$result = $this->client->didReceiveResponse(
$this->conduitMethod,
$result);
return $result;
}
}

View file

@ -0,0 +1,117 @@
<?php
final class ConduitSearchFuture
extends FutureAgent {
private $conduitEngine;
private $method;
private $constraints;
private $attachments;
private $objects = array();
private $cursor;
public function setConduitEngine(ArcanistConduitEngine $conduit_engine) {
$this->conduitEngine = $conduit_engine;
return $this;
}
public function getConduitEngine() {
return $this->conduitEngine;
}
public function setMethod($method) {
$this->method = $method;
return $this;
}
public function getMethod() {
return $this->method;
}
public function setConstraints(array $constraints) {
$this->constraints = $constraints;
return $this;
}
public function getConstraints() {
return $this->constraints;
}
public function setAttachments(array $attachments) {
$this->attachments = $attachments;
return $this;
}
public function getAttachments() {
return $this->attachments;
}
public function isReady() {
if ($this->hasResult()) {
return true;
}
$futures = $this->getFutures();
$future = head($futures);
if (!$future) {
$future = $this->newFuture();
}
if (!$future->isReady()) {
$this->setFutures(array($future));
return false;
} else {
$this->setFutures(array());
}
$result = $future->resolve();
foreach ($this->readResults($result) as $object) {
$this->objects[] = $object;
}
$cursor = idxv($result, array('cursor', 'after'));
if ($cursor === null) {
$this->setResult($this->objects);
return true;
}
$this->cursor = $cursor;
$future = $this->newFuture();
$this->setFutures(array($future));
return false;
}
private function newFuture() {
$engine = $this->getConduitEngine();
$method = $this->getMethod();
$constraints = $this->getConstraints();
$parameters = array(
'constraints' => $constraints,
);
if ($this->attachments) {
$parameters['attachments'] = $this->attachments;
}
if ($this->cursor !== null) {
$parameters['after'] = (string)$this->cursor;
}
$conduit_call = $engine->newCall($method, $parameters);
$conduit_future = $engine->newFuture($conduit_call);
return $conduit_future;
}
private function readResults(array $data) {
return idx($data, 'data');
}
}

View file

@ -0,0 +1,38 @@
<?php
abstract class FutureAgent
extends Future {
private $futures = array();
final protected function setFutures(array $futures) {
$this->futures = $futures;
}
final protected function getFutures() {
return $this->futures;
}
final public function getReadSockets() {
$sockets = array();
foreach ($this->getFutures() as $future) {
foreach ($future->getReadSockets() as $read_socket) {
$sockets[] = $read_socket;
}
}
return $sockets;
}
final public function getWriteSockets() {
$sockets = array();
foreach ($this->getFutures() as $future) {
foreach ($future->getWriteSockets() as $read_socket) {
$sockets[] = $read_socket;
}
}
return $sockets;
}
}

View file

@ -0,0 +1,34 @@
<?php
final class ConduitClientTestCase extends PhutilTestCase {
public function testConduitRequestEncoding() {
$input = array(
'z' => array(
'nothing' => null,
'emptystring' => '',
),
'empty' => array(
),
'list' => array(
15,
'quack',
true,
false,
),
'a' => array(
'key' => 'value',
'key2' => 'value2',
),
);
$expect =
'O4:S1:aO2:S3:keyS5:valueS4:key2S6:value2S5:emptyA0:S4:listA4:I2:15'.
'S5:quackB1:B0:S1:zO2:S11:emptystringS0:S7:nothingN:';
$this->assertEqual(
$expect,
ConduitClient::encodeRawDataForSignature($input));
}
}

View file

@ -0,0 +1,231 @@
<?php
final class ArcanistConfigurationEngine
extends Phobject {
private $workingCopy;
private $arguments;
private $toolset;
public function setWorkingCopy(ArcanistWorkingCopy $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
public function getWorkingCopy() {
return $this->workingCopy;
}
public function setArguments(PhutilArgumentParser $arguments) {
$this->arguments = $arguments;
return $this;
}
public function getArguments() {
if (!$this->arguments) {
throw new PhutilInvalidStateException('setArguments');
}
return $this->arguments;
}
public function newConfigurationSourceList() {
$list = new ArcanistConfigurationSourceList();
$list->addSource(new ArcanistDefaultsConfigurationSource());
$arguments = $this->getArguments();
// If the invoker has provided one or more configuration files with
// "--config-file" arguments, read those files instead of the system
// and user configuration files. Otherwise, read the system and user
// configuration files.
$config_files = $arguments->getArg('config-file');
if ($config_files) {
foreach ($config_files as $config_file) {
$list->addSource(new ArcanistFileConfigurationSource($config_file));
}
} else {
$system_path = $this->getSystemConfigurationFilePath();
$list->addSource(new ArcanistSystemConfigurationSource($system_path));
$user_path = $this->getUserConfigurationFilePath();
$list->addSource(new ArcanistUserConfigurationSource($user_path));
}
// If we're running in a working copy, load the ".arcconfig" and any
// local configuration.
$working_copy = $this->getWorkingCopy();
if ($working_copy) {
$project_path = $working_copy->getProjectConfigurationFilePath();
if ($project_path !== null) {
$list->addSource(new ArcanistProjectConfigurationSource($project_path));
}
$local_path = $working_copy->getLocalConfigurationFilePath();
if ($local_path !== null) {
$list->addSource(new ArcanistLocalConfigurationSource($local_path));
}
}
// If the invoker has provided "--config" arguments, parse those now.
$runtime_args = $arguments->getArg('config');
if ($runtime_args) {
$list->addSource(new ArcanistRuntimeConfigurationSource($runtime_args));
}
return $list;
}
private function getSystemConfigurationFilePath() {
if (phutil_is_windows()) {
return Filesystem::resolvePath(
'Phabricator/Arcanist/config',
getenv('ProgramData'));
} else {
return '/etc/arcconfig';
}
}
private function getUserConfigurationFilePath() {
if (phutil_is_windows()) {
return getenv('APPDATA').'/.arcrc';
} else {
return getenv('HOME').'/.arcrc';
}
}
public function newDefaults() {
$map = $this->newConfigOptionsMap();
return mpull($map, 'getDefaultValue');
}
public function newConfigOptionsMap() {
$extensions = $this->newEngineExtensions();
$map = array();
$alias_map = array();
foreach ($extensions as $extension) {
$options = $extension->newConfigurationOptions();
foreach ($options as $option) {
$key = $option->getKey();
$this->validateConfigOptionKey($key, $extension);
if (isset($map[$key])) {
throw new Exception(
pht(
'Configuration option ("%s") defined by extension "%s" '.
'conflicts with an existing option. Each option must have '.
'a unique key.',
$key,
get_class($extension)));
}
if (isset($alias_map[$key])) {
throw new Exception(
pht(
'Configuration option ("%s") defined by extension "%s" '.
'conflicts with an alias for another option ("%s"). The '.
'key and aliases of each option must be unique.',
$key,
get_class($extension),
$alias_map[$key]->getKey()));
}
$map[$key] = $option;
foreach ($option->getAliases() as $alias) {
$this->validateConfigOptionKey($alias, $extension, $key);
if (isset($map[$alias])) {
throw new Exception(
pht(
'Configuration option ("%s") defined by extension "%s" '.
'has an alias ("%s") which conflicts with an existing '.
'option. The key and aliases of each option must be '.
'unique.',
$key,
get_class($extension),
$alias));
}
if (isset($alias_map[$alias])) {
throw new Exception(
pht(
'Configuration option ("%s") defined by extension "%s" '.
'has an alias ("%s") which conflicts with the alias of '.
'another configuration option ("%s"). The key and aliases '.
'of each option must be unique.',
$key,
get_class($extension),
$alias,
$alias_map[$alias]->getKey()));
}
$alias_map[$alias] = $option;
}
}
}
return $map;
}
private function validateConfigOptionKey(
$key,
ArcanistConfigurationEngineExtension $extension,
$is_alias_of = null) {
$reserved = array(
// The presence of this key is used to detect old "~/.arcrc" files, so
// configuration options may not use it.
'config',
);
$reserved = array_fuse($reserved);
if (isset($reserved[$key])) {
throw new Exception(
pht(
'Extension ("%s") defines invalid configuration with key "%s". '.
'This key is reserved.',
get_class($extension),
$key));
}
$is_ok = preg_match('(^[a-z][a-z0-9._-]{2,}\z)', $key);
if (!$is_ok) {
if ($is_alias_of === null) {
throw new Exception(
pht(
'Extension ("%s") defines invalid configuration with key "%s". '.
'Configuration keys: may only contain lowercase letters, '.
'numbers, hyphens, underscores, and periods; must start with a '.
'letter; and must be at least three characters long.',
get_class($extension),
$key));
} else {
throw new Exception(
pht(
'Extension ("%s") defines invalid alias ("%s") for configuration '.
'key ("%s"). Configuration keys and aliases: may only contain '.
'lowercase letters, numbers, hyphens, underscores, and periods; '.
'must start with a letter; and must be at least three characters '.
'long.',
get_class($extension),
$key,
$is_alias_of));
}
}
}
private function newEngineExtensions() {
return id(new PhutilClassMapQuery())
->setAncestorClass('ArcanistConfigurationEngineExtension')
->setUniqueMethod('getExtensionKey')
->setContinueOnFailure(true)
->execute();
}
}

View file

@ -0,0 +1,10 @@
<?php
abstract class ArcanistConfigurationEngineExtension
extends Phobject {
final public function getExtensionKey() {
return $this->getPhobjectClassConstant('EXTENSIONKEY');
}
}

View file

@ -0,0 +1,202 @@
<?php
final class ArcanistConfigurationSourceList
extends Phobject {
private $sources = array();
private $configOptions;
public function addSource(ArcanistConfigurationSource $source) {
$this->sources[] = $source;
return $this;
}
public function getSources() {
return $this->sources;
}
private function getSourcesWithScopes($scopes) {
if ($scopes !== null) {
$scopes = array_fuse($scopes);
}
$results = array();
foreach ($this->getSources() as $source) {
if ($scopes !== null) {
$scope = $source->getConfigurationSourceScope();
if ($scope === null) {
continue;
}
if (!isset($scopes[$scope])) {
continue;
}
}
$results[] = $source;
}
return $results;
}
public function getWritableSourceFromScope($scope) {
$sources = $this->getSourcesWithScopes(array($scope));
$writable = array();
foreach ($sources as $source) {
if (!$source->isWritableConfigurationSource()) {
continue;
}
$writable[] = $source;
}
if (!$writable) {
throw new Exception(
pht(
'Unable to write configuration: there is no writable configuration '.
'source in the "%s" scope.',
$scope));
}
if (count($writable) > 1) {
throw new Exception(
pht(
'Unable to write configuration: more than one writable source '.
'exists in the "%s" scope.',
$scope));
}
return head($writable);
}
public function getConfig($key) {
$option = $this->getConfigOption($key);
$values = $this->getStorageValueList($key);
return $option->getValueFromStorageValueList($values);
}
public function getConfigFromScopes($key, array $scopes) {
$option = $this->getConfigOption($key);
$values = $this->getStorageValueListFromScopes($key, $scopes);
return $option->getValueFromStorageValueList($values);
}
public function getStorageValueList($key) {
return $this->getStorageValueListFromScopes($key, null);
}
private function getStorageValueListFromScopes($key, $scopes) {
$values = array();
foreach ($this->getSourcesWithScopes($scopes) as $source) {
if ($source->hasValueForKey($key)) {
$value = $source->getValueForKey($key);
$values[] = new ArcanistConfigurationSourceValue(
$source,
$source->getValueForKey($key));
}
}
return $values;
}
public function getConfigOption($key) {
$options = $this->getConfigOptions();
if (!isset($options[$key])) {
throw new Exception(
pht(
'Configuration option ("%s") is unrecognized. You can only read '.
'recognized configuration options.',
$key));
}
return $options[$key];
}
public function setConfigOptions(array $config_options) {
assert_instances_of($config_options, 'ArcanistConfigOption');
$config_options = mpull($config_options, null, 'getKey');
$this->configOptions = $config_options;
return $this;
}
public function getConfigOptions() {
if ($this->configOptions === null) {
throw new PhutilInvalidStateException('setConfigOptions');
}
return $this->configOptions;
}
public function validateConfiguration(ArcanistRuntime $runtime) {
$options = $this->getConfigOptions();
$aliases = array();
foreach ($options as $key => $option) {
foreach ($option->getAliases() as $alias) {
$aliases[$alias] = $key;
}
}
// TOOLSETS: Handle the case where config specifies both a value and an
// alias for that value. The alias should be ignored and we should emit
// a warning. This also needs to be implemented when actually reading
// configuration.
$value_lists = array();
foreach ($this->getSources() as $source) {
$keys = $source->getAllKeys();
foreach ($keys as $key) {
$resolved_key = idx($aliases, $key, $key);
$option = idx($options, $resolved_key);
// If there's no option object for this config, this value is
// unrecognized. Sources are free to handle this however they want:
// for config files we emit a warning; for "--config" we fatal.
if (!$option) {
$source->didReadUnknownOption($runtime, $key);
continue;
}
$raw_value = $source->getValueForKey($key);
// Make sure we can convert whatever value the configuration source is
// providing into a legitimate runtime value.
try {
$value = $raw_value;
if ($source->isStringSource()) {
$value = $option->getStorageValueFromStringValue($value);
}
$option->getValueFromStorageValue($value);
$value_lists[$resolved_key][] = new ArcanistConfigurationSourceValue(
$source,
$raw_value);
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Configuration value ("%s") defined in source "%s" is not '.
'valid.',
$key,
$source->getSourceDisplayName()),
$ex);
}
}
}
// Make sure each value list can be merged.
foreach ($value_lists as $key => $value_list) {
try {
$options[$key]->getValueFromStorageValueList($value_list);
} catch (Exception $ex) {
throw $ex;
}
}
}
}

View file

@ -0,0 +1,22 @@
<?php
final class ArcanistConfigurationSourceValue
extends Phobject {
private $source;
private $value;
public function __construct(ArcanistConfigurationSource $source, $value) {
$this->source = $source;
$this->value = $value;
}
public function getConfigurationSource() {
return $this->source;
}
public function getValue() {
return $this->value;
}
}

View file

@ -0,0 +1,151 @@
<?php
final class ArcanistArcConfigurationEngineExtension
extends ArcanistConfigurationEngineExtension {
const EXTENSIONKEY = 'arc';
const KEY_ALIASES = 'aliases';
public function newConfigurationOptions() {
// TOOLSETS: Restore "load", and maybe this other stuff.
/*
'load' => array(
'type' => 'list',
'legacy' => 'phutil_libraries',
'help' => pht(
'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.'),
'default' => array(),
'example' => '["/var/arc/customlib/src"]',
),
'arc.feature.start.default' => array(
'type' => 'string',
'help' => pht(
'The name of the default branch to create the new feature branch '.
'off of.'),
'example' => '"develop"',
),
'arc.land.onto.default' => array(
'type' => 'string',
'help' => pht(
'The name of the default branch to land changes onto when '.
'`%s` is run.',
'arc land'),
'example' => '"develop"',
),
'arc.autostash' => array(
'type' => 'bool',
'help' => pht(
'Whether %s should permit the automatic stashing of changes in the '.
'working directory when requiring a clean working copy. This option '.
'should only be used when users understand how to restore their '.
'working directory from the local stash if an Arcanist operation '.
'causes an unrecoverable error.',
'arc'),
'default' => false,
'example' => 'false',
),
'history.immutable' => array(
'type' => 'bool',
'legacy' => 'immutable_history',
'help' => pht(
'If true, %s 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.',
'arc'),
'example' => 'false',
),
'editor' => array(
'type' => 'string',
'help' => pht(
'Command to use to invoke an interactive editor, like `%s` or `%s`. '.
'This setting overrides the %s environmental variable.',
'nano',
'vim',
'EDITOR'),
'example' => '"nano"',
),
'https.cabundle' => array(
'type' => 'string',
'help' => pht(
"Path to a custom CA bundle file to be used for arcanist's cURL ".
"calls. This is used primarily when your conduit endpoint is ".
"behind HTTPS signed by your organization's internal CA."),
'example' => 'support/yourca.pem',
),
'browser' => array(
'type' => 'string',
'help' => pht('Command to use to invoke a web browser.'),
'example' => '"gnome-www-browser"',
),
*/
return array(
id(new ArcanistStringConfigOption())
->setKey('base')
->setSummary(pht('Ruleset for selecting commit ranges.'))
->setHelp(
pht(
'Base commit ruleset to invoke when determining the start of a '.
'commit range. See "Arcanist User Guide: Commit Ranges" for '.
'details.'))
->setExamples(
array(
'arc:amended, arc:prompt',
)),
id(new ArcanistStringConfigOption())
->setKey('repository')
->setAliases(
array(
'repository.callsign',
))
->setSummary(pht('Repository for the current working copy.'))
->setHelp(
pht(
'Associate the working copy with a specific Phabricator '.
'repository. Normally, `arc` can figure this association out on '.
'its own, but if your setup is unusual you can use this option '.
'to tell it what the desired value is.'))
->setExamples(
array(
'libexample',
'XYZ',
'R123',
'123',
)),
id(new ArcanistStringConfigOption())
->setKey('phabricator.uri')
->setAliases(
array(
'conduit_uri',
'default',
))
->setSummary(pht('Phabricator install to connect to.'))
->setHelp(
pht(
'Associates this working copy with a specific installation of '.
'Phabricator.'))
->setExamples(
array(
'https://phabricator.mycompany.com/',
)),
id(new ArcanistAliasesConfigOption())
->setKey(self::KEY_ALIASES)
->setDefaultValue(array())
->setSummary(pht('List of command aliases.'))
->setHelp(
pht(
'Configured command aliases. Use the "alias" workflow to define '.
'aliases.')),
);
}
}

View file

@ -0,0 +1,47 @@
<?php
final class ArcanistAliasesConfigOption
extends ArcanistListConfigOption {
public function getType() {
return 'list<alias>';
}
public function getValueFromStorageValue($value) {
if (!is_array($value)) {
throw new Exception(pht('Expected a list or dictionary!'));
}
$aliases = array();
foreach ($value as $key => $spec) {
$aliases[] = ArcanistAlias::newFromConfig($key, $spec);
}
return $aliases;
}
protected function didReadStorageValueList(array $list) {
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
$results = array();
foreach ($list as $spec) {
$source = $spec->getConfigurationSource();
$value = $spec->getValue();
$value->setConfigurationSource($source);
$results[] = $value;
}
return $results;
}
public function getDisplayValueFromValue($value) {
return pht('Use the "alias" workflow to review aliases.');
}
public function getStorageValueFromValue($value) {
return mpull($value, 'getStorageDictionary');
}
}

View file

@ -0,0 +1,100 @@
<?php
abstract class ArcanistConfigOption
extends Phobject {
private $key;
private $help;
private $summary;
private $aliases = array();
private $examples = array();
private $defaultValue;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setAliases($aliases) {
$this->aliases = $aliases;
return $this;
}
public function getAliases() {
return $this->aliases;
}
public function setSummary($summary) {
$this->summary = $summary;
return $this;
}
public function getSummary() {
return $this->summary;
}
public function setHelp($help) {
$this->help = $help;
return $this;
}
public function getHelp() {
return $this->help;
}
public function setExamples(array $examples) {
$this->examples = $examples;
return $this;
}
public function getExamples() {
return $this->examples;
}
public function setDefaultValue($default_value) {
$this->defaultValue = $default_value;
return $this;
}
public function getDefaultValue() {
return $this->defaultValue;
}
abstract public function getType();
abstract public function getValueFromStorageValueList(array $list);
abstract public function getValueFromStorageValue($value);
abstract public function getDisplayValueFromValue($value);
abstract public function getStorageValueFromValue($value);
public function getStorageValueFromStringValue($value) {
throw new Exception(
pht(
'This configuration option ("%s") does not support runtime definition '.
'with "--config".',
$this->getKey()));
}
protected function getStorageValueFromSourceValue(
ArcanistConfigurationSourceValue $source_value) {
$value = $source_value->getValue();
$source = $source_value->getConfigurationSource();
if ($source->isStringSource()) {
$value = $this->getStorageValueFromStringValue($value);
}
return $value;
}
public function writeValue(ArcanistConfigurationSource $source, $value) {
$value = $this->getStorageValueFromValue($value);
$source->setStorageValueForKey($this->getKey(), $value);
}
}

View file

@ -0,0 +1,32 @@
<?php
abstract class ArcanistListConfigOption
extends ArcanistConfigOption {
public function getValueFromStorageValueList(array $list) {
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
$result_list = array();
foreach ($list as $source_value) {
$source = $source_value->getConfigurationSource();
$storage_value = $this->getStorageValueFromSourceValue($source_value);
$items = $this->getValueFromStorageValue($storage_value);
foreach ($items as $item) {
$result_list[] = new ArcanistConfigurationSourceValue(
$source,
$item);
}
}
$result_list = $this->didReadStorageValueList($result_list);
return $result_list;
}
protected function didReadStorageValueList(array $list) {
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
return mpull($list, 'getValue');
}
}

View file

@ -0,0 +1,19 @@
<?php
abstract class ArcanistScalarConfigOption
extends ArcanistConfigOption {
public function getValueFromStorageValueList(array $list) {
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
$source_value = last($list);
$storage_value = $this->getStorageValueFromSourceValue($source_value);
return $this->getValueFromStorageValue($storage_value);
}
public function getValueFromStorageValue($value) {
return $value;
}
}

View file

@ -0,0 +1,22 @@
<?php
final class ArcanistStringConfigOption
extends ArcanistScalarConfigOption {
public function getType() {
return 'string';
}
public function getStorageValueFromStringValue($value) {
return (string)$value;
}
public function getDisplayValueFromValue($value) {
return $value;
}
public function getStorageValueFromValue($value) {
return $value;
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* This option type makes it easier to manage unknown options with unknown
* types.
*/
final class ArcanistWildConfigOption
extends ArcanistConfigOption {
public function getType() {
return 'wild';
}
public function getStorageValueFromStringValue($value) {
return (string)$value;
}
public function getDisplayValueFromValue($value) {
return json_encode($value);
}
public function getValueFromStorageValueList(array $list) {
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
$source_value = last($list);
$storage_value = $this->getStorageValueFromSourceValue($source_value);
return $this->getValueFromStorageValue($storage_value);
}
public function getValueFromStorageValue($value) {
return $value;
}
public function getStorageValueFromValue($value) {
return $value;
}
}

View file

@ -0,0 +1,39 @@
<?php
abstract class ArcanistConfigurationSource
extends Phobject {
const SCOPE_USER = 'user';
abstract public function getSourceDisplayName();
abstract public function getAllKeys();
abstract public function hasValueForKey($key);
abstract public function getValueForKey($key);
public function getConfigurationSourceScope() {
return null;
}
public function isStringSource() {
return false;
}
public function isWritableConfigurationSource() {
return false;
}
public function didReadUnknownOption(ArcanistRuntime $runtime, $key) {
// TOOLSETS: Restore this warning once the new "arc" flow is in better
// shape.
return;
$runtime->getLogEngine()->writeWarning(
pht('UNKNOWN CONFIGURATION'),
pht(
'Ignoring unrecognized configuration option ("%s") from source: %s.',
$key,
$this->getSourceDisplayName()));
}
}

View file

@ -0,0 +1,17 @@
<?php
final class ArcanistDefaultsConfigurationSource
extends ArcanistDictionaryConfigurationSource {
public function getSourceDisplayName() {
return pht('Builtin Defaults');
}
public function __construct() {
$values = id(new ArcanistConfigurationEngine())
->newDefaults();
parent::__construct($values);
}
}

View file

@ -0,0 +1,44 @@
<?php
abstract class ArcanistDictionaryConfigurationSource
extends ArcanistConfigurationSource {
private $values;
public function __construct(array $dictionary) {
$this->values = $dictionary;
}
public function getAllKeys() {
return array_keys($this->values);
}
public function hasValueForKey($key) {
return array_key_exists($key, $this->values);
}
public function getValueForKey($key) {
if (!$this->hasValueForKey($key)) {
throw new Exception(
pht(
'Configuration source ("%s") has no value for key ("%s").',
get_class($this),
$key));
}
return $this->values[$key];
}
public function setStorageValueForKey($key, $value) {
$this->values[$key] = $value;
$this->writeToStorage($this->values);
return $this;
}
protected function writeToStorage($values) {
throw new PhutilMethodNotImplementedException();
}
}

View file

@ -0,0 +1,10 @@
<?php
final class ArcanistFileConfigurationSource
extends ArcanistFilesystemConfigurationSource {
public function getFileKindDisplayName() {
return pht('Config File');
}
}

View file

@ -0,0 +1,45 @@
<?php
abstract class ArcanistFilesystemConfigurationSource
extends ArcanistDictionaryConfigurationSource {
private $path;
public function __construct($path) {
$this->path = $path;
$values = array();
if (Filesystem::pathExists($path)) {
$contents = Filesystem::readFile($path);
if (strlen(trim($contents))) {
$values = phutil_json_decode($contents);
}
}
$values = $this->didReadFilesystemValues($values);
parent::__construct($values);
}
public function getPath() {
return $this->path;
}
public function getSourceDisplayName() {
return pht('%s (%s)', $this->getFileKindDisplayName(), $this->getPath());
}
abstract public function getFileKindDisplayName();
protected function didReadFilesystemValues(array $values) {
return $values;
}
protected function writeToStorage($values) {
$content = id(new PhutilJSON())
->encodeFormatted($values);
Filesystem::writeFile($this->path, $content);
}
}

View file

@ -0,0 +1,10 @@
<?php
final class ArcanistLocalConfigurationSource
extends ArcanistWorkingCopyConfigurationSource {
public function getFileKindDisplayName() {
return pht('Local Config File');
}
}

View file

@ -0,0 +1,10 @@
<?php
final class ArcanistProjectConfigurationSource
extends ArcanistWorkingCopyConfigurationSource {
public function getFileKindDisplayName() {
return pht('Project Config File');
}
}

View file

@ -0,0 +1,49 @@
<?php
final class ArcanistRuntimeConfigurationSource
extends ArcanistDictionaryConfigurationSource {
public function __construct(array $argv) {
$map = array();
foreach ($argv as $raw) {
$parts = explode('=', $raw, 2);
if (count($parts) !== 2) {
throw new PhutilArgumentUsageException(
pht(
'Configuration option "%s" is not valid. Configuration options '.
'passed with command line flags must be in the form "name=value".',
$raw));
}
list($key, $value) = $parts;
if (isset($map[$key])) {
throw new PhutilArgumentUsageException(
pht(
'Configuration option "%s" was provided multiple times with '.
'"--config" flags. Specify each option no more than once.',
$key));
}
$map[$key] = $value;
}
parent::__construct($map);
}
public function didReadUnknownOption(ArcanistRuntime $runtime, $key) {
throw new PhutilArgumentUsageException(
pht(
'Configuration option ("%s") specified with "--config" flag is not '.
'a recognized option.',
$key));
}
public function getSourceDisplayName() {
return pht('Runtime "--config" Flags');
}
public function isStringSource() {
return true;
}
}

View file

@ -0,0 +1,10 @@
<?php
final class ArcanistSystemConfigurationSource
extends ArcanistFilesystemConfigurationSource {
public function getFileKindDisplayName() {
return pht('System Config File');
}
}

View file

@ -0,0 +1,56 @@
<?php
final class ArcanistUserConfigurationSource
extends ArcanistFilesystemConfigurationSource {
public function getFileKindDisplayName() {
return pht('User Config File');
}
public function isWritableConfigurationSource() {
return true;
}
public function getConfigurationSourceScope() {
return ArcanistConfigurationSource::SCOPE_USER;
}
protected function didReadFilesystemValues(array $values) {
// Before toolsets, the "~/.arcrc" file had separate top-level keys for
// "config", "hosts", and "aliases". Transform this older file format into
// a more modern format.
if (!isset($values['config'])) {
// This isn't an older file, so just return the values unmodified.
return $values;
}
// Make the keys in "config" top-level keys. Then add in whatever other
// top level keys exist, other than "config", preferring keys that already
// exist in the "config" dictionary.
// For example, this older configuration file:
//
// {
// "hosts": ...,
// "config": {x: ..., y: ...},
// "aliases": ...
// }
//
// ...becomes this modern file:
//
// {
// "x": ...,
// "y": ...,
// "hosts": ...,
// "aliases": ...
// }
$result = $values['config'];
unset($values['config']);
$result += $values;
return $result;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class ArcanistWorkingCopyConfigurationSource
extends ArcanistFilesystemConfigurationSource {}

View file

@ -81,53 +81,6 @@ class ArcanistConfiguration extends Phobject {
return $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($configuration_manager);
list($new_command, $args) = ArcanistAliasWorkflow::resolveAliases(
$command,
$this,
$args,
$configuration_manager);
$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(
"[%s: 'arc %s' -> $ %s]",
pht('alias'),
$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 = $this->buildWorkflow($new_command);
if ($workflow) {
$console->writeLog(
"[%s: 'arc %s' -> 'arc %s']\n",
pht('alias'),
$command,
$full_alias);
$command = $new_command;
return $workflow;
}
}
$all = array_keys($this->buildAllWorkflows());
// We haven't found a real command or an alias, so try to locate a command

View file

@ -270,7 +270,13 @@ final class ArcanistConfigurationManager extends Phobject {
}
public function readUserArcConfig() {
return idx($this->readUserConfigurationFile(), 'config', array());
$config = $this->readUserConfigurationFile();
if (isset($config['config'])) {
$config = $config['config'];
}
return $config;
}
public function writeUserArcConfig(array $options) {

View file

@ -0,0 +1,48 @@
<?php
abstract class ArcanistSetting
extends Phobject {
final public function getSettingKey() {
return $this->getPhobjectClassConstant('SETTINGKEY', 32);
}
public function getAliases() {
return array();
}
abstract public function getHelp();
abstract public function getType();
public function getExample() {
return null;
}
final public function getLegacyDictionary() {
$result = array(
'type' => $this->getType(),
'help' => $this->getHelp(),
);
$example = $this->getExample();
if ($example !== null) {
$result['example'] = $example;
}
$aliases = $this->getAliases();
if ($aliases) {
$result['legacy'] = head($aliases);
}
return $result;
}
final public static function getAllSettings() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getSettingKey')
->setSortMethod('getSettingKey')
->execute();
}
}

View file

@ -3,7 +3,7 @@
final class ArcanistSettings extends Phobject {
private function getOptions() {
return array(
$legacy_builtins = array(
'default' => array(
'type' => 'string',
'help' => pht(
@ -80,19 +80,6 @@ final class ArcanistSettings extends Phobject {
'arc land'),
'example' => '"develop"',
),
'arc.lint.cache' => array(
'type' => 'bool',
'help' => pht(
'Enable the lint cache by default. When enabled, `%s` attempts to '.
'use cached results if possible. Currently, the cache is not always '.
'invalidated correctly and may cause `%s` to report incorrect '.
'results, particularly while developing linters. This is probably '.
'worth enabling only if your linters are very slow.',
'arc lint',
'arc lint'),
'default' => false,
'example' => 'false',
),
'history.immutable' => array(
'type' => 'bool',
'legacy' => 'immutable_history',
@ -121,14 +108,6 @@ final class ArcanistSettings extends Phobject {
"behind HTTPS signed by your organization's internal CA."),
'example' => 'support/yourca.pem',
),
'https.blindly-trust-domains' => array(
'type' => 'list',
'help' => pht(
'List of domains to blindly trust SSL certificates for. '.
'Disables peer verification.'),
'default' => array(),
'example' => '["secure.mycompany.com"]',
),
'browser' => array(
'type' => 'string',
'help' => pht('Command to use to invoke a web browser.'),
@ -140,16 +119,6 @@ final class ArcanistSettings extends Phobject {
'default' => array(),
'example' => '["ExampleEventListener"]',
),
'http.basicauth.user' => array(
'type' => 'string',
'help' => pht('Username to use for basic auth over HTTP transports.'),
'example' => '"bob"',
),
'http.basicauth.pass' => array(
'type' => 'string',
'help' => pht('Password to use for basic auth over HTTP transports.'),
'example' => '"bobhasasecret"',
),
'arc.autostash' => array(
'type' => 'bool',
'help' => pht(
@ -168,6 +137,16 @@ final class ArcanistSettings extends Phobject {
'Configured command aliases. Use "arc alias" to define aliases.'),
),
);
$settings = ArcanistSetting::getAllSettings();
foreach ($settings as $key => $setting) {
$settings[$key] = $setting->getLegacyDictionary();
}
$results = $settings + $legacy_builtins;
ksort($results);
return $results;
}
private function getOption($key) {

View file

@ -0,0 +1,295 @@
<?php
/**
* Provides access to the command-line console. Instead of reading from or
* writing to stdin/stdout/stderr directly, this class provides a richer API
* including support for ANSI color and formatting, convenience methods for
* prompting the user, and the ability to interact with stdin/stdout/stderr
* in some other process instead of this one.
*
* @task construct Construction
* @task interface Interfacing with the User
* @task internal Internals
*/
final class PhutilConsole extends Phobject {
private static $console;
private $server;
private $channel;
private $messages = array();
private $flushing = false;
private $disabledTypes;
/* -( Console Construction )----------------------------------------------- */
/**
* Use @{method:newLocalConsole} or @{method:newRemoteConsole} to construct
* new consoles.
*
* @task construct
*/
private function __construct() {
$this->disabledTypes = new PhutilArrayWithDefaultValue();
}
/**
* Get the current console. If there's no active console, a new local console
* is created (see @{method:newLocalConsole} for details). You can change the
* active console with @{method:setConsole}.
*
* @return PhutilConsole Active console.
* @task construct
*/
public static function getConsole() {
if (empty(self::$console)) {
self::setConsole(self::newLocalConsole());
}
return self::$console;
}
/**
* Set the active console.
*
* @param PhutilConsole
* @return void
* @task construct
*/
public static function setConsole(PhutilConsole $console) {
self::$console = $console;
}
/**
* Create a new console attached to stdin/stdout/stderr of this process.
* This is how consoles normally work -- for instance, writing output with
* @{method:writeOut} prints directly to stdout. If you don't create a
* console explicitly, a new local console is created for you.
*
* @return PhutilConsole A new console which operates on the pipes of this
* process.
* @task construct
*/
public static function newLocalConsole() {
return self::newConsoleForServer(new PhutilConsoleServer());
}
public static function newConsoleForServer(PhutilConsoleServer $server) {
$console = new PhutilConsole();
$console->server = $server;
return $console;
}
public static function newRemoteConsole() {
$io_channel = new PhutilSocketChannel(
fopen('php://stdin', 'r'),
fopen('php://stdout', 'w'));
$protocol_channel = new PhutilPHPObjectProtocolChannel($io_channel);
$console = new PhutilConsole();
$console->channel = $protocol_channel;
return $console;
}
/* -( Interfacing with the User )------------------------------------------ */
public function confirm($prompt, $default = false) {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_CONFIRM)
->setData(
array(
'prompt' => $prompt,
'default' => $default,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
public function prompt($prompt, $history = '') {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_PROMPT)
->setData(
array(
'prompt' => $prompt,
'history' => $history,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
public function sendMessage($data) {
$message = id(new PhutilConsoleMessage())->setData($data);
return $this->writeMessage($message);
}
public function writeOut($pattern /* , ... */) {
$args = func_get_args();
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_OUT, $args);
}
public function writeErr($pattern /* , ... */) {
$args = func_get_args();
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_ERR, $args);
}
public function writeLog($pattern /* , ... */) {
$args = func_get_args();
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_LOG, $args);
}
public function beginRedirectOut() {
// We need as small buffer as possible. 0 means infinite, 1 means 4096 in
// PHP < 5.4.0.
ob_start(array($this, 'redirectOutCallback'), 2);
$this->flushing = true;
}
public function endRedirectOut() {
$this->flushing = false;
ob_end_flush();
}
/* -( Internals )---------------------------------------------------------- */
// Must be public because it is called from output buffering.
public function redirectOutCallback($string) {
if (strlen($string)) {
$this->flushing = false;
$this->writeOut('%s', $string);
$this->flushing = true;
}
return '';
}
private function writeTextMessage($type, array $argv) {
$message = id(new PhutilConsoleMessage())
->setType($type)
->setData($argv);
$this->writeMessage($message);
return $this;
}
private function writeMessage(PhutilConsoleMessage $message) {
if ($this->disabledTypes[$message->getType()]) {
return $this;
}
if ($this->flushing) {
ob_flush();
}
if ($this->channel) {
$this->channel->write($message);
$this->channel->flush();
} else {
$response = $this->server->handleMessage($message);
if ($response) {
$this->messages[] = $response;
}
}
return $this;
}
private function waitForMessage() {
if ($this->channel) {
$message = $this->channel->waitForMessage();
} else if ($this->messages) {
$message = array_shift($this->messages);
} else {
throw new Exception(
pht(
'%s called with no messages!',
__FUNCTION__.'()'));
}
return $message;
}
public function getServer() {
return $this->server;
}
private function disableMessageType($type) {
$this->disabledTypes[$type] += 1;
return $this;
}
private function enableMessageType($type) {
if ($this->disabledTypes[$type] == 0) {
throw new Exception(pht("Message type '%s' is already enabled!", $type));
}
$this->disabledTypes[$type] -= 1;
return $this;
}
public function disableOut() {
return $this->disableMessageType(PhutilConsoleMessage::TYPE_OUT);
}
public function enableOut() {
return $this->enableMessageType(PhutilConsoleMessage::TYPE_OUT);
}
public function isLogEnabled() {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_ENABLED)
->setData(
array(
'which' => PhutilConsoleMessage::TYPE_LOG,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
public function isErrATTY() {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_TTY)
->setData(
array(
'which' => PhutilConsoleMessage::TYPE_ERR,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
public function getErrCols() {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_COLS)
->setData(
array(
'which' => PhutilConsoleMessage::TYPE_ERR,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
}

View file

@ -0,0 +1,98 @@
<?php
final class PhutilConsoleFormatter extends Phobject {
private static $colorCodes = array(
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
'default' => 9,
);
private static $disableANSI;
public static function disableANSI($disable) {
self::$disableANSI = $disable;
}
public static function getDisableANSI() {
if (self::$disableANSI === null) {
$term = phutil_utf8_strtolower(getenv('TERM'));
// ansicon enables ANSI support on Windows
if (!$term && getenv('ANSICON')) {
$term = 'ansi';
}
if (phutil_is_windows() && $term !== 'cygwin' && $term !== 'ansi') {
self::$disableANSI = true;
} else if (!defined('STDOUT')) {
self::$disableANSI = true;
} else if (function_exists('posix_isatty') && !posix_isatty(STDOUT)) {
self::$disableANSI = true;
} else {
self::$disableANSI = false;
}
}
return self::$disableANSI;
}
public static function formatString($format /* ... */) {
$args = func_get_args();
$args[0] = self::interpretFormat($args[0]);
return call_user_func_array('sprintf', $args);
}
public static function replaceColorCode($matches) {
$codes = self::$colorCodes;
$offset = 30 + $codes[$matches[2]];
$default = 39;
if ($matches[1] == 'bg') {
$offset += 10;
$default += 10;
}
return chr(27).'['.$offset.'m'.$matches[3].chr(27).'['.$default.'m';
}
public static function interpretFormat($format) {
$colors = implode('|', array_keys(self::$colorCodes));
// Sequence should be preceded by start-of-string or non-backslash
// escaping.
$bold_re = '/(?<![\\\\])\*\*(.*)\*\*/sU';
$underline_re = '/(?<![\\\\])__(.*)__/sU';
$invert_re = '/(?<![\\\\])##(.*)##/sU';
if (self::getDisableANSI()) {
$format = preg_replace($bold_re, '\1', $format);
$format = preg_replace($underline_re, '\1', $format);
$format = preg_replace($invert_re, '\1', $format);
$format = preg_replace(
'@<(fg|bg):('.$colors.')>(.*)</\1>@sU',
'\3',
$format);
} else {
$esc = chr(27);
$bold = $esc.'[1m'.'\\1'.$esc.'[m';
$underline = $esc.'[4m'.'\\1'.$esc.'[m';
$invert = $esc.'[7m'.'\\1'.$esc.'[m';
$format = preg_replace($bold_re, $bold, $format);
$format = preg_replace($underline_re, $underline, $format);
$format = preg_replace($invert_re, $invert, $format);
$format = preg_replace_callback(
'@<(fg|bg):('.$colors.')>(.*)</\1>@sU',
array(__CLASS__, 'replaceColorCode'),
$format);
}
// Remove backslash escaping
return preg_replace('/\\\\(\*\*.*\*\*|__.*__|##.*##)/sU', '\1', $format);
}
}

View file

@ -0,0 +1,39 @@
<?php
final class PhutilConsoleMessage extends Phobject {
const TYPE_CONFIRM = 'phutil:confirm';
const TYPE_PROMPT = 'phutil:prompt';
const TYPE_INPUT = 'phutil:in';
const TYPE_OUT = 'phutil:out';
const TYPE_ERR = 'phutil:err';
const TYPE_LOG = 'phutil:log';
const TYPE_TTY = 'phutil:tty?';
const TYPE_IS_TTY = 'phutil:tty!';
const TYPE_COLS = 'phutil:cols?';
const TYPE_COL_WIDTH = 'phutil:cols!';
const TYPE_ENABLED = 'phutil:enabled?';
const TYPE_IS_ENABLED = 'phutil:enabled!';
private $type;
private $data;
public function setData($data) {
$this->data = $data;
return $this;
}
public function getData() {
return $this->data;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
}

View file

@ -0,0 +1,65 @@
<?php
final class PhutilConsoleMetrics extends Phobject {
const DEFAULT_CONSOLE = 'default';
private static $consoles = array();
private $width = false;
public static function getNamedConsole($key) {
if (!isset(self::$consoles[$key])) {
self::$consoles[$key] = new self();
}
return self::$consoles[$key];
}
public static function getDefaultConsole() {
return self::getNamedConsole(self::DEFAULT_CONSOLE);
}
public function didGetWINCHSignal() {
// When we receive a "WINCH" ("WINdow CHange") signal, clear the cached
// information we have about the terminal.
$this->width = false;
return $this;
}
public function getTerminalWidth() {
if ($this->width === false) {
$this->width = $this->computeTerminalWidth();
}
return $this->width;
}
private function computeTerminalWidth() {
if (phutil_is_windows()) {
// TODO: Figure out how to do this on Windows.
return null;
}
$tmp = new TempFile();
// NOTE: We can't just execute this because it won't be connected to a TTY
// if we do.
$err = id(new PhutilExecPassthru('tput cols > %s', $tmp))
->resolve();
$stdout = Filesystem::readFile($tmp);
unset($tmp);
if ($err) {
return null;
}
$width = (int)trim($stdout);
if ($width > 0) {
return $width;
}
return null;
}
}

View file

@ -0,0 +1,179 @@
<?php
/**
* Show a progress bar on the console. Usage:
*
* // Create a progress bar, and configure the total amount of work that
* // needs to be done.
* $bar = id(new PhutilConsoleProgressBar())
* ->setTotal(count($stuff));
*
* // As you complete the work, update the progress bar.
* foreach ($stuff as $thing) {
* do_stuff($thing);
* $bar->update(1);
* }
*
* // When complete, mark the work done to clear the bar.
* $bar->done();
*
* The progress bar attempts to account for various special cases, notably:
*
* - If stderr is not a TTY, the bar will not be drawn (for example, if
* it is being piped to a log file).
* - If the Phutil log output is enabled (usually because `--trace` was
* specified), the bar will not be drawn.
* - The bar will be resized to the width of the console if possible.
*
*/
final class PhutilConsoleProgressBar extends Phobject {
private $work;
private $done;
private $drawn;
private $console;
private $finished;
private $lastUpdate;
private $quiet = false;
public function setConsole(PhutilConsole $console) {
$this->console = $console;
return $this;
}
private function getConsole() {
if ($this->console) {
return $this->console;
}
return PhutilConsole::getConsole();
}
public function setTotal($work) {
$this->work = $work;
$this->redraw();
return $this;
}
public function setQuiet($quiet) {
$this->quiet = $quiet;
return $this;
}
public function update($work) {
$this->done += $work;
$this->redraw();
return $this;
}
private function redraw() {
if ($this->lastUpdate + 0.1 > microtime(true)) {
// We redrew the bar very recently; skip this update.
return $this;
}
return $this->draw();
}
/**
* Explicitly redraw the bar.
*
* Normally, the progress bar is automatically redrawn periodically, but
* you may want to force it to draw.
*
* For example, we force a draw after pre-filling the bar when resuming
* large file uploads in `arc upload`. Otherwise, the bar may sit at 0%
* until the first chunk completes.
*/
public function draw() {
if ($this->quiet) {
return;
}
if ($this->finished) {
return;
}
if (!$this->work) {
// There's no work to be done, so don't draw the bar.
return;
}
$console = $this->getConsole();
if ($console->isErrATTY() === false) {
return;
}
if ($console->isLogEnabled()) {
return;
}
// Width of the stuff other than the progress bar itself.
$chrome_width = strlen('[] 100.0% ');
$char_width = $this->getWidth();
if ($char_width < $chrome_width) {
return;
}
$this->lastUpdate = microtime(true);
if (!$this->drawn) {
$this->drawn = true;
}
$percent = $this->done / $this->work;
$max_width = $char_width - $chrome_width;
$bar_width = $percent * $max_width;
$bar_int = floor($bar_width);
$bar_frac = $bar_width - $bar_int;
$frac_map = array(
'',
'-',
'~',
);
$frac_char = $frac_map[floor($bar_frac * count($frac_map))];
$pattern = "[%-{$max_width}.{$max_width}s] % 5s%%";
$out = sprintf(
$pattern,
str_repeat('=', $bar_int).$frac_char,
sprintf('%.1f', 100 * $percent));
$this->eraseLine();
$console->writeErr('%s', $out);
return $this;
}
public function done($clean_exit = true) {
$console = $this->getConsole();
if ($this->drawn) {
$this->eraseLine();
if ($clean_exit) {
$console->writeErr("%s\n", pht('Done.'));
}
}
$this->finished = true;
}
private function eraseLine() {
$string = str_repeat(' ', $this->getWidth());
$console = $this->getConsole();
$console->writeErr("\r%s\r", $string);
}
private function getWidth() {
$console = $this->getConsole();
$width = $console->getErrCols();
return min(nonempty($width, 78), 78);
}
public function __destruct() {
$this->done($clean_exit = false);
}
}

View file

@ -0,0 +1,158 @@
<?php
final class PhutilConsoleServer extends Phobject {
private $clients = array();
private $handler;
private $enableLog;
public function handleMessage(PhutilConsoleMessage $message) {
$data = $message->getData();
$type = $message->getType();
switch ($type) {
case PhutilConsoleMessage::TYPE_CONFIRM:
$ok = phutil_console_confirm($data['prompt'], !$data['default']);
return $this->buildMessage(
PhutilConsoleMessage::TYPE_INPUT,
$ok);
case PhutilConsoleMessage::TYPE_PROMPT:
$response = phutil_console_prompt(
$data['prompt'],
idx($data, 'history'));
return $this->buildMessage(
PhutilConsoleMessage::TYPE_INPUT,
$response);
case PhutilConsoleMessage::TYPE_OUT:
$this->writeText(STDOUT, $data);
return null;
case PhutilConsoleMessage::TYPE_ERR:
$this->writeText(STDERR, $data);
return null;
case PhutilConsoleMessage::TYPE_LOG:
if ($this->enableLog) {
$this->writeText(STDERR, $data);
}
return null;
case PhutilConsoleMessage::TYPE_ENABLED:
switch ($data['which']) {
case PhutilConsoleMessage::TYPE_LOG:
$enabled = $this->enableLog;
break;
default:
$enabled = true;
break;
}
return $this->buildMessage(
PhutilConsoleMessage::TYPE_IS_ENABLED,
$enabled);
case PhutilConsoleMessage::TYPE_TTY:
case PhutilConsoleMessage::TYPE_COLS:
switch ($data['which']) {
case PhutilConsoleMessage::TYPE_OUT:
$which = STDOUT;
break;
case PhutilConsoleMessage::TYPE_ERR:
$which = STDERR;
break;
}
switch ($type) {
case PhutilConsoleMessage::TYPE_TTY:
if (function_exists('posix_isatty')) {
$is_a_tty = posix_isatty($which);
} else {
$is_a_tty = null;
}
return $this->buildMessage(
PhutilConsoleMessage::TYPE_IS_TTY,
$is_a_tty);
case PhutilConsoleMessage::TYPE_COLS:
// TODO: This is an approximation which might not be perfectly
// accurate.
$width = phutil_console_get_terminal_width();
return $this->buildMessage(
PhutilConsoleMessage::TYPE_COL_WIDTH,
$width);
}
break;
default:
if ($this->handler) {
return call_user_func($this->handler, $message);
} else {
throw new Exception(
pht(
"Received unknown console message of type '%s'.",
$type));
}
}
}
/**
* Set handler called for unknown messages.
*
* @param callable Signature: (PhutilConsoleMessage $message).
*/
public function setHandler($callback) {
$this->handler = $callback;
return $this;
}
private function buildMessage($type, $data) {
$response = new PhutilConsoleMessage();
$response->setType($type);
$response->setData($data);
return $response;
}
public function addExecFutureClient(ExecFuture $future) {
$io_channel = new PhutilExecChannel($future);
$protocol_channel = new PhutilPHPObjectProtocolChannel($io_channel);
$server_channel = new PhutilConsoleServerChannel($protocol_channel);
$io_channel->setStderrHandler(array($server_channel, 'didReceiveStderr'));
return $this->addClient($server_channel);
}
public function addClient(PhutilConsoleServerChannel $channel) {
$this->clients[] = $channel;
return $this;
}
public function setEnableLog($enable) {
$this->enableLog = $enable;
return $this;
}
public function run() {
while ($this->clients) {
PhutilChannel::waitForAny($this->clients);
foreach ($this->clients as $key => $client) {
if (!$client->update()) {
// If the client has exited, remove it from the list of clients.
// We still need to process any remaining buffered I/O.
unset($this->clients[$key]);
}
while ($message = $client->read()) {
$response = $this->handleMessage($message);
if ($response) {
$client->write($response);
}
}
}
}
}
private function writeText($where, array $argv) {
$text = call_user_func_array('phutil_console_format', $argv);
fprintf($where, '%s', $text);
}
}

View file

@ -0,0 +1,12 @@
<?php
final class PhutilConsoleServerChannel extends PhutilChannelChannel {
public function didReceiveStderr(PhutilExecChannel $channel, $stderr) {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_ERR)
->setData(array('%s', $stderr));
$this->getUnderlyingChannel()->addMessage($message);
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* Thrown when you prompt the user with @{function:phutil_console_prompt} or
* @{function:phutil_console_confirm} but stdin is not an interactive TTY so
* the user can't possibly respond. Usually this means the user ran the command
* with something piped into stdin.
*/
final class PhutilConsoleStdinNotInteractiveException extends Exception {
public function __construct() {
parent::__construct(
pht(
'The program is attempting to read user input, but stdin is being '.
'piped from some other source (not a TTY).'));
}
}

View file

@ -0,0 +1,308 @@
<?php
/**
* Edit a document interactively, by launching $EDITOR (like vi or nano).
*
* $result = id(new InteractiveEditor($document))
* ->setName('shopping_list')
* ->setLineOffset(15)
* ->editInteractively();
*
* This will launch the user's $EDITOR to edit the specified '$document', and
* return their changes into '$result'.
*
* @task create Creating a New Editor
* @task edit Editing Interactively
* @task config Configuring Options
*/
final class PhutilInteractiveEditor extends Phobject {
private $name = '';
private $content = '';
private $offset = 0;
private $preferred;
private $fallback;
/* -( Creating a New Editor )---------------------------------------------- */
/**
* Constructs an interactive editor, using the text of a document.
*
* @param string Document text.
* @return $this
*
* @task create
*/
public function __construct($content) {
$this->setContent($content);
}
/* -( Editing Interactively )----------------------------------------------- */
/**
* Launch an editor and edit the content. The edited content will be
* returned.
*
* @return string Edited content.
* @throws Exception The editor exited abnormally or something untoward
* occurred.
*
* @task edit
*/
public function editInteractively() {
$name = $this->getName();
$content = $this->getContent();
if (phutil_is_windows()) {
$content = str_replace("\n", "\r\n", $content);
}
$tmp = Filesystem::createTemporaryDirectory('edit.');
$path = $tmp.DIRECTORY_SEPARATOR.$name;
try {
Filesystem::writeFile($path, $content);
} catch (Exception $ex) {
Filesystem::remove($tmp);
throw $ex;
}
$editor = $this->getEditor();
$offset = $this->getLineOffset();
$err = $this->invokeEditor($editor, $path, $offset);
if ($err) {
// See T13297. On macOS, "vi" and "vim" may exit with errors even though
// the edit succeeded. If the binary is "vi" or "vim" and we get an exit
// code, we perform an additional test on the binary.
$vi_binaries = array(
'vi' => true,
'vim' => true,
);
$binary = basename($editor);
if (isset($vi_binaries[$binary])) {
// This runs "Q" (an invalid command), then "q" (a valid command,
// meaning "quit"). Vim binaries with behavior that makes them poor
// interactive editors will exit "1".
list($diagnostic_err) = exec_manual('%R +Q +q', $binary);
// If we get an error back, the binary is badly behaved. Ignore the
// original error and assume it's not meaningful, since it just
// indicates the user made a typo in a command when editing
// interactively, which is routine and unconcerning.
if ($diagnostic_err) {
$err = 0;
}
}
}
if ($err) {
Filesystem::remove($tmp);
throw new Exception(pht('Editor exited with an error code (#%d).', $err));
}
try {
$result = Filesystem::readFile($path);
Filesystem::remove($tmp);
} catch (Exception $ex) {
Filesystem::remove($tmp);
throw $ex;
}
if (phutil_is_windows()) {
$result = str_replace("\r\n", "\n", $result);
}
$this->setContent($result);
return $this->getContent();
}
private function invokeEditor($editor, $path, $offset) {
// NOTE: Popular Windows editors like Notepad++ and GitPad do not support
// line offsets, so just ignore the offset feature on Windows. We rarely
// use it anyway.
$offset_flag = '';
if ($offset && !phutil_is_windows()) {
$offset = (int)$offset;
if (preg_match('/^mate/', $editor)) {
$offset_flag = csprintf('-l %d', $offset);
} else {
$offset_flag = csprintf('+%d', $offset);
}
}
$cmd = csprintf(
'%C %C %s',
$editor,
$offset_flag,
$path);
return phutil_passthru('%C', $cmd);
}
/* -( Configuring Options )------------------------------------------------- */
/**
* Set the line offset where the cursor should be positioned when the editor
* opens. By default, the cursor will be positioned at the start of the
* content.
*
* @param int Line number where the cursor should be positioned.
* @return $this
*
* @task config
*/
public function setLineOffset($offset) {
$this->offset = (int)$offset;
return $this;
}
/**
* Get the current line offset. See setLineOffset().
*
* @return int Current line offset.
*
* @task config
*/
public function getLineOffset() {
return $this->offset;
}
/**
* Set the document name. Depending on the editor, this may be exposed to
* the user and can give them a sense of what they're editing.
*
* @param string Document name.
* @return $this
*
* @task config
*/
public function setName($name) {
$name = preg_replace('/[^A-Z0-9._-]+/i', '', $name);
$this->name = $name;
return $this;
}
/**
* Get the current document name. See @{method:setName} for details.
*
* @return string Current document name.
*
* @task config
*/
public function getName() {
if (!strlen($this->name)) {
return 'untitled';
}
return $this->name;
}
/**
* Set the text content to be edited.
*
* @param string New content.
* @return $this
*
* @task config
*/
public function setContent($content) {
$this->content = $content;
return $this;
}
/**
* Retrieve the current content.
*
* @return string
*
* @task config
*/
public function getContent() {
return $this->content;
}
/**
* Set the fallback editor program to be used if the env variable $EDITOR
* is not available and there is no `editor` binary in PATH.
*
* @param string Command-line editing program (e.g. 'emacs', 'vi')
* @return $this
*
* @task config
*/
public function setFallbackEditor($editor) {
$this->fallback = $editor;
return $this;
}
/**
* Set the preferred editor program. If set, this will override all other
* sources of editor configuration, like $EDITOR.
*
* @param string Command-line editing program (e.g. 'emacs', 'vi')
* @return $this
*
* @task config
*/
public function setPreferredEditor($editor) {
$this->preferred = $editor;
return $this;
}
/**
* Get the name of the editor program to use. The value of the environmental
* variable $EDITOR will be used if available; otherwise, the `editor` binary
* if present; otherwise the best editor will be selected.
*
* @return string Command-line editing program.
*
* @task config
*/
public function getEditor() {
if ($this->preferred) {
return $this->preferred;
}
$editor = getenv('EDITOR');
if ($editor) {
return $editor;
}
if ($this->fallback) {
return $this->fallback;
}
$candidates = array('editor', 'nano', 'sensible-editor', 'vi');
foreach ($candidates as $cmd) {
if (Filesystem::binaryExists($cmd)) {
return $cmd;
}
}
throw new Exception(
pht(
'Unable to launch an interactive text editor. Set the %s '.
'environment variable to an appropriate editor.',
'EDITOR'));
}
}

View file

@ -0,0 +1,48 @@
<?php
final class PhutilConsoleWrapTestCase extends PhutilTestCase {
public function testWrap() {
$dir = dirname(__FILE__).'/wrap/';
$files = Filesystem::listDirectory($dir);
foreach ($files as $file) {
if (preg_match('/.txt$/', $file)) {
$this->assertEqual(
Filesystem::readFile($dir.$file.'.expect'),
phutil_console_wrap(Filesystem::readFile($dir.$file)),
$file);
}
}
}
public function testConsoleWrap() {
$this->assertEqual(
phutil_console_format(
"<bg:red>** %s **</bg> abc abc abc abc abc abc abc abc abc abc ".
"abc abc abc abc abc abc abc\nabc abc abc abc abc abc abc abc abc ".
"abc abc!",
pht('ERROR')),
phutil_console_wrap(
phutil_console_format(
'<bg:red>** %s **</bg> abc abc abc abc abc abc abc abc abc abc '.
'abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc '.
'abc abc!',
pht('ERROR'))),
pht('ANSI escape sequences should not contribute toward wrap width.'));
}
public function testWrapIndent() {
$turtles = <<<EOTURTLES
turtle turtle turtle turtle turtle turtle turtle turtle
turtle turtle turtle turtle turtle turtle turtle turtle
turtle turtle turtle turtle
EOTURTLES;
$this->assertEqual(
$turtles,
phutil_console_wrap(
rtrim(str_repeat('turtle ', 20)),
$indent = 20));
}
}

View file

@ -0,0 +1 @@
Say MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM every day.

View file

@ -0,0 +1,3 @@
Say
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
every day.

View file

@ -0,0 +1,10 @@
Curabitur gravida lectus odio, nec dictum sapien.
Donec condimentum purus at est aliquam lobortis.
Sed facilisis justo a purus interdum at venenatis eros laoreet.
Quisque ac odio vitae erat congue elementum.
Etiam semper venenatis massa vitae faucibus.
Praesent eget eros tortor.
Vestibulum in pharetra massa.
Integer risus justo, malesuada auctor feugiat venenatis, viverra iaculis est.
Praesent a tortor et dui tempus egestas.
Sed lacinia diam id velit tincidunt sagittis.

View file

@ -0,0 +1,10 @@
Curabitur gravida lectus odio, nec dictum sapien.
Donec condimentum purus at est aliquam lobortis.
Sed facilisis justo a purus interdum at venenatis eros laoreet.
Quisque ac odio vitae erat congue elementum.
Etiam semper venenatis massa vitae faucibus.
Praesent eget eros tortor.
Vestibulum in pharetra massa.
Integer risus justo, malesuada auctor feugiat venenatis, viverra iaculis est.
Praesent a tortor et dui tempus egestas.
Sed lacinia diam id velit tincidunt sagittis.

View file

@ -0,0 +1 @@
Morbi auctor commodo libero, vel interdum leo commodo nec. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum dictum pretium lorem ac commodo. Vivamus ullamcorper neque et velit interdum ornare. Fusce adipiscing metus non sem porttitor scelerisque. Aliquam mattis sem non tortor semper eget fermentum libero faucibus. Nam vulputate mauris at nunc bibendum mollis. Aliquam mattis rutrum turpis a fringilla. Mauris quis nulla eget nunc mollis pharetra id sit amet arcu. Nam ut urna in ligula facilisis scelerisque in nec massa. Morbi posuere, turpis in bibendum fringilla, augue felis gravida est, vitae convallis quam nunc at tellus.

View file

@ -0,0 +1,9 @@
Morbi auctor commodo libero, vel interdum leo commodo nec. Cum sociis natoque
penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum
dictum pretium lorem ac commodo. Vivamus ullamcorper neque et velit interdum
ornare. Fusce adipiscing metus non sem porttitor scelerisque. Aliquam mattis
sem non tortor semper eget fermentum libero faucibus. Nam vulputate mauris at
nunc bibendum mollis. Aliquam mattis rutrum turpis a fringilla. Mauris quis
nulla eget nunc mollis pharetra id sit amet arcu. Nam ut urna in ligula
facilisis scelerisque in nec massa. Morbi posuere, turpis in bibendum
fringilla, augue felis gravida est, vitae convallis quam nunc at tellus.

View file

@ -0,0 +1 @@
Do you want to do stuff? [y/N]

View file

@ -0,0 +1 @@
Do you want to do stuff? [y/N]

View file

@ -0,0 +1 @@
☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃

View file

@ -0,0 +1,2 @@
☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃
☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃

209
src/console/format.php Normal file
View file

@ -0,0 +1,209 @@
<?php
function phutil_console_format($format /* ... */) {
$args = func_get_args();
return call_user_func_array(
array('PhutilConsoleFormatter', 'formatString'),
$args);
}
function phutil_console_confirm($prompt, $default_no = true) {
$prompt_options = $default_no ? '[y/N]' : '[Y/n]';
do {
$response = phutil_console_prompt($prompt.' '.$prompt_options);
$c = trim(strtolower($response));
} while ($c != 'y' && $c != 'n' && $c != '');
echo "\n";
if ($default_no) {
return ($c == 'y');
} else {
return ($c != 'n');
}
}
function phutil_console_select($prompt, $min, $max) {
$select_options = '['.$min.' - '.$max.']';
do {
$response = phutil_console_prompt($prompt.' '.$select_options);
$selection = trim($response);
if (preg_match('/^\d+\z/', $selection)) {
$selection = (int)$selection;
if ($selection >= $min && $selection <= $max) {
return $selection;
}
}
} while (true);
}
function phutil_console_prompt($prompt, $history = '') {
echo "\n\n";
$prompt = phutil_console_wrap($prompt.' ', 4);
try {
phutil_console_require_tty();
} catch (PhutilConsoleStdinNotInteractiveException $ex) {
// Throw after echoing the prompt so the user has some idea what happened.
echo $prompt;
throw $ex;
}
// `escapeshellarg` makes double quotes in the command below disappear on
// Windows, which breaks prompts when using history. See T6348
$use_history = !phutil_is_windows();
if ($history == '') {
$use_history = false;
} else {
// Test if bash is available by seeing if it can run `true`.
list($err) = exec_manual('bash -c %s', 'true');
if ($err) {
$use_history = false;
}
}
if (!$use_history) {
echo $prompt;
$response = fgets(STDIN);
} else {
// There's around 0% chance that readline() is available directly in PHP,
// so we're using bash/read/history instead.
$command = csprintf(
'bash -c %s',
csprintf(
'history -r %s 2>/dev/null; '.
'read -e -p %s; '.
'echo "$REPLY"; '.
'history -s "$REPLY" 2>/dev/null; '.
'history -w %s 2>/dev/null',
$history,
$prompt,
$history));
// execx() doesn't work with input, phutil_passthru() doesn't return output.
$response = shell_exec($command);
}
return rtrim($response, "\r\n");
}
/**
* Soft wrap text for display on a console, respecting UTF8 character boundaries
* and ANSI color escape sequences.
*
* @param string Text to wrap.
* @param int Optional indent level.
* @param bool True to also indent the first line.
* @return string Wrapped text.
*/
function phutil_console_wrap($text, $indent = 0, $with_prefix = true) {
$lines = array();
$width = (78 - $indent);
$esc = chr(27);
$break_pos = null;
$len_after_break = 0;
$line_len = 0;
$line = array();
$lines = array();
$vector = phutil_utf8v($text);
$vector_len = count($vector);
for ($ii = 0; $ii < $vector_len; $ii++) {
$chr = $vector[$ii];
// If this is an ANSI escape sequence for a color code, just consume it
// without counting it toward the character limit. This prevents lines
// with bold/color on them from wrapping too early.
if ($chr == $esc) {
for ($ii; $ii < $vector_len; $ii++) {
$line[] = $vector[$ii];
if ($vector[$ii] == 'm') {
break;
}
}
continue;
}
$line[] = $chr;
++$line_len;
++$len_after_break;
if ($line_len > $width) {
if ($break_pos !== null) {
$slice = array_slice($line, 0, $break_pos);
while (count($slice) && end($slice) == ' ') {
array_pop($slice);
}
$slice[] = "\n";
$lines[] = $slice;
$line = array_slice($line, $break_pos);
$line_len = $len_after_break;
$len_after_break = 0;
$break_pos = null;
}
}
if ($chr == ' ') {
$break_pos = count($line);
$len_after_break = 0;
}
if ($chr == "\n") {
$lines[] = $line;
$line = array();
$len_after_break = 0;
$line_len = 0;
$break_pos = null;
}
}
if ($line) {
if ($line) {
$lines[] = $line;
}
}
$pre = null;
if ($indent) {
$pre = str_repeat(' ', $indent);
}
foreach ($lines as $idx => $line) {
if ($idx == 0 && !$with_prefix) {
$prefix = null;
} else {
$prefix = $pre;
}
$lines[$idx] = $prefix.implode('', $line);
}
return implode('', $lines);
}
function phutil_console_require_tty() {
if (function_exists('posix_isatty') && !posix_isatty(STDIN)) {
throw new PhutilConsoleStdinNotInteractiveException();
}
}
/**
* Determine the width of the terminal, if possible. Returns `null` on failure.
*
* @return int|null Terminal width in characters, or null on failure.
*/
function phutil_console_get_terminal_width() {
return PhutilConsoleMetrics::getDefaultConsole()
->getTerminalWidth();
}

View file

@ -0,0 +1,48 @@
<?php
final class PhutilConsoleBlock extends PhutilConsoleView {
private $items = array();
public function addParagraph($item) {
$this->items[] = array(
'type' => 'paragraph',
'item' => $item,
);
return $this;
}
public function addList(PhutilConsoleList $list) {
$this->items[] = array(
'type' => 'list',
'item' => $list,
);
return $this;
}
protected function drawView() {
$output = array();
foreach ($this->items as $spec) {
$type = $spec['type'];
$item = $spec['item'];
switch ($type) {
case 'paragraph':
$item = array(
tsprintf('%s', $item)->applyWrap(),
"\n",
);
break;
case 'list':
$item = $item;
break;
}
$output[] = $item;
}
return $this->drawLines($output);
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhutilConsoleError
extends PhutilConsoleLogLine {
protected function getLogLineColor() {
return 'red';
}
}

Some files were not shown because too many files have changed in this diff Show more