| | } |
| | |
| | - public function setRepository( |
| | - string $repoPath |
| | - ): void { |
| | - $this->repoPath = \rtrim( $repoPath, '/' ); |
| | - |
| | - $objPath = $this->repoPath . '/objects'; |
| | - $this->refs = new GitRefs( $this->repoPath ); |
| | - $this->packs = new GitPacks( $objPath ); |
| | - $this->loose = new LooseObjects( $objPath ); |
| | - $this->packWriter = new PackfileWriter( |
| | - $this->packs, $this->loose |
| | - ); |
| | - } |
| | - |
| | - public function resolve( |
| | - string $reference |
| | - ): string { |
| | - return $this->refs->resolve( $reference ); |
| | - } |
| | - |
| | - public function getMainBranch(): array { |
| | - return $this->refs->getMainBranch(); |
| | - } |
| | - |
| | - public function eachBranch( |
| | - callable $callback |
| | - ): void { |
| | - $this->refs->scanRefs( |
| | - 'refs/heads', $callback |
| | - ); |
| | - } |
| | - |
| | - public function eachTag( |
| | - callable $callback |
| | - ): void { |
| | - $this->refs->scanRefs( |
| | - 'refs/tags', |
| | - function( $name, $sha ) use ( $callback ) { |
| | - $callback( |
| | - $this->parseTagData( |
| | - $name, $sha, $this->read( $sha ) |
| | - ) |
| | - ); |
| | - } |
| | - ); |
| | - } |
| | - |
| | - public function walk( |
| | - string $refOrSha, |
| | - callable $callback, |
| | - string $path = '' |
| | - ): void { |
| | - $sha = $this->resolve( $refOrSha ); |
| | - $treeSha = $sha !== '' |
| | - ? $this->getTreeSha( $sha ) |
| | - : ''; |
| | - |
| | - if( $path !== '' && $treeSha !== '' ) { |
| | - $info = $this->resolvePath( |
| | - $treeSha, $path |
| | - ); |
| | - $treeSha = $info['isDir'] ? $info['sha'] : ''; |
| | - } |
| | - |
| | - if( $treeSha !== '' ) { |
| | - $this->walkTree( $treeSha, $callback ); |
| | - } |
| | - } |
| | - |
| | - public function readFile( |
| | - string $ref, |
| | - string $path |
| | - ): File { |
| | - $sha = $this->resolve( $ref ); |
| | - $tree = $sha !== '' |
| | - ? $this->getTreeSha( $sha ) |
| | - : ''; |
| | - $info = $tree !== '' |
| | - ? $this->resolvePath( $tree, $path ) |
| | - : []; |
| | - |
| | - return isset( $info['sha'] ) |
| | - && !$info['isDir'] |
| | - && $info['sha'] !== '' |
| | - ? new File( |
| | - \basename( $path ), |
| | - $info['sha'], |
| | - $info['mode'], |
| | - 0, |
| | - $this->getObjectSize( $info['sha'] ), |
| | - $this->peek( $info['sha'] ) |
| | - ) |
| | - : new MissingFile(); |
| | - } |
| | - |
| | - public function getObjectSize( |
| | - string $sha, |
| | - string $path = '' |
| | - ): int { |
| | - $target = $sha; |
| | - |
| | - if( $path !== '' ) { |
| | - $info = $this->resolvePath( |
| | - $this->getTreeSha( |
| | - $this->resolve( $sha ) |
| | - ), |
| | - $path |
| | - ); |
| | - $target = $info['sha'] ?? ''; |
| | - } |
| | - |
| | - return $target !== '' |
| | - ? $this->packs->getSize( $target ) |
| | - ?: $this->loose->getSize( $target ) |
| | - : 0; |
| | - } |
| | - |
| | - public function stream( |
| | - string $sha, |
| | - callable $callback, |
| | - string $path = '' |
| | - ): void { |
| | - $target = $sha; |
| | - |
| | - if( $path !== '' ) { |
| | - $info = $this->resolvePath( |
| | - $this->getTreeSha( |
| | - $this->resolve( $sha ) |
| | - ), |
| | - $path |
| | - ); |
| | - $target = isset( $info['isDir'] ) |
| | - && !$info['isDir'] |
| | - ? $info['sha'] |
| | - : ''; |
| | - } |
| | - |
| | - if( $target !== '' ) { |
| | - $this->slurp( $target, $callback ); |
| | - } |
| | - } |
| | - |
| | - public function peek( |
| | - string $sha, |
| | - int $length = 255 |
| | - ): string { |
| | - return $this->packs->getSize( $sha ) > 0 |
| | - ? $this->packs->peek( $sha, $length ) |
| | - : $this->loose->peek( $sha, $length ); |
| | - } |
| | - |
| | - public function read( string $sha ): string { |
| | - $size = $this->getObjectSize( $sha ); |
| | - $content = ''; |
| | - |
| | - if( $size > 0 && $size <= self::MAX_READ ) { |
| | - $this->slurp( |
| | - $sha, |
| | - function( $chunk ) use ( &$content ) { |
| | - $content .= $chunk; |
| | - } |
| | - ); |
| | - } |
| | - |
| | - return $content; |
| | - } |
| | - |
| | - public function history( |
| | - string $ref, |
| | - int $limit, |
| | - callable $callback |
| | - ): void { |
| | - $sha = $this->resolve( $ref ); |
| | - $count = 0; |
| | - $done = false; |
| | - |
| | - while( |
| | - !$done && $sha !== '' && $count < $limit |
| | - ) { |
| | - $data = $this->read( $sha ); |
| | - |
| | - if( $data === '' ) { |
| | - $done = true; |
| | - } else { |
| | - $id = $this->parseIdentity( |
| | - $data, '/^author (.*) <(.*)> (\d+)/m' |
| | - ); |
| | - $parentSha = $this->extractPattern( |
| | - $data, '/^parent (.*)$/m', 1 |
| | - ); |
| | - |
| | - $commit = new Commit( |
| | - $sha, |
| | - $this->extractMessage( $data ), |
| | - $id['name'], |
| | - $id['email'], |
| | - $id['timestamp'], |
| | - $parentSha |
| | - ); |
| | - |
| | - if( $callback( $commit ) === false ) { |
| | - $done = true; |
| | - } else { |
| | - $sha = $parentSha; |
| | - $count++; |
| | - } |
| | - } |
| | - } |
| | - } |
| | - |
| | - public function streamRaw( |
| | - string $subPath |
| | - ): bool { |
| | - $result = false; |
| | - |
| | - if( \strpos( $subPath, '..' ) === false ) { |
| | - $path = "{$this->repoPath}/$subPath"; |
| | - |
| | - if( \is_file( $path ) ) { |
| | - $real = \realpath( $path ); |
| | - $repo = \realpath( $this->repoPath ); |
| | - |
| | - if( |
| | - $real !== false |
| | - && \strpos( $real, $repo ) === 0 |
| | - ) { |
| | - \header( |
| | - 'X-Accel-Redirect: ' . $path |
| | - ); |
| | - \header( |
| | - 'Content-Type: application/octet-stream' |
| | - ); |
| | - $result = true; |
| | - } |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - public function eachRef( |
| | - callable $callback |
| | - ): void { |
| | - $head = $this->resolve( 'HEAD' ); |
| | - |
| | - if( $head !== '' ) { |
| | - $callback( 'HEAD', $head ); |
| | - } |
| | - |
| | - $this->refs->scanRefs( |
| | - 'refs/heads', |
| | - function( $n, $s ) use ( $callback ) { |
| | - $callback( "refs/heads/$n", $s ); |
| | - } |
| | - ); |
| | - |
| | - $this->refs->scanRefs( |
| | - 'refs/tags', |
| | - function( $n, $s ) use ( $callback ) { |
| | - $callback( "refs/tags/$n", $s ); |
| | - } |
| | - ); |
| | - } |
| | - |
| | - public function generatePackfile( |
| | - array $objs |
| | - ): Generator { |
| | - yield from $this->packWriter->generate( |
| | - $objs |
| | - ); |
| | - } |
| | - |
| | - public function collectObjects( |
| | - array $wants, |
| | - array $haves = [] |
| | - ): array { |
| | - $objs = $this->traverseObjects( $wants ); |
| | - |
| | - if( !empty( $haves ) ) { |
| | - foreach( |
| | - $this->traverseObjects( |
| | - $haves |
| | - ) as $sha => $type |
| | - ) { |
| | - unset( $objs[$sha] ); |
| | - } |
| | - } |
| | - |
| | - return $objs; |
| | - } |
| | - |
| | - public function parseTreeData( |
| | - string $data, |
| | - callable $callback |
| | - ): void { |
| | - $pos = 0; |
| | - $len = \strlen( $data ); |
| | - |
| | - while( $pos < $len ) { |
| | - $space = \strpos( $data, ' ', $pos ); |
| | - $eos = \strpos( $data, "\0", $space ); |
| | - |
| | - if( |
| | - $space === false |
| | - || $eos === false |
| | - || $eos + 21 > $len |
| | - ) { |
| | - break; |
| | - } |
| | - |
| | - $mode = \substr( |
| | - $data, $pos, $space - $pos |
| | - ); |
| | - $name = \substr( |
| | - $data, $space + 1, $eos - $space - 1 |
| | - ); |
| | - $sha = \bin2hex( |
| | - \substr( $data, $eos + 1, 20 ) |
| | - ); |
| | - |
| | - if( |
| | - $callback( $name, $sha, $mode ) === false |
| | - ) { |
| | - break; |
| | - } |
| | - |
| | - $pos = $eos + 21; |
| | - } |
| | - } |
| | - |
| | - private function slurp( |
| | - string $sha, |
| | - callable $callback |
| | - ): void { |
| | - if( |
| | - !$this->loose->stream( $sha, $callback ) |
| | - && !$this->packs->stream( |
| | - $sha, $callback |
| | - ) |
| | - ) { |
| | - $data = $this->packs->read( $sha ); |
| | - |
| | - if( $data !== '' ) { |
| | - $callback( $data ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - private function walkTree( |
| | - string $sha, |
| | - callable $callback |
| | - ): void { |
| | - $data = $this->read( $sha ); |
| | - $tree = $data !== '' |
| | - && \preg_match( |
| | - '/^tree (.*)$/m', $data, $m |
| | - ) |
| | - ? $this->read( $m[1] ) |
| | - : $data; |
| | - |
| | - if( |
| | - $tree !== '' |
| | - && $this->isTreeData( $tree ) |
| | - ) { |
| | - $this->parseTreeData( |
| | - $tree, |
| | - function( |
| | - $n, $s, $m |
| | - ) use ( $callback ) { |
| | - $dir = $m === '40000' |
| | - || $m === '040000'; |
| | - $isSub = $m === '160000'; |
| | - |
| | - $callback( new File( |
| | - $n, |
| | - $s, |
| | - $m, |
| | - 0, |
| | - $dir || $isSub |
| | - ? 0 |
| | - : $this->getObjectSize( $s ), |
| | - $dir || $isSub |
| | - ? '' |
| | - : $this->peek( $s ) |
| | - ) ); |
| | - } |
| | - ); |
| | - } |
| | - } |
| | - |
| | - private function isTreeData( |
| | - string $data |
| | - ): bool { |
| | - $len = \strlen( $data ); |
| | - $match = $len >= 25 |
| | - && \preg_match( |
| | - '/^(40000|100644|100755|120000|160000) /', |
| | - $data |
| | - ); |
| | - $eos = $match |
| | - ? \strpos( $data, "\0" ) |
| | - : false; |
| | - |
| | - return $match |
| | - && $eos !== false |
| | - && $eos + 21 <= $len; |
| | - } |
| | - |
| | - private function getTreeSha( |
| | - string $commitOrTreeSha |
| | - ): string { |
| | - $data = $this->read( $commitOrTreeSha ); |
| | - $sha = $commitOrTreeSha; |
| | - |
| | - if( |
| | - \preg_match( |
| | - '/^object ([0-9a-f]{40})/m', |
| | - $data, |
| | - $matches |
| | - ) |
| | - ) { |
| | - $sha = $this->getTreeSha( $matches[1] ); |
| | - } |
| | - |
| | - if( |
| | - $sha === $commitOrTreeSha |
| | - && \preg_match( |
| | - '/^tree ([0-9a-f]{40})/m', |
| | - $data, |
| | - $matches |
| | - ) |
| | - ) { |
| | - $sha = $matches[1]; |
| | - } |
| | - |
| | - return $sha; |
| | - } |
| | - |
| | - private function resolvePath( |
| | - string $treeSha, |
| | - string $path |
| | - ): array { |
| | - $parts = \explode( |
| | - '/', \trim( $path, '/' ) |
| | - ); |
| | - |
| | - $sha = $treeSha; |
| | - $mode = '40000'; |
| | - |
| | - foreach( $parts as $part ) { |
| | - $entry = $part !== '' && $sha !== '' |
| | - ? $this->findTreeEntry( $sha, $part ) |
| | - : [ 'sha' => '', 'mode' => '' ]; |
| | - |
| | - $sha = $entry['sha']; |
| | - $mode = $entry['mode']; |
| | - } |
| | - |
| | - return [ |
| | - 'sha' => $sha, |
| | - 'mode' => $mode, |
| | - 'isDir' => $mode === '40000' |
| | - || $mode === '040000' |
| | - ]; |
| | - } |
| | - |
| | - private function findTreeEntry( |
| | - string $treeSha, |
| | - string $name |
| | - ): array { |
| | - $entry = [ 'sha' => '', 'mode' => '' ]; |
| | - |
| | - $this->parseTreeData( |
| | - $this->read( $treeSha ), |
| | - function( |
| | - $n, $s, $m |
| | - ) use ( $name, &$entry ) { |
| | - if( $n === $name ) { |
| | - $entry = [ |
| | - 'sha' => $s, |
| | - 'mode' => $m |
| | - ]; |
| | - |
| | - return false; |
| | - } |
| | - } |
| | - ); |
| | - |
| | - return $entry; |
| | - } |
| | - |
| | - private function parseTagData( |
| | - string $name, |
| | - string $sha, |
| | - string $data |
| | - ): Tag { |
| | - $isAnn = \strncmp( |
| | - $data, 'object ', 7 |
| | - ) === 0; |
| | - |
| | - $id = $this->parseIdentity( |
| | - $data, |
| | - $isAnn |
| | - ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m' |
| | - : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m' |
| | - ); |
| | - |
| | - return new Tag( |
| | - $name, |
| | - $sha, |
| | - $isAnn |
| | - ? $this->extractPattern( |
| | - $data, |
| | - '/^object (.*)$/m', |
| | - 1, |
| | - $sha |
| | - ) |
| | - : $sha, |
| | - $id['timestamp'], |
| | - $this->extractMessage( $data ), |
| | - $id['name'] |
| | - ); |
| | - } |
| | - |
| | - private function extractPattern( |
| | - string $data, |
| | - string $pattern, |
| | - int $group, |
| | - string $default = '' |
| | - ): string { |
| | - return \preg_match( |
| | - $pattern, $data, $matches |
| | - ) |
| | - ? $matches[$group] |
| | - : $default; |
| | - } |
| | - |
| | - private function parseIdentity( |
| | - string $data, |
| | - string $pattern |
| | - ): array { |
| | - $found = \preg_match( |
| | - $pattern, $data, $matches |
| | - ); |
| | - |
| | - return [ |
| | - 'name' => $found |
| | - ? \trim( $matches[1] ) |
| | - : 'Unknown', |
| | - 'email' => $found |
| | - ? $matches[2] |
| | - : '', |
| | - 'timestamp' => $found |
| | - ? (int)$matches[3] |
| | - : 0 |
| | - ]; |
| | - } |
| | - |
| | - private function extractMessage( |
| | - string $data |
| | - ): string { |
| | - $pos = \strpos( $data, "\n\n" ); |
| | - |
| | - return $pos !== false |
| | - ? \trim( \substr( $data, $pos + 2 ) ) |
| | - : ''; |
| | - } |
| | - |
| | - private function traverseObjects( |
| | - array $roots |
| | - ): array { |
| | - $objs = []; |
| | - $queue = []; |
| | - |
| | - foreach( $roots as $sha ) { |
| | - $queue[] = [ |
| | - 'sha' => $sha, |
| | - 'type' => 0 |
| | - ]; |
| | - } |
| | - |
| | - while( !empty( $queue ) ) { |
| | - $item = \array_pop( $queue ); |
| | - $sha = $item['sha']; |
| | - $type = $item['type']; |
| | - |
| | - if( !isset( $objs[$sha] ) ) { |
| | - $data = $type !== 3 |
| | - ? $this->read( $sha ) |
| | - : ''; |
| | - $type = $type === 0 |
| | - ? $this->getObjectType( $data ) |
| | - : $type; |
| | - |
| | - $objs[$sha] = $type; |
| | - |
| | - if( $type === 1 ) { |
| | - if( |
| | - \preg_match( |
| | - '/^tree ([0-9a-f]{40})/m', |
| | - $data, |
| | - $m |
| | - ) |
| | - ) { |
| | - $queue[] = [ |
| | - 'sha' => $m[1], |
| | - 'type' => 2 |
| | - ]; |
| | - } |
| | - |
| | - if( |
| | - \preg_match_all( |
| | - '/^parent ([0-9a-f]{40})/m', |
| | - $data, |
| | - $m |
| | - ) |
| | - ) { |
| | - foreach( $m[1] as $parentSha ) { |
| | - $queue[] = [ |
| | - 'sha' => $parentSha, |
| | - 'type' => 1 |
| | - ]; |
| | - } |
| | - } |
| | - } elseif( $type === 2 ) { |
| | - $this->parseTreeData( |
| | - $data, |
| | - function( |
| | - $n, $s, $m |
| | - ) use ( &$queue ) { |
| | - if( $m !== '160000' ) { |
| | - $queue[] = [ |
| | - 'sha' => $s, |
| | - 'type' => $m === '40000' |
| | - || $m === '040000' |
| | - ? 2 |
| | - : 3 |
| | - ]; |
| | - } |
| | - } |
| | - ); |
| | - } elseif( $type === 4 ) { |
| | - if( |
| | - \preg_match( |
| | - '/^object ([0-9a-f]{40})/m', |
| | - $data, |
| | - $m |
| | - ) |
| | - ) { |
| | - $nextType = 1; |
| | - |
| | - if( |
| | - \preg_match( |
| | - '/^type (commit|tree|blob|tag)/m', |
| | - $data, |
| | - $t |
| | - ) |
| | - ) { |
| | - $map = [ |
| | - 'commit' => 1, |
| | - 'tree' => 2, |
| | - 'blob' => 3, |
| | - 'tag' => 4 |
| | - ]; |
| | - |
| | - $nextType = $map[$t[1]] ?? 1; |
| | - } |
| | - |
| | - $queue[] = [ |
| | - 'sha' => $m[1], |
| | - 'type' => $nextType |
| | - ]; |
| | - } |
| | - } |
| | - } |
| | - } |
| | - |
| | - return $objs; |
| | - } |
| | - |
| | - private function getObjectType( |
| | - string $data |
| | - ): int { |
| | - $result = 3; |
| | - |
| | - if( \strpos( $data, "tree " ) === 0 ) { |
| | - $result = 1; |
| | - } elseif( |
| | - \strpos( $data, "object " ) === 0 |
| | - ) { |
| | - $result = 4; |
| | - } elseif( $this->isTreeData( $data ) ) { |
| | - $result = 2; |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | -} |
| | - |
| | -class MissingFile extends File { |
| | - public function __construct() { |
| | - parent::__construct( |
| | - '', '', '0', 0, 0, '' |
| | - ); |
| | + public function setRepository( string $repoPath ): void { |
| | + $this->repoPath = \rtrim( $repoPath, '/' ); |
| | + $objPath = $this->repoPath . '/objects'; |
| | + $this->refs = new GitRefs( $this->repoPath ); |
| | + $this->packs = new GitPacks( $objPath ); |
| | + $this->loose = new LooseObjects( $objPath ); |
| | + $this->packWriter = new PackfileWriter( |
| | + $this->packs, $this->loose |
| | + ); |
| | + } |
| | + |
| | + public function resolve( string $reference ): string { |
| | + return $this->refs->resolve( $reference ); |
| | + } |
| | + |
| | + public function getMainBranch(): array { |
| | + return $this->refs->getMainBranch(); |
| | + } |
| | + |
| | + public function eachBranch( callable $callback ): void { |
| | + $this->refs->scanRefs( 'refs/heads', $callback ); |
| | + } |
| | + |
| | + public function eachTag( callable $callback ): void { |
| | + $this->refs->scanRefs( |
| | + 'refs/tags', |
| | + function( $name, $sha ) use ( $callback ) { |
| | + $callback( |
| | + new Tag( $name, $sha, $this->read( $sha ) ) |
| | + ); |
| | + } |
| | + ); |
| | + } |
| | + |
| | + public function walk( |
| | + string $refOrSha, |
| | + callable $callback, |
| | + string $path = '' |
| | + ): void { |
| | + $sha = $this->resolve( $refOrSha ); |
| | + $treeSha = $sha !== '' ? $this->getTreeSha( $sha ) : ''; |
| | + |
| | + if( $path !== '' && $treeSha !== '' ) { |
| | + $info = $this->resolvePath( $treeSha, $path ); |
| | + $treeSha = $info['isDir'] ? $info['sha'] : ''; |
| | + } |
| | + |
| | + if( $treeSha !== '' ) { |
| | + $this->walkTree( $treeSha, $callback ); |
| | + } |
| | + } |
| | + |
| | + public function readFile( string $ref, string $path ): File { |
| | + $sha = $this->resolve( $ref ); |
| | + $tree = $sha !== '' ? $this->getTreeSha( $sha ) : ''; |
| | + $info = $tree !== '' ? $this->resolvePath( $tree, $path ) : []; |
| | + |
| | + return isset( $info['sha'] ) |
| | + && !$info['isDir'] |
| | + && $info['sha'] !== '' |
| | + ? new File( |
| | + \basename( $path ), |
| | + $info['sha'], |
| | + $info['mode'], |
| | + 0, |
| | + $this->getObjectSize( $info['sha'] ), |
| | + $this->peek( $info['sha'] ) |
| | + ) |
| | + : new MissingFile(); |
| | + } |
| | + |
| | + public function getObjectSize( string $sha, string $path = '' ): int { |
| | + return $path !== '' |
| | + ? ( $this->resolvePath( |
| | + $this->getTreeSha( $this->resolve( $sha ) ), |
| | + $path |
| | + )['sha'] ?? '' ) !== '' |
| | + ? $this->packs->getSize( $this->resolvePath( $this->getTreeSha( $this->resolve( $sha ) ), $path )['sha'] ) |
| | + ?: $this->loose->getSize( $this->resolvePath( $this->getTreeSha( $this->resolve( $sha ) ), $path )['sha'] ) |
| | + : 0 |
| | + : ( $sha !== '' |
| | + ? $this->packs->getSize( $sha ) ?: $this->loose->getSize( $sha ) |
| | + : 0 ); |
| | + } |
| | + |
| | + public function stream( |
| | + string $sha, |
| | + callable $callback, |
| | + string $path = '' |
| | + ): void { |
| | + $target = $sha; |
| | + |
| | + if( $path !== '' ) { |
| | + $info = $this->resolvePath( |
| | + $this->getTreeSha( $this->resolve( $sha ) ), |
| | + $path |
| | + ); |
| | + $target = isset( $info['isDir'] ) && !$info['isDir'] |
| | + ? $info['sha'] |
| | + : ''; |
| | + } |
| | + |
| | + if( $target !== '' ) { |
| | + $this->slurp( $target, $callback ); |
| | + } |
| | + } |
| | + |
| | + public function peek( string $sha, int $length = 255 ): string { |
| | + return $this->packs->getSize( $sha ) > 0 |
| | + ? $this->packs->peek( $sha, $length ) |
| | + : $this->loose->peek( $sha, $length ); |
| | + } |
| | + |
| | + public function read( string $sha ): string { |
| | + $size = $this->getObjectSize( $sha ); |
| | + $content = ''; |
| | + |
| | + if( $size > 0 && $size <= self::MAX_READ ) { |
| | + $this->slurp( |
| | + $sha, |
| | + function( $chunk ) use ( &$content ) { |
| | + $content .= $chunk; |
| | + } |
| | + ); |
| | + } |
| | + |
| | + return $content; |
| | + } |
| | + |
| | + public function history( |
| | + string $ref, |
| | + int $limit, |
| | + callable $callback |
| | + ): void { |
| | + $this->traverseHistory( |
| | + $this->resolve( $ref ), |
| | + $limit, |
| | + $callback, |
| | + 0 |
| | + ); |
| | + } |
| | + |
| | + private function traverseHistory( |
| | + string $sha, |
| | + int $limit, |
| | + callable $callback, |
| | + int $count |
| | + ): void { |
| | + $data = $sha !== '' && $count < $limit |
| | + ? $this->read( $sha ) |
| | + : ''; |
| | + |
| | + if( $data !== '' ) { |
| | + $commit = new Commit( $sha, $data ); |
| | + |
| | + if( $callback( $commit ) !== false ) { |
| | + $commit->provideParent( |
| | + function( $parent ) use ( $limit, $callback, $count ): void { |
| | + $this->traverseHistory( |
| | + $parent, |
| | + $limit, |
| | + $callback, |
| | + $count + 1 |
| | + ); |
| | + } |
| | + ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + public function streamRaw( string $subPath ): bool { |
| | + return \strpos( $subPath, '..' ) === false |
| | + && \is_file( "{$this->repoPath}/$subPath" ) |
| | + && \realpath( "{$this->repoPath}/$subPath" ) !== false |
| | + && \strpos( |
| | + \realpath( "{$this->repoPath}/$subPath" ), |
| | + \realpath( $this->repoPath ) |
| | + ) === 0 |
| | + ? $this->sendHeaders( "{$this->repoPath}/$subPath" ) |
| | + : false; |
| | + } |
| | + |
| | + private function sendHeaders( string $path ): bool { |
| | + \header( 'X-Accel-Redirect: ' . $path ); |
| | + \header( 'Content-Type: application/octet-stream' ); |
| | + |
| | + return true; |
| | + } |
| | + |
| | + public function eachRef( callable $callback ): void { |
| | + $head = $this->resolve( 'HEAD' ); |
| | + |
| | + if( $head !== '' ) { |
| | + $callback( 'HEAD', $head ); |
| | + } |
| | + |
| | + $this->refs->scanRefs( |
| | + 'refs/heads', |
| | + function( $n, $s ) use ( $callback ) { |
| | + $callback( "refs/heads/$n", $s ); |
| | + } |
| | + ); |
| | + |
| | + $this->refs->scanRefs( |
| | + 'refs/tags', |
| | + function( $n, $s ) use ( $callback ) { |
| | + $callback( "refs/tags/$n", $s ); |
| | + } |
| | + ); |
| | + } |
| | + |
| | + public function generatePackfile( array $objs ): Generator { |
| | + yield from $this->packWriter->generate( $objs ); |
| | + } |
| | + |
| | + public function collectObjects( |
| | + array $wants, |
| | + array $haves = [] |
| | + ): array { |
| | + $objs = $this->traverseObjects( $wants ); |
| | + |
| | + if( !empty( $haves ) ) { |
| | + foreach( $this->traverseObjects( $haves ) as $sha => $type ) { |
| | + unset( $objs[$sha] ); |
| | + } |
| | + } |
| | + |
| | + return $objs; |
| | + } |
| | + |
| | + public function parseTreeData( string $data, callable $callback ): void { |
| | + $pos = 0; |
| | + $len = \strlen( $data ); |
| | + |
| | + while( $pos < $len ) { |
| | + $space = \strpos( $data, ' ', $pos ); |
| | + $eos = \strpos( $data, "\0", $space ); |
| | + |
| | + if( $space === false || $eos === false || $eos + 21 > $len ) { |
| | + break; |
| | + } |
| | + |
| | + if( |
| | + $callback( |
| | + \substr( $data, $space + 1, $eos - $space - 1 ), |
| | + \bin2hex( \substr( $data, $eos + 1, 20 ) ), |
| | + \substr( $data, $pos, $space - $pos ) |
| | + ) === false |
| | + ) { |
| | + break; |
| | + } |
| | + |
| | + $pos = $eos + 21; |
| | + } |
| | + } |
| | + |
| | + private function slurp( string $sha, callable $callback ): void { |
| | + if( |
| | + !$this->loose->stream( $sha, $callback ) |
| | + && !$this->packs->stream( $sha, $callback ) |
| | + ) { |
| | + $data = $this->packs->read( $sha ); |
| | + |
| | + if( $data !== '' ) { |
| | + $callback( $data ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + private function walkTree( string $sha, callable $callback ): void { |
| | + $data = $this->read( $sha ); |
| | + $tree = $data !== '' && \preg_match( '/^tree (.*)$/m', $data, $m ) |
| | + ? $this->read( $m[1] ) |
| | + : $data; |
| | + |
| | + if( $tree !== '' && $this->isTreeData( $tree ) ) { |
| | + $this->parseTreeData( |
| | + $tree, |
| | + function( $n, $s, $m ) use ( $callback ) { |
| | + $dir = $m === '40000' || $m === '040000'; |
| | + $isSub = $m === '160000'; |
| | + |
| | + $callback( new File( |
| | + $n, |
| | + $s, |
| | + $m, |
| | + 0, |
| | + $dir || $isSub ? 0 : $this->getObjectSize( $s ), |
| | + $dir || $isSub ? '' : $this->peek( $s ) |
| | + ) ); |
| | + } |
| | + ); |
| | + } |
| | + } |
| | + |
| | + private function isTreeData( string $data ): bool { |
| | + $len = \strlen( $data ); |
| | + $match = $len >= 25 |
| | + && \preg_match( |
| | + '/^(40000|100644|100755|120000|160000) /', |
| | + $data |
| | + ); |
| | + |
| | + return $match |
| | + && \strpos( $data, "\0" ) !== false |
| | + && \strpos( $data, "\0" ) + 21 <= $len; |
| | + } |
| | + |
| | + private function getTreeSha( string $commitOrTreeSha ): string { |
| | + $data = $this->read( $commitOrTreeSha ); |
| | + |
| | + return \preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) |
| | + ? $this->getTreeSha( $matches[1] ) |
| | + : ( \preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) |
| | + ? $matches[1] |
| | + : $commitOrTreeSha ); |
| | + } |
| | + |
| | + private function resolvePath( string $treeSha, string $path ): array { |
| | + $parts = \explode( '/', \trim( $path, '/' ) ); |
| | + $sha = $treeSha; |
| | + $mode = '40000'; |
| | + |
| | + foreach( $parts as $part ) { |
| | + $entry = $part !== '' && $sha !== '' |
| | + ? $this->findTreeEntry( $sha, $part ) |
| | + : [ 'sha' => '', 'mode' => '' ]; |
| | + |
| | + $sha = $entry['sha']; |
| | + $mode = $entry['mode']; |
| | + } |
| | + |
| | + return [ |
| | + 'sha' => $sha, |
| | + 'mode' => $mode, |
| | + 'isDir' => $mode === '40000' || $mode === '040000' |
| | + ]; |
| | + } |
| | + |
| | + private function findTreeEntry( string $treeSha, string $name ): array { |
| | + $entry = [ 'sha' => '', 'mode' => '' ]; |
| | + |
| | + $this->parseTreeData( |
| | + $this->read( $treeSha ), |
| | + function( $n, $s, $m ) use ( $name, &$entry ) { |
| | + if( $n === $name ) { |
| | + $entry = [ 'sha' => $s, 'mode' => $m ]; |
| | + |
| | + return false; |
| | + } |
| | + } |
| | + ); |
| | + |
| | + return $entry; |
| | + } |
| | + |
| | + private function traverseObjects( array $roots ): array { |
| | + $objs = []; |
| | + $queue = []; |
| | + |
| | + foreach( $roots as $sha ) { |
| | + $queue[] = [ 'sha' => $sha, 'type' => 0 ]; |
| | + } |
| | + |
| | + while( !empty( $queue ) ) { |
| | + $item = \array_pop( $queue ); |
| | + $sha = $item['sha']; |
| | + $type = $item['type']; |
| | + |
| | + if( !isset( $objs[$sha] ) ) { |
| | + $data = $type !== 3 ? $this->read( $sha ) : ''; |
| | + $type = $type === 0 ? $this->getObjectType( $data ) : $type; |
| | + |
| | + $objs[$sha] = $type; |
| | + |
| | + if( $type === 1 ) { |
| | + if( \preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m ) ) { |
| | + $queue[] = [ 'sha' => $m[1], 'type' => 2 ]; |
| | + } |
| | + |
| | + if( \preg_match_all( '/^parent ([0-9a-f]{40})/m', $data, $m ) ) { |
| | + foreach( $m[1] as $parentSha ) { |
| | + $queue[] = [ 'sha' => $parentSha, 'type' => 1 ]; |
| | + } |
| | + } |
| | + } elseif( $type === 2 ) { |
| | + $this->parseTreeData( |
| | + $data, |
| | + function( $n, $s, $m ) use ( &$queue ) { |
| | + if( $m !== '160000' ) { |
| | + $queue[] = [ |
| | + 'sha' => $s, |
| | + 'type' => $m === '40000' || $m === '040000' ? 2 : 3 |
| | + ]; |
| | + } |
| | + } |
| | + ); |
| | + } elseif( $type === 4 ) { |
| | + if( \preg_match( '/^object ([0-9a-f]{40})/m', $data, $m ) ) { |
| | + $queue[] = [ |
| | + 'sha' => $m[1], |
| | + 'type' => \preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) |
| | + ? ([ 'commit' => 1, 'tree' => 2, 'blob' => 3, 'tag' => 4 ][$t[1]] ?? 1) |
| | + : 1 |
| | + ]; |
| | + } |
| | + } |
| | + } |
| | + } |
| | + |
| | + return $objs; |
| | + } |
| | + |
| | + private function getObjectType( string $data ): int { |
| | + return \strpos( $data, "tree " ) === 0 |
| | + ? 1 |
| | + : (\strpos( $data, "object " ) === 0 |
| | + ? 4 |
| | + : ($this->isTreeData( $data ) |
| | + ? 2 |
| | + : 3)); |
| | + } |
| | +} |
| | + |
| | +class MissingFile extends File { |
| | + public function __construct() { |
| | + parent::__construct( '', '', '0', 0, 0, '' ); |
| | } |
| | |