Skip to content

Commit

Permalink
Parse Git Client messages
Browse files Browse the repository at this point in the history
  • Loading branch information
adamziel committed Jan 5, 2025
1 parent 6357abd commit dd73f79
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ static private function wrap_object($type, $object) {
return "$type_name $length\x00" . $object;
}

static public function encode_packet_lines(array $payloads): string {
$lines = [];
foreach($payloads as $payload) {
if($payload === '0000' || $payload === '0001' || $payload === '0002') {
$lines[] = $payload;
} else {
$lines[] = self::encode_packet_line($payload);
}
}
return implode('', $lines);
}

static public function encode_packet_line(string $payload): string {
$length = strlen($payload) + 4;
return sprintf("%04x", $length) . $payload;
Expand Down Expand Up @@ -239,7 +251,7 @@ static public function decode_next_packet_line($pack_bytes, &$offset) {
case '0002':
return ['type' => '#response-end'];
default:
$length = intval($packet_length_bytes, 16);
$length = intval($packet_length_bytes, 16) - 4 ;
$payload = substr($pack_bytes, $offset, $length);
if(str_ends_with($payload, "\n")) {
$payload = substr($payload, 0, -1);
Expand Down
176 changes: 122 additions & 54 deletions packages/playground/data-liberation/src/git/WP_Git_Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,45 @@ public function __construct(WP_Git_Repository $repository) {
/**
* Handle Git protocol v2 ls-refs command
*
* ls-refs is the command used to request a reference advertisement in v2.
* Unlike the current reference advertisement, ls-refs takes in arguments
* which can be used to limit the refs sent from the server.
*
* Additional features not supported in the base command will be advertised as
* the value of the command in the capability advertisement in the form of a space
* separated list of features: "<command>=<feature 1> <feature 2>"
*
* ls-refs takes in the following arguments:
*
* symrefs
* In addition to the object pointed by it, show the underlying ref
* pointed by it when showing a symbolic ref.
*
* peel
* Show peeled tags.
*
* ref-prefix <prefix>
* When specified, only references having a prefix matching one of
* the provided prefixes are displayed. Multiple instances may be
* given, in which case references matching any prefix will be
* shown. Note that this is purely for optimization; a server MAY
* show refs not matching the prefix if it chooses, and clients
* should filter the result themselves.
*
* unborn
* The server will send information about HEAD even if it is a symref
* pointing to an unborn branch in the form "unborn HEAD symref-target:<target>".
*
* @see https://git-scm.com/docs/protocol-v2#_ls_refs
* @param array $request The parsed request data
* @return string The response in Git protocol v2 format
*/
public function handle_ls_refs_request($request) {
$parsed = $this->parse_ls_refs_request($request);
$parsed = $this->parse_message($request);
if(!$parsed) {
return false;
}
$prefix = $parsed['arguments']['ref-prefix'] ?? '';
$prefix = $parsed['arguments']['ref-prefix'][0] ?? '';

$refs = $this->repository->list_refs($prefix);
$response = '';
Expand Down Expand Up @@ -75,60 +105,29 @@ public function capability_advertise() {
"0000";
}

/**
* ls-refs is the command used to request a reference advertisement in v2.
* Unlike the current reference advertisement, ls-refs takes in arguments
* which can be used to limit the refs sent from the server.
*
* Additional features not supported in the base command will be advertised as
* the value of the command in the capability advertisement in the form of a space
* separated list of features: "<command>=<feature 1> <feature 2>"
*
* ls-refs takes in the following arguments:
*
* symrefs
* In addition to the object pointed by it, show the underlying ref
* pointed by it when showing a symbolic ref.
*
* peel
* Show peeled tags.
*
* ref-prefix <prefix>
* When specified, only references having a prefix matching one of
* the provided prefixes are displayed. Multiple instances may be
* given, in which case references matching any prefix will be
* shown. Note that this is purely for optimization; a server MAY
* show refs not matching the prefix if it chooses, and clients
* should filter the result themselves.
*
* unborn
* The server will send information about HEAD even if it is a symref
* pointing to an unborn branch in the form "unborn HEAD symref-target:<target>".
*
* @see https://git-scm.com/docs/protocol-v2#_ls_refs
*
* @param string $request_bytes Raw request bytes
* @return array Parsed request data
*/
public function parse_ls_refs_request($request_bytes) {
$parsed = [
'capabilities' => [],
'arguments' => [],
];

// Parse the capability advertisement part
public function parse_message($request_bytes) {
$offset = 0;
return [
'capabilities' => $this->parse_capabilities($request_bytes, $offset),
'arguments' => $this->parse_arguments($request_bytes, $offset),
];
}

private function parse_capabilities($request_bytes, &$offset=0) {
$capabilities = [];
while (true) {
$line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset);
if ($line === false || $line['type'] !== '#packet') {
break;
}

list($key, $value) = explode('=', $line['payload']);
$parsed['capabilities'][$key] = $value;
$capabilities[$key] = $value;
}
return $capabilities;
}

// Parse the optional arguments part
private function parse_arguments($request_bytes, &$offset=0) {
$arguments = [];
while (true) {
$line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset);
if ($line === false || $line['type'] !== '#packet') {
Expand All @@ -137,15 +136,84 @@ public function parse_ls_refs_request($request_bytes) {

$space_at = strpos($line['payload'], ' ');
if($space_at === false) {
$parsed['arguments'][$line['payload']] = true;
continue;
$key = $line['payload'];
$value = true;
} else {
$key = substr($line['payload'], 0, $space_at);
$value = substr($line['payload'], $space_at + 1);
}
$key = substr($line['payload'], 0, $space_at);
$value = substr($line['payload'], $space_at + 1);
$parsed['arguments'][$key] = $value;
}

return $parsed;
if(!array_key_exists($key, $arguments)) {
$arguments[$key] = [];
}
$arguments[$key][] = $value;
}
return $arguments;
}

/**
* Handle Git protocol v2 fetch command with "want" packets
*
* @param array $request The parsed request data
* @return string The response in Git protocol v2 format containing the pack data
*/
public function handle_fetch_request($request) {
$parsed = $this->parse_message($request);
if (!$parsed || empty($parsed['arguments']['want'])) {
return false;
}

$objects_to_send = [];
foreach ($parsed['arguments']['want'] as $want_hash) {
// For each wanted commit, find objects not present in any of the have commits
$new_objects = $this->repository->find_objects_added_in(
$want_hash,
$parsed['arguments']['have'] ?? ['0000000000000000000000000000000000000000']
);
$objects_to_send = array_merge($objects_to_send, $new_objects);
}
$objects_to_send = array_unique($objects_to_send);

// Pack the objects
$pack_objects = [];
foreach ($objects_to_send as $oid) {
$this->repository->read_object($oid);

// Apply blob filters if specified
if ($this->repository->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_BLOB) {
$filter = $parsed['arguments']['filter'] ?? null;
if ($filter) {
if ($filter['type'] === 'none') {
continue; // Skip all blobs
} else if ($filter['type'] === 'limit') {
$content = $this->repository->read_entire_object_contents();
if (strlen($content) > $filter['size']) {
continue; // Skip large blobs
}
}
}
}

$pack_objects[] = [
'type' => $this->repository->get_type(),
'content' => $this->repository->read_entire_object_contents(),
];
}

// Handle deepen if specified
if (isset($parsed['arguments']['deepen'])) {
// @TODO: Implement history truncation based on deepen value
// This would involve walking the commit history and including
// only commits within the specified depth
}

// Encode the pack
$pack_data = WP_Git_Pack_Processor::encode($pack_objects);

// Format the response according to protocol v2
return
WP_Git_Pack_Processor::encode_packet_line("packfile\n") .
WP_Git_Pack_Processor::encode_packet_line("\x01" . $pack_data) . // side-band channel 1
"0000";
}
}

0 comments on commit dd73f79

Please sign in to comment.