feat: improve pure PHP Git protocol validation and negotiation
This commit is contained in:
@@ -12,18 +12,23 @@ This is a PHP Smart HTTP Git server with no dependency manager or generated buil
|
||||
- `schema.mysql.sql` and `migration.repository-ownership.mysql.sql` define database changes.
|
||||
- `config.php.sample` documents settings; local `config.php` must never be committed.
|
||||
- `repos/` contains runtime bare repositories. `README.md` and `usage.md` document deployment.
|
||||
|
||||
There is no dedicated automated test directory.
|
||||
- `tests/` contains the dependency-free protocol regression suite and disposable repository fixtures.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
PHP is interpreted directly, so there is no build step. Run syntax checks from the repository root:
|
||||
PHP is interpreted directly, so there is no build step. Run the complete regression suite from the repository root:
|
||||
|
||||
```sh
|
||||
php tests/run.php
|
||||
```
|
||||
|
||||
Run syntax checks across production and test code:
|
||||
|
||||
```sh
|
||||
php -l index.php
|
||||
php -l install.php
|
||||
php -l manage.php
|
||||
for file in lib/*.php operations/*.php; do php -l "$file" || exit 1; done
|
||||
for file in lib/*.php operations/*.php tests/*.php; do php -l "$file" || exit 1; done
|
||||
```
|
||||
|
||||
Check required runtime support with `php -m | grep pdo_mysql`. For a local smoke test, use a disposable `config.php` and PHP-capable web server, then exercise the UI and Git endpoints with `git ls-remote` or `git clone`.
|
||||
|
||||
@@ -359,25 +359,45 @@ function git_object_store_apply_delta($base, $delta, $max_result_bytes=0) {
|
||||
function git_object_store_links($object) {
|
||||
$links = array();
|
||||
if ($object['type'] === 'commit') {
|
||||
foreach (preg_split('~\n~', $object['body']) as $line) {
|
||||
if ($line === '') {
|
||||
break;
|
||||
}
|
||||
if (preg_match('~^(?:tree|parent) ([0-9a-f]{40})$~D', $line, $matches)) {
|
||||
$headers = strstr($object['body'], "\n\n", TRUE);
|
||||
if ($headers === FALSE) {
|
||||
return FALSE;
|
||||
}
|
||||
$lines = explode("\n", $headers);
|
||||
if (empty($lines) || !preg_match('~^tree ([0-9a-f]{40})$~D', $lines[0], $matches)) {
|
||||
return FALSE;
|
||||
}
|
||||
$links[] = $matches[1];
|
||||
foreach (array_slice($lines, 1) as $line) {
|
||||
if (strpos($line, 'parent ') === 0) {
|
||||
if (!preg_match('~^parent ([0-9a-f]{40})$~D', $line, $matches)) {
|
||||
return FALSE;
|
||||
}
|
||||
$links[] = $matches[1];
|
||||
}
|
||||
}
|
||||
} else if ($object['type'] === 'tag') {
|
||||
if (preg_match('~^object ([0-9a-f]{40})$~m', $object['body'], $matches)) {
|
||||
$links[] = $matches[1];
|
||||
$headers = strstr($object['body'], "\n\n", TRUE);
|
||||
if ($headers === FALSE) {
|
||||
return FALSE;
|
||||
}
|
||||
$lines = explode("\n", $headers);
|
||||
if (count($lines) < 3
|
||||
|| !preg_match('~^object ([0-9a-f]{40})$~D', $lines[0], $object_match)
|
||||
|| !preg_match('~^type (commit|tree|blob|tag)$~D', $lines[1])
|
||||
|| !preg_match('~^tag .+$~D', $lines[2])) {
|
||||
return FALSE;
|
||||
}
|
||||
$links[] = $object_match[1];
|
||||
} else if ($object['type'] === 'tree') {
|
||||
$position = 0;
|
||||
$length = strlen($object['body']);
|
||||
while ($position < $length) {
|
||||
$separator = strpos($object['body'], "\0", $position);
|
||||
if ($separator === FALSE || $separator + 21 > $length
|
||||
|| !preg_match('~^[0-7]+ [^/]+$~D', substr($object['body'], $position, $separator - $position))) {
|
||||
|| !preg_match(
|
||||
'~^(?:40000|100644|100755|120000|160000) [^/\x00]+$~D',
|
||||
substr($object['body'], $position, $separator - $position))) {
|
||||
return FALSE;
|
||||
}
|
||||
$links[] = bin2hex(substr($object['body'], $separator + 1, 20));
|
||||
@@ -388,6 +408,35 @@ function git_object_store_links($object) {
|
||||
return $links;
|
||||
}
|
||||
|
||||
function git_object_store_validate_links(&$store, $object, $links) {
|
||||
if ($object['type'] === 'commit') {
|
||||
foreach ($links as $index => $oid) {
|
||||
$linked = git_object_store_read($store, $oid);
|
||||
if ($linked === FALSE
|
||||
|| ($index === 0 && $linked['type'] !== 'tree')
|
||||
|| ($index > 0 && $linked['type'] !== 'commit')) {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
} else if ($object['type'] === 'tag') {
|
||||
if (!preg_match('~^type (commit|tree|blob|tag)$~m', $object['body'], $matches)) {
|
||||
return FALSE;
|
||||
}
|
||||
$linked = git_object_store_read($store, $links[0]);
|
||||
if ($linked === FALSE || $linked['type'] !== $matches[1]) {
|
||||
return FALSE;
|
||||
}
|
||||
} else if ($object['type'] === 'tree') {
|
||||
foreach ($links as $oid) {
|
||||
if (git_object_store_read($store, $oid) === FALSE) {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
function git_object_store_collect(&$store, $roots) {
|
||||
$objects = array();
|
||||
$pending = array_values($roots);
|
||||
@@ -401,7 +450,7 @@ function git_object_store_collect(&$store, $roots) {
|
||||
return FALSE;
|
||||
}
|
||||
$links = git_object_store_links($object);
|
||||
if ($links === FALSE) {
|
||||
if ($links === FALSE || !git_object_store_validate_links($store, $object, $links)) {
|
||||
return FALSE;
|
||||
}
|
||||
$objects[$oid] = $object;
|
||||
|
||||
+34
-11
@@ -438,6 +438,9 @@ function git_receive_pack_commit_updates($git_path, $locks) {
|
||||
foreach ($locks as $lock) {
|
||||
$updates[] = $lock['update'];
|
||||
}
|
||||
$packed_path = get_safe_file_path($git_path, '/packed-refs');
|
||||
$packed_contents = $packed_path === FALSE ? NULL : @file_get_contents($packed_path);
|
||||
$committed = array();
|
||||
if (!git_receive_pack_remove_packed_refs($git_path, $updates)) {
|
||||
return FALSE;
|
||||
}
|
||||
@@ -447,17 +450,37 @@ function git_receive_pack_commit_updates($git_path, $locks) {
|
||||
$lock['file'] = NULL;
|
||||
if ($lock['update']['new'] === str_repeat('0', 40)) {
|
||||
if (is_file($lock['path']) && !@unlink($lock['path'])) {
|
||||
git_receive_pack_release_locks($locks);
|
||||
return FALSE;
|
||||
break;
|
||||
}
|
||||
@unlink($lock['lock_path']);
|
||||
} else if (!@rename($lock['lock_path'], $lock['path'])) {
|
||||
git_receive_pack_release_locks($locks);
|
||||
return FALSE;
|
||||
break;
|
||||
}
|
||||
$committed[] = $lock['update'];
|
||||
}
|
||||
unset($lock);
|
||||
return TRUE;
|
||||
if (count($committed) === count($locks)) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
git_receive_pack_release_locks($locks);
|
||||
foreach ($committed as $update) {
|
||||
$path = $git_path.'/'.$update['ref'];
|
||||
if ($update['old'] === str_repeat('0', 40)) {
|
||||
@unlink($path);
|
||||
continue;
|
||||
}
|
||||
if (!is_dir(dirname($path))) {
|
||||
@mkdir(dirname($path), 0777, TRUE);
|
||||
}
|
||||
@file_put_contents($path, $update['old']."\n", LOCK_EX);
|
||||
}
|
||||
if ($packed_contents !== NULL) {
|
||||
@file_put_contents($git_path.'/packed-refs', $packed_contents, LOCK_EX);
|
||||
} else {
|
||||
@unlink($git_path.'/packed-refs');
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
function git_receive_pack_status($capabilities, $updates, $unpack_ok, $message) {
|
||||
@@ -526,7 +549,7 @@ function git_receive_pack_rpc_native($repository, $input) {
|
||||
$objects = array();
|
||||
if ($store === FALSE) {
|
||||
git_receive_pack_status($commands['capabilities'], $commands['updates'], FALSE, 'unsupported repository format');
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
if ($needs_pack) {
|
||||
$pack = stream_get_contents($input);
|
||||
@@ -537,7 +560,7 @@ function git_receive_pack_rpc_native($repository, $input) {
|
||||
(int) $repository['options']['max_pack_objects']);
|
||||
if ($objects === FALSE) {
|
||||
git_receive_pack_status($commands['capabilities'], $commands['updates'], FALSE, 'invalid pack');
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,27 +573,27 @@ function git_receive_pack_rpc_native($repository, $input) {
|
||||
|| (strpos($update['ref'], 'refs/heads/') === 0
|
||||
&& $store['objects'][$update['new']]['type'] !== 'commit')) {
|
||||
git_receive_pack_status($commands['capabilities'], $commands['updates'], FALSE, 'invalid object graph');
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
if (strpos($update['ref'], 'refs/heads/') === 0
|
||||
&& $update['old'] !== $zero
|
||||
&& !$repository['options']['allow_non_fast_forward']
|
||||
&& !git_receive_pack_is_ancestor($store, $update['old'], $update['new'])) {
|
||||
git_receive_pack_status($commands['capabilities'], $commands['updates'], FALSE, 'non-fast-forward');
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
$locks = git_receive_pack_lock_updates($repository['path'], $commands['updates']);
|
||||
if ($locks === FALSE) {
|
||||
git_receive_pack_status($commands['capabilities'], $commands['updates'], FALSE, 'stale or locked ref');
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
if (!git_receive_pack_write_objects($repository['path'], $objects)
|
||||
|| !git_receive_pack_commit_updates($repository['path'], $locks)) {
|
||||
git_receive_pack_release_locks($locks);
|
||||
git_receive_pack_status($commands['capabilities'], $commands['updates'], FALSE, 'repository update failed');
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
git_receive_pack_status($commands['capabilities'], $commands['updates'], TRUE, '');
|
||||
|
||||
+37
-3
@@ -2,8 +2,12 @@
|
||||
|
||||
function git_upload_pack_capabilities($git_path) {
|
||||
$capabilities = array(
|
||||
'multi_ack_detailed',
|
||||
'no-done',
|
||||
'side-band-64k',
|
||||
'ofs-delta',
|
||||
'thin-pack',
|
||||
'include-tag',
|
||||
'agent=php-git-server/1');
|
||||
|
||||
$head = resolve_ref($git_path, 'HEAD');
|
||||
@@ -49,9 +53,17 @@ function git_upload_pack_advertise_native($repository) {
|
||||
function git_upload_pack_parse_request($input) {
|
||||
$request = array('wants' => array(), 'haves' => array(), 'capabilities' => array());
|
||||
$first_want = TRUE;
|
||||
$want_phase_done = FALSE;
|
||||
$complete = FALSE;
|
||||
|
||||
while (($packet = git_protocol_read_packet($input)) !== FALSE) {
|
||||
if ($packet['type'] === 'flush') {
|
||||
if ($want_phase_done && !empty($request['haves'])
|
||||
&& isset($request['capabilities']['no-done'])) {
|
||||
$complete = TRUE;
|
||||
break;
|
||||
}
|
||||
$want_phase_done = TRUE;
|
||||
continue;
|
||||
}
|
||||
if ($packet['type'] !== 'data') {
|
||||
@@ -70,13 +82,14 @@ function git_upload_pack_parse_request($input) {
|
||||
} else if (preg_match('~^have ([0-9a-f]{40})$~D', $line, $matches)) {
|
||||
$request['haves'][] = $matches[1];
|
||||
} else if ($line === 'done') {
|
||||
$complete = TRUE;
|
||||
break;
|
||||
} else {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
if ($packet === FALSE || empty($request['wants'])) {
|
||||
if (!$complete || empty($request['wants'])) {
|
||||
return FALSE;
|
||||
}
|
||||
return $request;
|
||||
@@ -121,6 +134,21 @@ function git_upload_pack_rpc_native($repository, $input) {
|
||||
if ($objects === FALSE) {
|
||||
return FALSE;
|
||||
}
|
||||
if (isset($request['capabilities']['include-tag'])) {
|
||||
foreach (get_repository_refs($repository['path']) as $ref) {
|
||||
if (strpos($ref[0], 'refs/tags/') !== 0) {
|
||||
continue;
|
||||
}
|
||||
$tag = git_object_store_read($store, $ref[1]);
|
||||
if ($tag === FALSE || $tag['type'] !== 'tag') {
|
||||
continue;
|
||||
}
|
||||
$links = git_object_store_links($tag);
|
||||
if ($links !== FALSE && isset($objects[$links[0]])) {
|
||||
$objects[$tag['oid']] = $tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$last_common = NULL;
|
||||
foreach ($request['haves'] as $have) {
|
||||
@@ -139,8 +167,14 @@ function git_upload_pack_rpc_native($repository, $input) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
echo git_protocol_format_packet(
|
||||
$last_common === NULL ? "NAK\n" : 'ACK '.$last_common."\n");
|
||||
if ($last_common === NULL) {
|
||||
echo git_protocol_format_packet("NAK\n");
|
||||
} else if (isset($request['capabilities']['multi_ack_detailed'])) {
|
||||
echo git_protocol_format_packet('ACK '.$last_common." common\n");
|
||||
echo git_protocol_format_packet('ACK '.$last_common." ready\n");
|
||||
} else {
|
||||
echo git_protocol_format_packet('ACK '.$last_common."\n");
|
||||
}
|
||||
if (isset($request['capabilities']['side-band-64k'])) {
|
||||
git_upload_pack_send_sideband(1, $pack);
|
||||
echo '0000';
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__).'/lib/repository.php';
|
||||
require_once dirname(__DIR__).'/lib/git_protocol.php';
|
||||
require_once dirname(__DIR__).'/lib/git_object_store.php';
|
||||
require_once dirname(__DIR__).'/lib/git_upload_pack.php';
|
||||
require_once dirname(__DIR__).'/lib/git_receive_pack.php';
|
||||
require_once dirname(__DIR__).'/lib/git_service.php';
|
||||
|
||||
if (!isset($GLOBALS['test_results'])) {
|
||||
$GLOBALS['test_results'] = array('passed' => 0, 'failed' => 0);
|
||||
}
|
||||
|
||||
function test_case($name, $callback) {
|
||||
try {
|
||||
$callback();
|
||||
$GLOBALS['test_results']['passed'] += 1;
|
||||
fwrite(STDOUT, "ok - ".$name."\n");
|
||||
} catch (Throwable $error) {
|
||||
$GLOBALS['test_results']['failed'] += 1;
|
||||
fwrite(STDERR, "not ok - ".$name."\n ".$error->getMessage()."\n");
|
||||
}
|
||||
}
|
||||
|
||||
function test_fail($message) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
function test_assert_true($value, $message='Expected value to be TRUE.') {
|
||||
if ($value !== TRUE) {
|
||||
test_fail($message);
|
||||
}
|
||||
}
|
||||
|
||||
function test_assert_false($value, $message='Expected value to be FALSE.') {
|
||||
if ($value !== FALSE) {
|
||||
test_fail($message);
|
||||
}
|
||||
}
|
||||
|
||||
function test_assert_same($expected, $actual, $message='') {
|
||||
if ($expected !== $actual) {
|
||||
$detail = 'Expected '.var_export($expected, TRUE).', got '.var_export($actual, TRUE).'.';
|
||||
test_fail($message === '' ? $detail : $message.' '.$detail);
|
||||
}
|
||||
}
|
||||
|
||||
function test_assert_contains($needle, $haystack, $message='') {
|
||||
if (strpos($haystack, $needle) === FALSE) {
|
||||
$detail = 'Expected output to contain '.var_export($needle, TRUE).'.';
|
||||
test_fail($message === '' ? $detail : $message.' '.$detail);
|
||||
}
|
||||
}
|
||||
|
||||
function test_capture_output($callback) {
|
||||
ob_start();
|
||||
$result = NULL;
|
||||
$output = '';
|
||||
try {
|
||||
$result = $callback();
|
||||
$output = ob_get_contents();
|
||||
} finally {
|
||||
ob_end_clean();
|
||||
}
|
||||
if ($result !== TRUE) {
|
||||
test_fail('Protocol function returned failure.');
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
function test_stream($contents) {
|
||||
$stream = fopen('php://temp', 'w+b');
|
||||
if ($stream === FALSE || fwrite($stream, $contents) !== strlen($contents)) {
|
||||
test_fail('Unable to create test input stream.');
|
||||
}
|
||||
rewind($stream);
|
||||
return $stream;
|
||||
}
|
||||
|
||||
function test_remove_directory($path) {
|
||||
if (is_link($path) || is_file($path)) {
|
||||
@unlink($path);
|
||||
return;
|
||||
}
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
foreach (scandir($path) as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
test_remove_directory($path.DIRECTORY_SEPARATOR.$entry);
|
||||
}
|
||||
@rmdir($path);
|
||||
}
|
||||
|
||||
function test_protocol_packets($buffer) {
|
||||
$stream = test_stream($buffer);
|
||||
$packets = array();
|
||||
while (!feof($stream)) {
|
||||
$position = ftell($stream);
|
||||
$packet = git_protocol_read_packet($stream);
|
||||
if ($packet === FALSE) {
|
||||
fseek($stream, $position);
|
||||
break;
|
||||
}
|
||||
$packets[] = $packet;
|
||||
}
|
||||
fclose($stream);
|
||||
return $packets;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
function git_fixture_create() {
|
||||
$path = sys_get_temp_dir().'/php-git-server-test-'.bin2hex(random_bytes(8)).'.git';
|
||||
if (!git_service_init_bare_repository($path)) {
|
||||
test_remove_directory($path);
|
||||
test_fail('Unable to initialize fixture repository.');
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
function git_fixture_repository($path, $options=array()) {
|
||||
return array(
|
||||
'url' => '/fixture.git',
|
||||
'path' => $path,
|
||||
'options' => array_merge(repository_default_options(), $options));
|
||||
}
|
||||
|
||||
function git_fixture_object($path, $type, $body, $write=TRUE) {
|
||||
$contents = $type.' '.strlen($body)."\0".$body;
|
||||
$oid = sha1($contents);
|
||||
if ($write) {
|
||||
$directory = $path.'/objects/'.substr($oid, 0, 2);
|
||||
if (!is_dir($directory) && !mkdir($directory, 0777, TRUE)) {
|
||||
test_fail('Unable to create fixture object directory.');
|
||||
}
|
||||
$compressed = gzcompress($contents);
|
||||
if ($compressed === FALSE
|
||||
|| file_put_contents($directory.'/'.substr($oid, 2), $compressed) !== strlen($compressed)) {
|
||||
test_fail('Unable to write fixture object.');
|
||||
}
|
||||
}
|
||||
return array('type' => $type, 'body' => $body, 'oid' => $oid);
|
||||
}
|
||||
|
||||
function git_fixture_history($path, $messages) {
|
||||
$blob = git_fixture_object($path, 'blob', "fixture\n");
|
||||
$tree = git_fixture_object($path, 'tree', "100644 fixture.txt\0".hex2bin($blob['oid']));
|
||||
$commits = array();
|
||||
$parent = NULL;
|
||||
foreach ($messages as $index => $message) {
|
||||
$body = 'tree '.$tree['oid']."\n";
|
||||
if ($parent !== NULL) {
|
||||
$body .= 'parent '.$parent."\n";
|
||||
}
|
||||
$timestamp = 1700000000 + $index;
|
||||
$body .= 'author Fixture <[email protected]> '.$timestamp." +0000\n";
|
||||
$body .= 'committer Fixture <[email protected]> '.$timestamp." +0000\n\n".$message."\n";
|
||||
$commit = git_fixture_object($path, 'commit', $body);
|
||||
$commits[] = $commit['oid'];
|
||||
$parent = $commit['oid'];
|
||||
}
|
||||
return $commits;
|
||||
}
|
||||
|
||||
function git_fixture_tag($path, $target, $name='v1.0.0') {
|
||||
$body = 'object '.$target."\n";
|
||||
$body .= "type commit\n";
|
||||
$body .= 'tag '.$name."\n";
|
||||
$body .= "tagger Fixture <[email protected]> 1700000100 +0000\n\nfixture tag\n";
|
||||
$tag = git_fixture_object($path, 'tag', $body);
|
||||
return $tag['oid'];
|
||||
}
|
||||
|
||||
function git_fixture_write_ref($path, $ref, $oid) {
|
||||
$ref_path = $path.'/'.$ref;
|
||||
if (!is_dir(dirname($ref_path)) && !mkdir(dirname($ref_path), 0777, TRUE)) {
|
||||
test_fail('Unable to create fixture ref directory.');
|
||||
}
|
||||
if (file_put_contents($ref_path, $oid."\n") !== 41) {
|
||||
test_fail('Unable to write fixture ref.');
|
||||
}
|
||||
}
|
||||
|
||||
function git_fixture_pack_objects($objects) {
|
||||
$indexed = array();
|
||||
foreach ($objects as $object) {
|
||||
$indexed[$object['oid']] = $object;
|
||||
}
|
||||
$pack = git_object_store_build_pack($indexed);
|
||||
if ($pack === FALSE) {
|
||||
test_fail('Unable to build fixture pack.');
|
||||
}
|
||||
return $pack;
|
||||
}
|
||||
|
||||
function git_fixture_pack($path, $oids) {
|
||||
$store = git_object_store_create($path);
|
||||
$objects = git_object_store_collect($store, $oids);
|
||||
if ($objects === FALSE) {
|
||||
test_fail('Unable to collect objects for fixture pack.');
|
||||
}
|
||||
return git_fixture_pack_objects(array_values($objects));
|
||||
}
|
||||
|
||||
function git_fixture_remove_loose_objects($path) {
|
||||
foreach (scandir($path.'/objects') as $entry) {
|
||||
if (!preg_match('~^[0-9a-f]{2}$~D', $entry)) {
|
||||
continue;
|
||||
}
|
||||
test_remove_directory($path.'/objects/'.$entry);
|
||||
}
|
||||
}
|
||||
|
||||
function git_fixture_has_object($path, $oid) {
|
||||
$store = git_object_store_create($path);
|
||||
return $store !== FALSE && git_object_store_read($store, $oid) !== FALSE;
|
||||
}
|
||||
|
||||
function git_fixture_receive_input($updates, $pack='', $capabilities='report-status') {
|
||||
$input = '';
|
||||
foreach ($updates as $index => $update) {
|
||||
$payload = $update['old'].' '.$update['new'].' '.$update['ref'];
|
||||
if ($index === 0 && $capabilities !== '') {
|
||||
$payload .= "\0".$capabilities;
|
||||
}
|
||||
$input .= git_protocol_format_packet($payload."\n");
|
||||
}
|
||||
return $input.'0000'.$pack;
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/bootstrap.php';
|
||||
require_once __DIR__.'/git_fixture.php';
|
||||
|
||||
function receive_test_run($fixture, $updates, $pack='', $options=array()) {
|
||||
$result = receive_test_run_result($fixture, $updates, $pack, $options);
|
||||
return $result[1];
|
||||
}
|
||||
|
||||
function receive_test_run_result($fixture, $updates, $pack='', $options=array()) {
|
||||
$input = test_stream(git_fixture_receive_input($updates, $pack));
|
||||
ob_start();
|
||||
try {
|
||||
$result = git_receive_pack_rpc_native(git_fixture_repository($fixture, $options), $input);
|
||||
$output = ob_get_contents();
|
||||
} finally {
|
||||
ob_end_clean();
|
||||
fclose($input);
|
||||
}
|
||||
return array($result, $output);
|
||||
}
|
||||
|
||||
test_case('receive-pack does not advertise atomic before crash recovery exists', function () {
|
||||
test_assert_false(strpos(git_receive_pack_capabilities(), 'atomic') !== FALSE);
|
||||
});
|
||||
|
||||
test_case('receive-pack accepts the first branch push', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one'));
|
||||
$pack = git_fixture_pack($fixture, array($history[0]));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
test_assert_false(git_fixture_has_object($fixture, $history[0]));
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $history[0],
|
||||
'ref' => 'refs/heads/main')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack ok\n", $output);
|
||||
test_assert_contains("ok refs/heads/main\n", $output);
|
||||
test_assert_same($history[0], resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack accepts a fast-forward update', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one', 'two'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[0]);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => $history[0],
|
||||
'new' => $history[1],
|
||||
'ref' => 'refs/heads/main')),
|
||||
git_fixture_pack($fixture, array($history[1])));
|
||||
|
||||
test_assert_contains("unpack ok\n", $output);
|
||||
test_assert_same($history[1], resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects a non-fast-forward update', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$main = git_fixture_history($fixture, array('main'))[0];
|
||||
$other = git_fixture_history($fixture, array('other'))[0];
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $main);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => $main,
|
||||
'new' => $other,
|
||||
'ref' => 'refs/heads/main')),
|
||||
git_fixture_pack($fixture, array($other)));
|
||||
|
||||
test_assert_contains("unpack non-fast-forward\n", $output);
|
||||
test_assert_contains("ng refs/heads/main non-fast-forward\n", $output);
|
||||
test_assert_same($main, resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack deletes an existing ref', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$oid = git_fixture_history($fixture, array('one'))[0];
|
||||
git_fixture_write_ref($fixture, 'refs/heads/topic', $oid);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => $oid,
|
||||
'new' => str_repeat('0', 40),
|
||||
'ref' => 'refs/heads/topic')));
|
||||
|
||||
test_assert_contains("unpack ok\n", $output);
|
||||
test_assert_contains("ok refs/heads/topic\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/heads/topic')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack accepts an annotated tag', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$commit = git_fixture_history($fixture, array('one'))[0];
|
||||
$tag = git_fixture_tag($fixture, $commit);
|
||||
$pack = git_fixture_pack($fixture, array($tag));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $tag,
|
||||
'ref' => 'refs/tags/v1.0.0')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack ok\n", $output);
|
||||
test_assert_same($tag, resolve_ref($fixture, 'refs/tags/v1.0.0')[1]);
|
||||
test_assert_true(git_fixture_has_object($fixture, $tag));
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects a corrupt pack without changing refs', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$oid = git_fixture_history($fixture, array('one'))[0];
|
||||
$pack = git_fixture_pack($fixture, array($oid));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$pack[strlen($pack) - 1] = chr(ord($pack[strlen($pack) - 1]) ^ 1);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $oid,
|
||||
'ref' => 'refs/heads/main')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack invalid pack\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
test_assert_false(git_fixture_has_object($fixture, $oid));
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects an object graph with a missing object', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$missing_tree = str_repeat('a', 40);
|
||||
$body = 'tree '.$missing_tree."\n";
|
||||
$body .= "author Fixture <[email protected]> 1700000000 +0000\n";
|
||||
$body .= "committer Fixture <[email protected]> 1700000000 +0000\n\nmissing tree\n";
|
||||
$commit = git_fixture_object($fixture, 'commit', $body);
|
||||
$pack = git_fixture_pack_objects(array($commit));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $commit['oid'],
|
||||
'ref' => 'refs/heads/main')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack invalid object graph\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects a commit without a tree header', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$body = "author Fixture <[email protected]> 1700000000 +0000\n";
|
||||
$body .= "committer Fixture <[email protected]> 1700000000 +0000\n\nmissing tree\n";
|
||||
$commit = git_fixture_object($fixture, 'commit', $body);
|
||||
$pack = git_fixture_pack_objects(array($commit));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $commit['oid'],
|
||||
'ref' => 'refs/heads/main')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack invalid object graph\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects a malformed tree entry mode', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$blob = git_fixture_object($fixture, 'blob', "fixture\n");
|
||||
$tree = git_fixture_object($fixture, 'tree', "100600 fixture.txt\0".hex2bin($blob['oid']));
|
||||
$body = 'tree '.$tree['oid']."\n";
|
||||
$body .= "author Fixture <[email protected]> 1700000000 +0000\n";
|
||||
$body .= "committer Fixture <[email protected]> 1700000000 +0000\n\ninvalid tree\n";
|
||||
$commit = git_fixture_object($fixture, 'commit', $body);
|
||||
$pack = git_fixture_pack_objects(array($blob, $tree, $commit));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $commit['oid'],
|
||||
'ref' => 'refs/heads/main')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack invalid object graph\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects an annotated tag without a declared type', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$commit = git_fixture_history($fixture, array('one'))[0];
|
||||
$body = 'object '.$commit."\n";
|
||||
$body .= "tag v1.0.0\n";
|
||||
$body .= "tagger Fixture <[email protected]> 1700000100 +0000\n\ninvalid tag\n";
|
||||
$tag = git_fixture_object($fixture, 'tag', $body);
|
||||
$pack = git_fixture_pack_objects(array($tag));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $tag['oid'],
|
||||
'ref' => 'refs/tags/v1.0.0')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack invalid object graph\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/tags/v1.0.0')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects a commit whose tree points to a blob', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$blob = git_fixture_object($fixture, 'blob', "not a tree\n");
|
||||
$body = 'tree '.$blob['oid']."\n";
|
||||
$body .= "author Fixture <[email protected]> 1700000000 +0000\n";
|
||||
$body .= "committer Fixture <[email protected]> 1700000000 +0000\n\nwrong type\n";
|
||||
$commit = git_fixture_object($fixture, 'commit', $body);
|
||||
$pack = git_fixture_pack_objects(array($blob, $commit));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $commit['oid'],
|
||||
'ref' => 'refs/heads/main')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack invalid object graph\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects a malformed parent header', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$blob = git_fixture_object($fixture, 'blob', "fixture\n");
|
||||
$tree = git_fixture_object($fixture, 'tree', "100644 fixture.txt\0".hex2bin($blob['oid']));
|
||||
$body = 'tree '.$tree['oid']."\n";
|
||||
$body .= "parent not-an-object-id\n";
|
||||
$body .= "author Fixture <[email protected]> 1700000000 +0000\n";
|
||||
$body .= "committer Fixture <[email protected]> 1700000000 +0000\n\ninvalid parent\n";
|
||||
$commit = git_fixture_object($fixture, 'commit', $body);
|
||||
$pack = git_fixture_pack_objects(array($blob, $tree, $commit));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $commit['oid'],
|
||||
'ref' => 'refs/heads/main')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack invalid object graph\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects a tag whose declared type does not match its target', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$blob = git_fixture_object($fixture, 'blob', "fixture\n");
|
||||
$body = 'object '.$blob['oid']."\n";
|
||||
$body .= "type commit\n";
|
||||
$body .= "tag v1.0.0\n";
|
||||
$body .= "tagger Fixture <[email protected]> 1700000100 +0000\n\nwrong type\n";
|
||||
$tag = git_fixture_object($fixture, 'tag', $body);
|
||||
$pack = git_fixture_pack_objects(array($blob, $tag));
|
||||
git_fixture_remove_loose_objects($fixture);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => str_repeat('0', 40),
|
||||
'new' => $tag['oid'],
|
||||
'ref' => 'refs/tags/v1.0.0')),
|
||||
$pack);
|
||||
|
||||
test_assert_contains("unpack invalid object graph\n", $output);
|
||||
test_assert_same(NULL, resolve_ref($fixture, 'refs/tags/v1.0.0')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rejects a stale old OID without changing the ref', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one', 'two', 'three'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[1]);
|
||||
$output = receive_test_run($fixture, array(array(
|
||||
'old' => $history[0],
|
||||
'new' => $history[2],
|
||||
'ref' => 'refs/heads/main')),
|
||||
git_fixture_pack($fixture, array($history[2])));
|
||||
|
||||
test_assert_contains("unpack stale or locked ref\n", $output);
|
||||
test_assert_contains("ng refs/heads/main stale or locked ref\n", $output);
|
||||
test_assert_same($history[1], resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack returns failure when refs are rejected', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one', 'two'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[1]);
|
||||
$result = receive_test_run_result($fixture, array(array(
|
||||
'old' => $history[0],
|
||||
'new' => $history[1],
|
||||
'ref' => 'refs/heads/main')),
|
||||
git_fixture_pack($fixture, array($history[1])));
|
||||
|
||||
test_assert_false($result[0]);
|
||||
test_assert_contains("ng refs/heads/main stale or locked ref\n", $result[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('receive-pack rolls back earlier refs when a later commit fails', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one', 'two'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[0]);
|
||||
git_fixture_write_ref($fixture, 'refs/heads/topic', $history[0]);
|
||||
$updates = array(
|
||||
array('old' => $history[0], 'new' => $history[1], 'ref' => 'refs/heads/main'),
|
||||
array('old' => $history[0], 'new' => $history[1], 'ref' => 'refs/heads/topic'));
|
||||
$locks = git_receive_pack_lock_updates($fixture, $updates);
|
||||
test_assert_true(is_array($locks), 'Expected both refs to lock.');
|
||||
$locks[1]['path'] = $fixture.'/missing/refs/heads/topic';
|
||||
|
||||
test_assert_false(git_receive_pack_commit_updates($fixture, $locks));
|
||||
test_assert_same($history[0], resolve_ref($fixture, 'refs/heads/main')[1]);
|
||||
test_assert_same($history[0], resolve_ref($fixture, 'refs/heads/topic')[1]);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/bootstrap.php';
|
||||
require_once __DIR__.'/git_fixture.php';
|
||||
|
||||
test_case('upload-pack advertises refs and capabilities', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[0]);
|
||||
|
||||
$output = test_capture_output(function () use ($fixture) {
|
||||
return git_upload_pack_advertise_native(git_fixture_repository($fixture));
|
||||
});
|
||||
|
||||
test_assert_contains($history[0].' HEAD', $output);
|
||||
test_assert_contains($history[0].' refs/heads/main', $output);
|
||||
test_assert_contains('side-band-64k', $output);
|
||||
test_assert_contains('multi_ack_detailed', $output);
|
||||
test_assert_contains('no-done', $output);
|
||||
test_assert_contains('thin-pack', $output);
|
||||
test_assert_contains('include-tag', $output);
|
||||
test_assert_same('0000', substr($output, -4));
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('upload-pack returns NAK and a valid pack for clone', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[0]);
|
||||
$request = git_protocol_format_packet('want '.$history[0]."\n").'0000'
|
||||
.git_protocol_format_packet("done\n");
|
||||
$input = test_stream($request);
|
||||
$output = test_capture_output(function () use ($fixture, $input) {
|
||||
return git_upload_pack_rpc_native(git_fixture_repository($fixture), $input);
|
||||
});
|
||||
fclose($input);
|
||||
|
||||
$nak = git_protocol_format_packet("NAK\n");
|
||||
test_assert_same($nak, substr($output, 0, strlen($nak)));
|
||||
$pack = substr($output, strlen($nak));
|
||||
test_assert_same('PACK', substr($pack, 0, 4));
|
||||
test_assert_same(hash('sha1', substr($pack, 0, -20), TRUE), substr($pack, -20));
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('upload-pack ACKs a common commit and excludes its history', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one', 'two'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[1]);
|
||||
$request = git_protocol_format_packet('want '.$history[1]."\n").'0000'
|
||||
.git_protocol_format_packet('have '.$history[0]."\n")
|
||||
.git_protocol_format_packet("done\n");
|
||||
$input = test_stream($request);
|
||||
$output = test_capture_output(function () use ($fixture, $input) {
|
||||
return git_upload_pack_rpc_native(git_fixture_repository($fixture), $input);
|
||||
});
|
||||
fclose($input);
|
||||
|
||||
test_assert_same(
|
||||
git_protocol_format_packet('ACK '.$history[0]."\n"),
|
||||
substr($output, 0, 49));
|
||||
test_assert_same('PACK', substr($output, 49, 4));
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('upload-pack rejects an unadvertised want', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('advertised', 'hidden'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[0]);
|
||||
$request = git_protocol_format_packet('want '.$history[1]."\n").'0000'
|
||||
.git_protocol_format_packet("done\n");
|
||||
$input = test_stream($request);
|
||||
$result = NULL;
|
||||
$output = '';
|
||||
ob_start();
|
||||
try {
|
||||
$result = git_upload_pack_rpc_native(git_fixture_repository($fixture), $input);
|
||||
$output = ob_get_contents();
|
||||
} finally {
|
||||
ob_end_clean();
|
||||
fclose($input);
|
||||
}
|
||||
test_assert_false($result);
|
||||
test_assert_same('', $output);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('upload-pack reports detailed common and ready acknowledgments', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one', 'two'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[1]);
|
||||
$request = git_protocol_format_packet(
|
||||
'want '.$history[1]." multi_ack_detailed no-done\n").'0000'
|
||||
.git_protocol_format_packet('have '.$history[0]."\n").'0000';
|
||||
$input = test_stream($request);
|
||||
$output = test_capture_output(function () use ($fixture, $input) {
|
||||
return git_upload_pack_rpc_native(git_fixture_repository($fixture), $input);
|
||||
});
|
||||
fclose($input);
|
||||
|
||||
test_assert_contains('ACK '.$history[0]." common\n", $output);
|
||||
test_assert_contains('ACK '.$history[0]." ready\n", $output);
|
||||
test_assert_contains('PACK', $output);
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('upload-pack include-tag sends an annotated tag for a wanted commit', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('one'));
|
||||
$tag = git_fixture_tag($fixture, $history[0]);
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[0]);
|
||||
git_fixture_write_ref($fixture, 'refs/tags/v1.0.0', $tag);
|
||||
$request = git_protocol_format_packet(
|
||||
'want '.$history[0]." include-tag\n").'0000'.git_protocol_format_packet("done\n");
|
||||
$input = test_stream($request);
|
||||
$output = test_capture_output(function () use ($fixture, $input) {
|
||||
return git_upload_pack_rpc_native(git_fixture_repository($fixture), $input);
|
||||
});
|
||||
fclose($input);
|
||||
|
||||
$nak = git_protocol_format_packet("NAK\n");
|
||||
$pack = substr($output, strlen($nak));
|
||||
$store = git_object_store_create($fixture);
|
||||
$objects = git_receive_pack_parse($pack, $store, 0, 0);
|
||||
test_assert_true(is_array($objects));
|
||||
test_assert_true(isset($objects[$tag]), 'Expected include-tag to add the tag object.');
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
});
|
||||
@@ -1,49 +1,35 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__).'/lib/repository.php';
|
||||
require_once dirname(__DIR__).'/lib/git_service.php';
|
||||
require_once __DIR__.'/bootstrap.php';
|
||||
require_once __DIR__.'/git_fixture.php';
|
||||
|
||||
function test_remove_directory($path) {
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
test_case('unborn HEAD follows the first pushed branch', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$oid = git_fixture_history($fixture, array('one'))[0];
|
||||
git_fixture_write_ref($fixture, 'refs/heads/master', $oid);
|
||||
test_assert_true(git_service_update_unborn_head(
|
||||
$fixture, array('refs/heads/master')));
|
||||
test_assert_same(
|
||||
"ref: refs/heads/master\n",
|
||||
file_get_contents($fixture.'/HEAD'));
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
foreach (scandir($path) as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
$entry_path = $path.'/'.$entry;
|
||||
if (is_dir($entry_path)) {
|
||||
test_remove_directory($entry_path);
|
||||
} else {
|
||||
unlink($entry_path);
|
||||
}
|
||||
});
|
||||
|
||||
test_case('an established HEAD remains unchanged', function () {
|
||||
$fixture = git_fixture_create();
|
||||
try {
|
||||
$history = git_fixture_history($fixture, array('main', 'master'));
|
||||
git_fixture_write_ref($fixture, 'refs/heads/main', $history[0]);
|
||||
git_fixture_write_ref($fixture, 'refs/heads/master', $history[1]);
|
||||
test_assert_true(git_service_update_unborn_head(
|
||||
$fixture, array('refs/heads/master')));
|
||||
test_assert_same(
|
||||
"ref: refs/heads/main\n",
|
||||
file_get_contents($fixture.'/HEAD'));
|
||||
} finally {
|
||||
test_remove_directory($fixture);
|
||||
}
|
||||
rmdir($path);
|
||||
}
|
||||
|
||||
$path = sys_get_temp_dir().'/php-git-server-head-'.bin2hex(random_bytes(8)).'.git';
|
||||
mkdir($path.'/refs/heads', 0777, TRUE);
|
||||
mkdir($path.'/objects', 0777, TRUE);
|
||||
file_put_contents($path.'/HEAD', "ref: refs/heads/main\n");
|
||||
file_put_contents($path.'/refs/heads/master', str_repeat('1', 40)."\n");
|
||||
|
||||
try {
|
||||
git_service_update_unborn_head($path, array('refs/heads/master'));
|
||||
$head = file_get_contents($path.'/HEAD');
|
||||
if ($head !== "ref: refs/heads/master\n") {
|
||||
fwrite(STDERR, 'Expected HEAD to follow the first pushed branch, got: '.$head);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
file_put_contents($path.'/refs/heads/main', str_repeat('2', 40)."\n");
|
||||
git_service_update_unborn_head($path, array('refs/heads/master'));
|
||||
$head = file_get_contents($path.'/HEAD');
|
||||
if ($head !== "ref: refs/heads/master\n") {
|
||||
fwrite(STDERR, "Expected an existing HEAD target to remain unchanged.\n");
|
||||
exit(1);
|
||||
}
|
||||
} finally {
|
||||
test_remove_directory($path);
|
||||
}
|
||||
|
||||
fwrite(STDOUT, "unborn HEAD regression test passed\n");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/bootstrap.php';
|
||||
|
||||
$test_files = array(
|
||||
__DIR__.'/protocol_upload_pack.php',
|
||||
__DIR__.'/protocol_receive_pack.php',
|
||||
__DIR__.'/regression_unborn_head.php');
|
||||
|
||||
foreach ($test_files as $test_file) {
|
||||
require $test_file;
|
||||
}
|
||||
|
||||
$passed = $GLOBALS['test_results']['passed'];
|
||||
$failed = $GLOBALS['test_results']['failed'];
|
||||
fwrite(STDOUT, "\n".$passed.' passed, '.$failed." failed\n");
|
||||
exit($failed === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user