Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15409756
D7520.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
10 KB
Referenced Files
None
Subscribers
None
D7520.diff
View Options
Index: src/__phutil_library_map__.php
===================================================================
--- src/__phutil_library_map__.php
+++ src/__phutil_library_map__.php
@@ -505,6 +505,8 @@
'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
+ 'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php',
+ 'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php',
'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
@@ -2757,6 +2759,7 @@
'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionMercurialRequest' => 'DiffusionRequest',
+ 'DiffusionMercurialResponse' => 'AphrontResponse',
'DiffusionPathCompleteController' => 'DiffusionController',
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
'DiffusionPathValidateController' => 'DiffusionController',
Index: src/applications/diffusion/controller/DiffusionServeController.php
===================================================================
--- src/applications/diffusion/controller/DiffusionServeController.php
+++ src/applications/diffusion/controller/DiffusionServeController.php
@@ -176,6 +176,9 @@
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->serveGitRequest($repository, $viewer);
break;
+ case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
+ $result = $this->serveMercurialRequest($repository, $viewer);
+ break;
default:
$result = new PhabricatorVCSResponse(
999,
@@ -224,13 +227,43 @@
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$cmd = $request->getStr('cmd');
- switch ($cmd) {
- case 'capabilities':
+ if ($cmd == 'batch') {
+ // For "batch" we get a "cmds" argument like
+ //
+ // heads ;known nodes=
+ //
+ // We need to examine the commands (here, "heads" and "known") to
+ // make sure they're all read-only.
+
+ $args = $this->getMercurialArguments();
+ $cmds = idx($args, 'cmds');
+ if ($cmds) {
+
+ // NOTE: Mercurial has some code to escape semicolons, but it does
+ // not actually function for command separation. For example, these
+ // two batch commands will produce completely different results (the
+ // former will run the lookup; the latter will fail with a parser
+ // error):
+ //
+ // lookup key=a:xb;lookup key=z* 0
+ // lookup key=a:;b;lookup key=z* 0
+ // ^
+ // |
+ // +-- Note semicolon.
+ //
+ // So just split unconditionally.
+
+ $cmds = explode(';', $cmds);
+ foreach ($cmds as $sub_cmd) {
+ $name = head(explode(' ', $sub_cmd, 2));
+ if (!DiffusionMercurialWireProtocol::isReadOnlyCommand($name)) {
+ return false;
+ }
+ }
return true;
- default:
- return false;
+ }
}
- break;
+ return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION:
break;
}
@@ -357,5 +390,127 @@
return $user;
}
+
+ private function serveMercurialRequest(PhabricatorRepository $repository) {
+ $request = $this->getRequest();
+
+ $bin = Filesystem::resolveBinary('hg');
+ if (!$bin) {
+ throw new Exception("Unable to find `hg` in PATH!");
+ }
+
+ $env = array();
+ $input = PhabricatorStartup::getRawInput();
+
+ $cmd = $request->getStr('cmd');
+
+ $args = $this->getMercurialArguments();
+ $args = $this->formatMercurialArguments($cmd, $args);
+
+ if (strlen($input)) {
+ $input = strlen($input)."\n".$input."0\n";
+ }
+
+ list($err, $stdout, $stderr) = id(new ExecFuture('%s serve --stdio', $bin))
+ ->setEnv($env, true)
+ ->setCWD($repository->getLocalPath())
+ ->write("{$cmd}\n{$args}{$input}")
+ ->resolve();
+
+ if ($err) {
+ return new PhabricatorVCSResponse(
+ 500,
+ pht('Error %d: %s', $err, $stderr));
+ }
+
+ if ($cmd == 'getbundle' ||
+ $cmd == 'changegroup' ||
+ $cmd == 'changegroupsubset') {
+ // We're not completely sure that "changegroup" and "changegroupsubset"
+ // actually work, they're for very old Mercurial.
+ $body = gzcompress($stdout);
+ } else if ($cmd == 'unbundle') {
+ // This includes diagnostic information and anything echoed by commit
+ // hooks. We ignore `stdout` since it just has protocol garbage, and
+ // substitute `stderr`.
+ $body = strlen($stderr)."\n".$stderr;
+ } else {
+ list($length, $body) = explode("\n", $stdout, 2);
+ }
+
+ return id(new DiffusionMercurialResponse())->setContent($body);
+ }
+
+
+ private function getMercurialArguments() {
+ // Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
+ // "Why would you do this?".
+
+ $args_raw = array();
+ for ($ii = 1; ; $ii++) {
+ $header = 'HTTP_X_HGARG_'.$ii;
+ if (!array_key_exists($header, $_SERVER)) {
+ break;
+ }
+ $args_raw[] = $_SERVER[$header];
+ }
+ $args_raw = implode('', $args_raw);
+
+ return id(new PhutilQueryStringParser())
+ ->parseQueryString($args_raw);
+ }
+
+ private function formatMercurialArguments($command, array $arguments) {
+ $spec = DiffusionMercurialWireProtocol::getCommandArgs($command);
+
+ $out = array();
+
+ // Mercurial takes normal arguments like this:
+ //
+ // name <length(value)>
+ // value
+
+ $has_star = false;
+ foreach ($spec as $arg_key) {
+ if ($arg_key == '*') {
+ $has_star = true;
+ continue;
+ }
+ if (isset($arguments[$arg_key])) {
+ $value = $arguments[$arg_key];
+ $size = strlen($value);
+ $out[] = "{$arg_key} {$size}\n{$value}";
+ unset($arguments[$arg_key]);
+ }
+ }
+
+ if ($has_star) {
+
+ // Mercurial takes arguments for variable argument lists roughly like
+ // this:
+ //
+ // * <count(args)>
+ // argname1 <length(argvalue1)>
+ // argvalue1
+ // argname2 <length(argvalue2)>
+ // argvalue2
+
+ $count = count($arguments);
+
+ $out[] = "* {$count}\n";
+
+ foreach ($arguments as $key => $value) {
+ if (in_array($key, $spec)) {
+ // We already added this argument above, so skip it.
+ continue;
+ }
+ $size = strlen($value);
+ $out[] = "{$key} {$size}\n{$value}";
+ }
+ }
+
+ return implode('', $out);
+ }
+
}
Index: src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
===================================================================
--- /dev/null
+++ src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
@@ -0,0 +1,62 @@
+<?php
+
+final class DiffusionMercurialWireProtocol {
+
+ public static function getCommandArgs($command) {
+ // We need to enumerate all of the Mercurial wire commands because the
+ // argument encoding varies based on the command. "Why?", you might ask,
+ // "Why would you do this?".
+
+ $commands = array(
+ 'batch' => array('cmds', '*'),
+ 'between' => array('pairs'),
+ 'branchmap' => array(),
+ 'branches' => array('nodes'),
+ 'capabilities' => array(),
+ 'changegroup' => array('roots'),
+ 'changegroupsubset' => array('bases heads'),
+ 'debugwireargs' => array('one two *'),
+ 'getbundle' => array('*'),
+ 'heads' => array(),
+ 'hello' => array(),
+ 'known' => array('nodes', '*'),
+ 'listkeys' => array('namespace'),
+ 'lookup' => array('key'),
+ 'pushkey' => array('namespace', 'key', 'old', 'new'),
+ 'stream_out' => array(''),
+ 'unbundle' => array('heads'),
+ );
+
+ if (!isset($commands[$command])) {
+ throw new Exception("Unknown Mercurial command '{$command}!");
+ }
+
+ return $commands[$command];
+ }
+
+ public static function isReadOnlyCommand($command) {
+ $read_only = array(
+ 'between' => true,
+ 'branchmap' => true,
+ 'branches' => true,
+ 'capabilities' => true,
+ 'changegroup' => true,
+ 'changegroupsubset' => true,
+ 'debugwireargs' => true,
+ 'getbundle' => true,
+ 'heads' => true,
+ 'hello' => true,
+ 'known' => true,
+ 'listkeys' => true,
+ 'lookup' => true,
+ 'stream_out' => true,
+ );
+
+ // Notably, the write commands are "pushkey" and "unbundle". The
+ // "batch" command is theoretically read only, but we require explicit
+ // analysis of the actual commands.
+
+ return isset($read_only[$command]);
+ }
+
+}
Index: src/applications/diffusion/response/DiffusionGitResponse.php
===================================================================
--- src/applications/diffusion/response/DiffusionGitResponse.php
+++ src/applications/diffusion/response/DiffusionGitResponse.php
@@ -30,7 +30,7 @@
}
public function getHeaders() {
- return $this->headers;
+ return array_merge(parent::getHeaders(), $this->headers);
}
public function getCacheHeaders() {
Index: src/applications/diffusion/response/DiffusionMercurialResponse.php
===================================================================
--- /dev/null
+++ src/applications/diffusion/response/DiffusionMercurialResponse.php
@@ -0,0 +1,31 @@
+<?php
+
+final class DiffusionMercurialResponse extends AphrontResponse {
+
+ private $content;
+
+ public function setContent($content) {
+ $this->content = $content;
+ return $this;
+ }
+
+ public function buildResponseString() {
+ return $this->content;
+ }
+
+ public function getHeaders() {
+ $headers = array(
+ array('Content-Type', 'application/mercurial-0.1'),
+ );
+ return array_merge(parent::getHeaders(), $headers);
+ }
+
+ public function getCacheHeaders() {
+ return array();
+ }
+
+ public function getHTTPResponseCode() {
+ return 200;
+ }
+
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Mar 20, 5:06 AM (4 w, 1 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7704969
Default Alt Text
D7520.diff (10 KB)
Attached To
Mode
D7520: Allow Phabricator to serve Mercurial repositories over HTTP
Attached
Detach File
Event Timeline
Log In to Comment