mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 14:00:56 +01:00
Allow Phabricator to serve Mercurial repositories over HTTP
Summary: Ref T2230. This is easily the worst thing I've had to write in a while. I'll leave some notes inline. Test Plan: Ran `hg clone http://...` on a hosted repo. Ran `hg push` on the same. Changed sync'd both ways. Reviewers: asherkin, btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2230 Differential Revision: https://secure.phabricator.com/D7520
This commit is contained in:
parent
44a40eaf57
commit
6324669748
5 changed files with 257 additions and 6 deletions
|
@ -505,6 +505,8 @@ phutil_register_library_map(array(
|
||||||
'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
|
'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
|
||||||
'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
|
'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
|
||||||
'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.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',
|
'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
|
||||||
'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
|
'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
|
||||||
'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
|
'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
|
||||||
|
@ -2757,6 +2759,7 @@ phutil_register_library_map(array(
|
||||||
'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
|
'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
|
||||||
'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
|
'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
|
||||||
'DiffusionMercurialRequest' => 'DiffusionRequest',
|
'DiffusionMercurialRequest' => 'DiffusionRequest',
|
||||||
|
'DiffusionMercurialResponse' => 'AphrontResponse',
|
||||||
'DiffusionPathCompleteController' => 'DiffusionController',
|
'DiffusionPathCompleteController' => 'DiffusionController',
|
||||||
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
|
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
|
||||||
'DiffusionPathValidateController' => 'DiffusionController',
|
'DiffusionPathValidateController' => 'DiffusionController',
|
||||||
|
|
|
@ -176,6 +176,9 @@ final class DiffusionServeController extends DiffusionController {
|
||||||
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
|
||||||
$result = $this->serveGitRequest($repository, $viewer);
|
$result = $this->serveGitRequest($repository, $viewer);
|
||||||
break;
|
break;
|
||||||
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
|
||||||
|
$result = $this->serveMercurialRequest($repository, $viewer);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$result = new PhabricatorVCSResponse(
|
$result = new PhabricatorVCSResponse(
|
||||||
999,
|
999,
|
||||||
|
@ -224,13 +227,43 @@ final class DiffusionServeController extends DiffusionController {
|
||||||
break;
|
break;
|
||||||
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
|
||||||
$cmd = $request->getStr('cmd');
|
$cmd = $request->getStr('cmd');
|
||||||
switch ($cmd) {
|
if ($cmd == 'batch') {
|
||||||
case 'capabilities':
|
// For "batch" we get a "cmds" argument like
|
||||||
return true;
|
//
|
||||||
default:
|
// 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 false;
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
|
||||||
case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION:
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -357,5 +390,127 @@ final class DiffusionServeController extends DiffusionController {
|
||||||
|
|
||||||
return $user;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ final class DiffusionGitResponse extends AphrontResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHeaders() {
|
public function getHeaders() {
|
||||||
return $this->headers;
|
return array_merge(parent::getHeaders(), $this->headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCacheHeaders() {
|
public function getCacheHeaders() {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue