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:
epriestley
2013-11-06 18:00:42 -08:00
parent 44a40eaf57
commit 6324669748
5 changed files with 257 additions and 6 deletions

View File

@@ -176,6 +176,9 @@ final class DiffusionServeController extends DiffusionController {
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 @@ final class DiffusionServeController extends DiffusionController {
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 @@ final class DiffusionServeController extends DiffusionController {
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);
}
}