Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
parameters:
level: 5
level: 10
treatPhpDocTypesAsCertain: false
paths:
- src
Expand Down
3 changes: 3 additions & 0 deletions src/Diff/MyersDiff.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ private static function myers(array $a, array $b, int $n, int $m): array
* rounds 0..d-1 finished. For backtrace at step s, we need V after
* round s-1 finished = trace[s] (NOT trace[s-1]).
*
* @param array<int, array<int, int>> $trace
* @param array<int, string> $a
* @param array<int, string> $b
* @return array<int, array{type: string, line: string}>
*/
private static function backtrace(
Expand Down
15 changes: 14 additions & 1 deletion src/Diff/TreeDiff.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,15 @@ private function detectRenames(array $results): array
$matched = [];

foreach ($deleted as $di => $del) {
if ($del->oldHash === null) {
continue;
}

$bestScore = 0;
$bestIdx = null;

foreach ($added as $ai => $add) {
if (isset($matched[$ai])) {
if (isset($matched[$ai]) || $add->newHash === null) {
continue;
}

Expand Down Expand Up @@ -101,6 +105,11 @@ private function detectRenames(array $results): array

if ($bestIdx !== null) {
$add = $added[$bestIdx];

if ($add->newHash === null) {
continue;
}

$oldContent = $this->readBlobContent($del->oldHash);
$newContent = $this->readBlobContent($add->newHash);
$hunks = DiffAlgorithm::diff($oldContent, $newContent, $this->algorithm);
Expand Down Expand Up @@ -220,6 +229,10 @@ private function diffInto(?ObjectId $oldTree, ?ObjectId $newTree, string $prefix
continue;
}

if ($oldEntry === null || $newEntry === null) {
continue;
}

if ($oldEntry['hash'] === $newEntry['hash']) {
continue;
}
Expand Down
40 changes: 34 additions & 6 deletions src/Encoding/BinaryReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,8 @@ public function readByte(): int
public function readUint32(): int
{
$data = $this->read(4);
$unpacked = unpack('N', $data);

return (int) $unpacked[1];
return $this->unpackOne('N', $data);
}

/**
Expand All @@ -117,9 +116,9 @@ public function readUint24(): int
public function readUint64(): int
{
$data = $this->read(8);
$parts = unpack('Nhigh/Nlow', $data);
$parts = $this->unpackPair('Nhigh/Nlow', $data);

return ((int) $parts['high'] << 32) | (int) $parts['low'];
return ($parts['high'] << 32) | $parts['low'];
}

/**
Expand All @@ -128,9 +127,38 @@ public function readUint64(): int
public function readUint16(): int
{
$data = $this->read(2);
$unpacked = unpack('n', $data);

return (int) $unpacked[1];
return $this->unpackOne('n', $data);
}

private function unpackOne(string $format, string $data): int
{
$unpacked = unpack($format, $data);

if ($unpacked === false || !isset($unpacked[1]) || !is_int($unpacked[1])) {
throw new RuntimeException("Failed to unpack {$format}");
}

return $unpacked[1];
}

/**
* @return array{high: int, low: int}
*/
private function unpackPair(string $format, string $data): array
{
$unpacked = unpack($format, $data);

if (
$unpacked === false
|| !isset($unpacked['high'], $unpacked['low'])
|| !is_int($unpacked['high'])
|| !is_int($unpacked['low'])
) {
throw new RuntimeException("Failed to unpack {$format}");
}

return ['high' => $unpacked['high'], 'low' => $unpacked['low']];
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Encoding/VarInt.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ public static function encodePackSize(int $type, int $size): string
$result = '';

while ($size > 0) {
$result .= chr($byte | 0x80);
$result .= chr(($byte | 0x80) & 0xFF);
$byte = $size & 0x7F;
$size >>= 7;
}

$result .= chr($byte);
$result .= chr($byte & 0xFF);

return $result;
}
Expand Down
8 changes: 8 additions & 0 deletions src/Graph/Blame.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ private function matchedLineIndexes(array $olderLines, array $newerLines): array
return array_keys($matchedNewIndexes);
}

/**
* @param array<int, string> $olderLines
* @param array<int, string> $newerLines
*/
private function isPrefixMatch(array $olderLines, array $newerLines, int $length): bool
{
for ($index = 0; $index < $length; $index++) {
Expand All @@ -217,6 +221,10 @@ private function isPrefixMatch(array $olderLines, array $newerLines, int $length
return true;
}

/**
* @param array<int, string> $olderLines
* @param array<int, string> $newerLines
*/
private function isSuffixMatch(array $olderLines, array $newerLines, int $oldCount, int $newCount): bool
{
$offset = $newCount - $oldCount;
Expand Down
52 changes: 27 additions & 25 deletions src/Graph/CommitWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,7 @@ public function __construct(private readonly ObjectDatabase $objects)
*/
public function walk(ObjectId $from, int $limit = 50): array
{
$commits = [];
$visited = [];
$queue = new \SplPriorityQueue();

$this->enqueue($queue, $from, $visited);

while (!$queue->isEmpty() && count($commits) < $limit) {
['id' => $id, 'commit' => $object] = $queue->extract();

$commits[] = $object;

foreach ($object->parents as $parentId) {
$this->enqueue($queue, $parentId, $visited);
}
}

return $commits;
return $this->walkAll([$from], $limit);
}

/**
Expand All @@ -57,15 +41,15 @@ public function walkAll(array $from, int $limit = 50): array
{
$commits = [];
$visited = [];
$queue = new \SplPriorityQueue();
$queue = [];

foreach ($from as $id) {
$this->enqueue($queue, $id, $visited);
}

while (!$queue->isEmpty() && count($commits) < $limit) {
['id' => $id, 'commit' => $object] = $queue->extract();

while ($queue !== [] && count($commits) < $limit) {
$entry = array_shift($queue);
$object = $entry['commit'];
$commits[] = $object;

foreach ($object->parents as $parentId) {
Expand All @@ -77,9 +61,13 @@ public function walkAll(array $from, int $limit = 50): array
}

/**
* Insert by descending timestamp (newest first).
*
* @param array<int, array{timestamp: int, commit: Commit}> $queue
* @param array<string, true> $visited
* @param-out array<int, array{timestamp: int, commit: Commit}> $queue
*/
private function enqueue(\SplPriorityQueue $queue, ObjectId $id, array &$visited): void
private function enqueue(array &$queue, ObjectId $id, array &$visited): void
{
if (isset($visited[$id->hex])) {
return;
Expand All @@ -89,9 +77,23 @@ private function enqueue(\SplPriorityQueue $queue, ObjectId $id, array &$visited

$object = $this->objects->read($id);

if ($object instanceof Commit) {
$timestamp = $object->committerTimestamp() ?? 0;
$queue->insert(['id' => $id, 'commit' => $object], $timestamp);
if (!$object instanceof Commit) {
return;
}

$timestamp = $object->committerTimestamp() ?? 0;
$entry = ['timestamp' => $timestamp, 'commit' => $object];

$count = count($queue);

for ($i = 0; $i < $count; $i++) {
if ($queue[$i]['timestamp'] < $timestamp) {
array_splice($queue, $i, 0, [$entry]);

return;
}
}

$queue[] = $entry;
}
}
1 change: 1 addition & 0 deletions src/Graph/Grep.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function __construct(private readonly ObjectDatabase $objects)
/**
* Search for a pattern in all files in a tree.
*
* @param array{regex?: bool, ignore_case?: bool} $options
* @return array<int, array{path: string, line: int, content: string}>
*/
public function grep(ObjectId $treeId, string $pattern, string $prefix = '', array $options = []): array
Expand Down
29 changes: 23 additions & 6 deletions src/Lfs/LfsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,24 +122,41 @@ private function batch(string $operation, array $objects): array

$data = json_decode($response, true);

if (!is_array($data) || !isset($data['objects'])) {
if (!is_array($data) || !isset($data['objects']) || !is_array($data['objects'])) {
throw new ProtocolException('Invalid LFS batch response');
}

$results = [];

foreach ($data['objects'] as $obj) {
if (!is_array($obj)) {
continue;
}

$href = null;
$actions = $obj['actions'] ?? null;

if (is_array($actions) && isset($actions[$operation]) && is_array($actions[$operation])) {
$candidate = $actions[$operation]['href'] ?? null;

if (is_string($candidate)) {
$href = $candidate;
}
}

$errorBag = $obj['error'] ?? null;
$error = null;

if (isset($obj['actions'][$operation]['href'])) {
$href = $obj['actions'][$operation]['href'];
if (is_array($errorBag) && isset($errorBag['message']) && is_string($errorBag['message'])) {
$error = $errorBag['message'];
}

$error = $obj['error']['message'] ?? null;
$oid = $obj['oid'] ?? '';
$size = $obj['size'] ?? 0;

$results[] = [
'oid' => $obj['oid'] ?? '',
'size' => $obj['size'] ?? 0,
'oid' => is_string($oid) ? $oid : '',
'size' => is_int($size) ? $size : 0,
'href' => $href,
'error' => $error,
];
Expand Down
4 changes: 2 additions & 2 deletions src/Merge/MergeBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,9 @@ private function parentsOf(string $hex): array
return [];
}

return array_map(
return array_values(array_map(
static fn (ObjectId $parent): string => $parent->hex,
$commit->parents,
);
));
}
}
7 changes: 6 additions & 1 deletion src/Object/ObjectId.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,13 @@ public static function compute(ObjectType $type, string $content, string $algo =
{
$header = $type->value . ' ' . strlen($content) . "\0";
$hex = hash($algo, $header . $content);
$binary = hex2bin($hex);

return new self($hex, hex2bin($hex), $algo);
if ($binary === false) {
throw new \RuntimeException("hash({$algo}) produced invalid hex: {$hex}");
}

return new self($hex, $binary, $algo);
}

/**
Expand Down
10 changes: 7 additions & 3 deletions src/Pack/DeltaResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ final class DeltaResolver
{
public static function maxChainDepth(): int
{
return defined('PITMASTER_MAX_DELTA_CHAIN')
? (int) constant('PITMASTER_MAX_DELTA_CHAIN')
: 50;
if (!defined('PITMASTER_MAX_DELTA_CHAIN')) {
return 50;
}

$value = constant('PITMASTER_MAX_DELTA_CHAIN');

return is_int($value) ? $value : 50;
}
}
18 changes: 14 additions & 4 deletions src/Pack/PackFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ public function readAtOffset(int $offset, ?string $expectedHash = null): GitObje
*/
private function resolveAtOffset(int $offset, int $depth): array
{
$maxDepth = defined('PITMASTER_MAX_DELTA_CHAIN')
? (int) constant('PITMASTER_MAX_DELTA_CHAIN')
: 50;
$maxDepth = DeltaResolver::maxChainDepth();

if ($depth > $maxDepth) {
throw PackParseException::deltaChainTooDeep($depth, $maxDepth);
Expand All @@ -119,16 +117,28 @@ private function resolveAtOffset(int $offset, int $depth): array

if (!$entry->isDelta()) {
$type = $entry->objectType();

if ($type === null) {
throw PackParseException::invalidDeltaBase("invalid base type at offset {$offset}");
}

$content = $this->readCompressedData($entry->dataOffset, $entry->uncompressedSize);

return ['type' => $type, 'content' => $content];
}

if ($entry->isOfsDelta()) {
if ($entry->baseOffset === null) {
throw PackParseException::invalidDeltaBase("ofs-delta missing baseOffset at {$offset}");
}

$baseOffset = $entry->entryOffset - $entry->baseOffset;
$base = $this->resolveAtOffset($baseOffset, $depth + 1);
} else {
// REF_DELTA: look up the base by hash
if ($entry->baseHash === null) {
throw PackParseException::invalidDeltaBase("ref-delta missing baseHash at {$offset}");
}

$basePackOffset = $this->index->findOffset($entry->baseHash);

if ($basePackOffset === null) {
Expand Down
4 changes: 1 addition & 3 deletions src/Pack/PackIndexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,7 @@ private static function resolveObjectAtOffset(
array &$hashToOffset,
int $depth = 0,
): array {
$maxDepth = defined('PITMASTER_MAX_DELTA_CHAIN')
? (int) constant('PITMASTER_MAX_DELTA_CHAIN')
: 50;
$maxDepth = DeltaResolver::maxChainDepth();

if ($depth > $maxDepth) {
throw PackParseException::deltaChainTooDeep($depth, $maxDepth);
Expand Down
Loading