<?php
/*
* Copyright (C) 2008, 2009 Patrik Fimml
* Copyright (c) 2023 Daniel Marschall
*
* This file is part of glip.
*
* glip is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
* glip is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with glip. If not, see <http://www.gnu.org/licenses/>.
*/
namespace ViaThinkSoft\Glip;
class Git
{
public $dir;
private $packs;
const OBJ_NONE = 0;
const OBJ_COMMIT = 1;
const OBJ_TREE = 2;
const OBJ_BLOB = 3;
const OBJ_TAG = 4;
const OBJ_OFS_DELTA = 6;
const OBJ_REF_DELTA = 7;
static public function getTypeID($name) {
if ($name == 'commit')
return Git::OBJ_COMMIT;
else if ($name == 'tree')
return Git::OBJ_TREE;
else if ($name == 'blob')
return Git::OBJ_BLOB;
else if ($name == 'tag')
return Git::OBJ_TAG;
throw new \Exception
(sprintf('unknown type name: %s', $name));
}
static public function getTypeName($type) {
if ($type == Git::OBJ_COMMIT)
return 'commit';
else if ($type == Git::OBJ_TREE)
return 'tree';
else if ($type == Git::OBJ_BLOB)
return 'blob';
else if ($type == Git::OBJ_TAG)
return 'tag';
throw new \Exception
(sprintf('no string representation of type %d', $type));
}
public function __construct($dir) {
throw new \Exception
(sprintf('not a directory: %s', $dir));
if ($dh !== false) {
while (($entry = readdir($dh)) !== false)
if (preg_match('#^pack-([0-9a-fA-F]{40})\.idx$#', $entry, $m))
$this->packs[] = pack('H40', $m[1]);
}
}
/**
* @brief Tries to find $object_name in the fanout table in $f at $offset.
*
* @returns array The range where the object can be located (first possible
* location and past-the-end location)
*/
protected function readFanout($f, $object_name, $offset) {
if ($object_name[0] == "\x00") {
$cur = 0;
$after = Binary::fuint32($f);
} else {
fseek($f, $offset + (ord($object_name[0]) - 1) * 4);
$cur = Binary::fuint32($f);
$after = Binary::fuint32($f);
}
return array($cur, $after);
}
/**
* @brief Try to find an object in a pack.
*
* @param $object_name (string) name of the object (binary SHA1)
* @returns (array) an array consisting of the name of the pack (string) and
* the byte offset inside it, or NULL if not found
*/
protected function findPackedObject($object_name) {
foreach ($this->packs as $pack_name) {
/* check version */
$magic = fread($index, 4);
if ($magic != "\xFFtOc") {
/* version 1 */
/* read corresponding fanout entry */
list($cur, $after) = $this->readFanout($index, $object_name, 0);
$n = $after - $cur;
if ($n == 0)
continue;
/*
* TODO: do a binary search in [$offset, $offset+24*$n)
*/
fseek($index, 4 * 256 + 24 * $cur);
for ($i = 0; $i < $n; $i++) {
$off = Binary::fuint32($index);
$name = fread($index, 20);
if ($name == $object_name) {
/* we found the object */
return array($pack_name, $off);
}
}
} else {
/* version 2+ */
$version = Binary::fuint32($index);
if ($version == 2) {
list($cur, $after) = $this->readFanout($index, $object_name, 8);
if ($cur == $after)
continue;
fseek($index, 8 + 4 * 255);
$total_objects = Binary::fuint32($index);
/* look up sha1 */
fseek($index, 8 + 4 * 256 + 20 * $cur);
for ($i = $cur; $i < $after; $i++) {
$name = fread($index, 20);
if ($name == $object_name)
break;
}
if ($i == $after)
continue;
fseek($index, 8 + 4 * 256 + 24 * $total_objects + 4 * $i);
$off = Binary::fuint32($index);
if ($off & 0x80000000) {
/* packfile > 2 GB. Gee, you really want to handle this
* much data with PHP?
*/
throw new \Exception('64-bit packfiles offsets not implemented');
}
return array($pack_name, $off);
} else
throw new \Exception('unsupported pack index format');
}
}
/* not found */
return null;
}
/**
* @brief Apply the git delta $delta to the byte sequence $base.
*
* @param $delta (string) the delta to apply
* @param $base (string) the sequence to patch
* @returns (string) the patched byte sequence
*/
protected function applyDelta($delta, $base) {
$pos = 0;
$base_size = Binary::git_varint($delta, $pos);
$result_size = Binary::git_varint($delta, $pos);
$r = '';
while ($pos < strlen($delta)) {
$opcode = ord($delta[$pos++]);
if ($opcode & 0x80) {
/* copy a part of $base */
$off = 0;
if ($opcode & 0x01) $off = ord($delta[$pos++]);
if ($opcode & 0x02) $off |= ord($delta[$pos++]) << 8;
if ($opcode & 0x04) $off |= ord($delta[$pos++]) << 16;
if ($opcode & 0x08) $off |= ord($delta[$pos++]) << 24;
$len = 0;
if ($opcode & 0x10) $len = ord($delta[$pos++]);
if ($opcode & 0x20) $len |= ord($delta[$pos++]) << 8;
if ($opcode & 0x40) $len |= ord($delta[$pos++]) << 16;
if ($len == 0) $len = 0x10000;
$r .= substr($base, $off, $len);
} else {
/* take the next $opcode bytes as they are */
$r .= substr($delta, $pos, $opcode);
$pos += $opcode;
}
}
return $r;
}
/**
* @brief Unpack an object from a pack.
*
* @param $pack (resource) open .pack file
* @param $object_offset (integer) offset of the object in the pack
* @returns (array) an array consisting of the object type (int) and the
* binary representation of the object (string)
*/
protected function unpackObject($pack, $object_offset) {
fseek($pack, $object_offset);
/* read object header */
$type = ($c >> 4) & 0x07;
$size = $c & 0x0F;
for ($i = 4; $c & 0x80; $i += 7) {
$size |= (($c & 0x7F) << $i);
}
/* compare sha1_file.c:1608 unpack_entry */
if ($type == Git::OBJ_COMMIT || $type == Git::OBJ_TREE || $type == Git::OBJ_BLOB || $type == Git::OBJ_TAG) {
/*
* We don't know the actual size of the compressed
* data, so we'll assume it's less than
* $object_size+512.
*
* FIXME use PHP stream filter API as soon as it behaves
* consistently
*/
} else if ($type == Git::OBJ_OFS_DELTA) {
/* 20 = maximum varint length for offset */
$buf = fread($pack, $size + 512 + 20);
/*
* contrary to varints in other places, this one is big endian
* (and 1 is added each turn)
* see sha1_file.c (get_delta_base)
*/
$pos = 0;
$offset = -1;
do {
$offset++;
$offset = ($offset << 7) + ($c & 0x7F);
} while ($c & 0x80);
$base_offset = $object_offset - $offset;
list($type, $base) = $this->unpackObject($pack, $base_offset);
$data = $this->applyDelta($delta, $base);
} else if ($type == Git::OBJ_REF_DELTA) {
$base_name = fread($pack, 20);
list($type, $base) = $this->getRawObject($base_name);
// $size is the length of the uncompressed delta
$data = $this->applyDelta($delta, $base);
} else
throw new \Exception
(sprintf('object of unknown type %d', $type));
return array($type, $data);
}
/**
* @brief Fetch an object in its binary representation by name.
*
* Throws an exception if the object cannot be found.
*
* @param $object_name (string) name of the object (binary SHA1)
* @returns (array) an array consisting of the object type (int) and the
* binary representation of the object (string)
*/
protected function getRawObject($object_name) {
/* FIXME allow limiting the cache to a certain size */
if (isset($cache[$object_name]))
return $cache[$object_name];
sscanf($hdr, "%s %d", $type, $object_size);
$object_type = Git::getTypeID($type);
$r = array($object_type, $object_data);
} else if ($x = $this->findPackedObject($object_name)) {
list($pack_name, $object_offset) = $x;
/* check magic and version */
$magic = fread($pack, 4);
$version = Binary::fuint32($pack);
if ($magic != 'PACK' || $version != 2)
throw new \Exception('unsupported pack format');
$r = $this->unpackObject($pack, $object_offset);
} else
throw new \Exception
(sprintf('object not found: %s', bin2hex($object_name)));
$cache[$object_name] = $r;
return $r;
}
/**
* @brief Fetch an object in its PHP representation.
*
* @param $name (string) name of the object (binary SHA1)
* @returns (GitObject) the object
*/
public function getObject($name) {
list($type, $data) = $this->getRawObject($name);
$object = GitObject::create($this, $type);
assert($name == $object->getName());
return $object;
}
/**
* @brief Look up a branch.
*
* @param $branch (string) The branch to look up, defaulting to @em master.
* @returns (string) The tip of the branch (binary sha1).
*/
public function getTip($branch = 'master') {
$subpath = sprintf('refs/heads/%s', $branch);
$head = null;
while ($head === null && ($line = fgets($f)) !== false) {
if ($line[0] == '#')
continue;
if (count($parts) == 2 && $parts[1] == $subpath)
$head = pack('H40', $parts[0]);;
}
if ($head !== null)
return $head;
}
throw new \Exception
(sprintf('no such branch: %s', $branch));
}
}