.
*
* slightly modified for use with the open qoob framework.
* added is_ascii function to attempt to detect if a blob is a string or binary file.
* and archive function to zip/tar a tree of objects from a repo.
*
* released open-source under the GNU Lesser General Public License version 2
* see .
*
* @author Patrik Fimml https://github.com/patrikf/glip
* @author andrew harrison
* @copyright (cc) creative commons - attribution-shareAlike 3.0 unported
* @version 2.03
* @package qoob
* @subpackage utils
* @category version control
*/
namespace qoob\utils;
//__________________________________________________________________________________
// GLIP
/**
* @relates Git
* @brief Convert a SHA-1 hash from hexadecimal to binary representation.
*
* @param $hex (string) The hash in hexadecimal representation.
* @return (string) The hash in binary representation.
*/
function sha1_bin($hex)
{
return pack('H40', $hex);
}
/**
* @relates Git
* @brief Convert a SHA-1 hash from binary to hexadecimal representation.
*
* @param $bin (string) The hash in binary representation.
* @return (string) The hash in hexadecimal representation.
*/
function sha1_hex($bin)
{
return bin2hex($bin);
}
/**
* @relates Git
* test if blob data is ascii or binary.
* this is hacky. please suggest alternatives.
*
* @param $blob
* @return boolean
*/
function is_ascii($blob) {
$test = addcDIRECTORY_SEPARATORes(substr($blob, 0, 1024), "\\\"'\0..\37\177..\377");
$size = strlen(preg_replace('/[^\\\\]/', '', $test));
return ($size < 200) ? true : false;
}
//__________________________________________________________________________________
// git class
class Git
{
public $dir;
public $branches;
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), 500);
}
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), 500);
}
public function init($dir)
{
$this->dir = realpath($dir);
if ($this->dir === FALSE || !@is_dir($this->dir))
throw new \Exception($dir." is not a directory", 500);
$this->packs = array();
$dh = opendir(sprintf('%s'.DIRECTORY_SEPARATOR.'objects'.DIRECTORY_SEPARATOR.'pack', $this->dir));
if ($dh !== FALSE) {
while (($entry = readdir($dh)) !== FALSE)
if (preg_match('#^pack-([0-9a-fA-F]{40})\.idx$#', $entry, $m))
$this->packs[] = sha1_bin($m[1]);
closedir($dh);
}
$this->branches = array();
$ddh = opendir(sprintf('%s'.DIRECTORY_SEPARATOR.'refs'.DIRECTORY_SEPARATOR.'heads', $this->dir));
if ($ddh !== FALSE) {
while (($entry = readdir($ddh)) !== FALSE) {
if($entry != "." && $entry != "..") {
$this->branches[] = $entry;
}
}
closedir($ddh);
}
}
/**
* @brief Tries to find $object_name in the fanout table in $f at $offset.
*
* @return 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;
fseek($f, $offset);
$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)
* @return (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)
{
$index = fopen(sprintf('%s'.DIRECTORY_SEPARATOR.'objects'.DIRECTORY_SEPARATOR.'pack'.DIRECTORY_SEPARATOR.'pack-%s.idx', $this->dir, sha1_hex($pack_name)), 'rb');
flock($index, LOCK_SH);
/* 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 */
fclose($index);
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', 500);
}
fclose($index);
return array($pack_name, $off);
}
else
throw new \Exception('unsupported pack index format', 500);
}
fclose($index);
}
/* 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
* @return (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
* @return (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 */
$c = ord(fgetc($pack));
$type = ($c >> 4) & 0x07;
$size = $c & 0x0F;
for ($i = 4; $c & 0x80; $i += 7)
{
$c = ord(fgetc($pack));
$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
*/
$data = gzuncompress(fread($pack, $size+512), $size);
}
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++;
$c = ord($buf{$pos++});
$offset = ($offset << 7) + ($c & 0x7F);
}
while ($c & 0x80);
$delta = gzuncompress(substr($buf, $pos), $size);
unset($buf);
$base_offset = $object_offset - $offset;
assert($base_offset >= 0);
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
$delta = gzuncompress(fread($pack, $size+512), $size);
$data = $this->applyDelta($delta, $base);
}
else
throw new \Exception(sprintf('object of unknown type %d', $type), 500);
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)
* @return (array) an array consisting of the object type (int) and the
* binary representation of the object (string)
*/
protected function getRawObject($object_name)
{
static $cache = array();
/* FIXME allow limiting the cache to a certain size */
if (isset($cache[$object_name]))
return $cache[$object_name];
$sha1 = sha1_hex($object_name);
$path = sprintf('%s'.DIRECTORY_SEPARATOR.'objects'.DIRECTORY_SEPARATOR.'%s'.DIRECTORY_SEPARATOR.'%s', $this->dir, substr($sha1, 0, 2), substr($sha1, 2));
if (file_exists($path))
{
list($hdr, $object_data) = explode("\0", gzuncompress(file_get_contents($path)), 2);
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;
$pack = fopen(sprintf('%s'.DIRECTORY_SEPARATOR.'objects'.DIRECTORY_SEPARATOR.'pack'.DIRECTORY_SEPARATOR.'pack-%s.pack', $this->dir, sha1_hex($pack_name)), 'rb');
flock($pack, LOCK_SH);
/* check magic and version */
$magic = fread($pack, 4);
$version = Binary::fuint32($pack);
if ($magic != 'PACK' || $version != 2)
throw new \Exception('unsupported pack format', 500);
$r = $this->unpackObject($pack, $object_offset);
fclose($pack);
}
else
throw new \Exception(sprintf('object not found: %s', sha1_hex($object_name)), 500);
$cache[$object_name] = $r;
return $r;
}
/**
* @brief Fetch an object in its PHP representation.
*
* @param $name (string) name of the object (binary SHA1)
* @return (GitObject) the object
*/
public function getObject($name)
{
list($type, $data) = $this->getRawObject($name);
$object = GitObject::create($this, $type);
$object->unserialize($data);
assert($name == $object->getName());
return $object;
}
/**
* @brief Look up a branch.
*
* @param $branch (string) The branch to look up, defaulting to @em master.
* @return (string) The tip of the branch (binary sha1).
*/
public function getTip($branch='master')
{
$subpath = sprintf('refs'.DIRECTORY_SEPARATOR.'heads'.DIRECTORY_SEPARATOR.'%s', $branch);
$path = sprintf('%s'.DIRECTORY_SEPARATOR.'%s', $this->dir, $subpath);
if (file_exists($path))
return sha1_bin(file_get_contents($path));
$path = sprintf('%s'.DIRECTORY_SEPARATOR.'packed-refs', $this->dir);
if (file_exists($path))
{
$head = NULL;
$f = fopen($path, 'rb');
flock($f, LOCK_SH);
while ($head === NULL && ($line = fgets($f)) !== FALSE)
{
if ($line{0} == '#')
continue;
$parts = explode(' ', trim($line));
if (count($parts) == 2 && $parts[1] == $subpath)
$head = sha1_bin($parts[0]);
}
fclose($f);
if ($head !== NULL)
return $head;
}
throw new \Exception(sprintf('no such branch: %s', $branch), 500);
}
/**
* @brief Compress and export a Git tree
*
* @param $file (string) the export filename w/o extension
* @param $repo (string) the local directory of the repository
* @param $tree (string) the SHA1 hash of the tree to export
* @param $compression (string) the compression type 'tar' or 'zip'
* @return (string) The tip of the branch (binary sha1).
*/
function archive($file, $repo, $tree, $compression = 'zip')
{
if($compression == 'tar') {
$mime = 'x-tar-gz';
$ext = 'tar.gz';
$cmd = "git --git-dir=".escapeshellarg($repo)." archive --format=tar ".escapeshellarg($tree)." |gzip";
} else {
$mime = 'x-zip';
$ext = 'zip';
$cmd = "git --git-dir=".escapeshellarg($repo)." archive --format=zip ".escapeshellarg($tree);
}
header("Content-Type: application/$mime");
header("Content-Transfer-Encoding: binary");
header("Content-Disposition: attachment; filename=\"$file.$ext\";");
$result = 0;
passthru($cmd, $result);
return $result;
}
}
//__________________________________________________________________________________
// git object
class GitObject
{
/**
* @brief (Git) The repository this object belongs to.
*/
public $repo;
public $type;
public $name = NULL;
/**
* @brief Get the object's cached SHA-1 hash value.
*
* @return (string) The hash value (binary sha1).
*/
public function getName() { return $this->name; }
/**
* @brief Get the object's type.
*
* @return (integer) One of Git::OBJ_COMMIT, Git::OBJ_TREE or
* GIT::OBJ_BLOB.
*/
public function getType() { return $this->type; }
/**
* @brief Create a GitObject of the specified type.
*
* @param $repo (Git) The repository the object belongs to.
* @param $type (integer) Object type (one of Git::OBJ_COMMIT, Git::OBJ_TREE, Git::OBJ_BLOB).
* @return A new GitCommit, GitTree or GitBlob object respectively.
*/
static public function create($repo, $type)
{
if ($type == Git::OBJ_COMMIT)
return new GitCommit($repo);
if ($type == Git::OBJ_TREE)
return new GitTree($repo);
if ($type == Git::OBJ_BLOB)
return new GitBlob($repo);
throw new \Exception(sprintf('unhandled object type %d', $type), 500);
}
/**
* @brief Internal function to calculate the hash value of a git object of the
* current type with content $data.
*
* @param $data (string) The data to hash.
* @return (string) The hash value (binary sha1).
*/
protected function hash($data)
{
$hash = hash_init('sha1');
hash_update($hash, Git::getTypeName($this->type));
hash_update($hash, ' ');
hash_update($hash, strlen($data));
hash_update($hash, "\0");
hash_update($hash, $data);
return hash_final($hash, TRUE);
}
/**
* @brief Internal constructor for use from derived classes.
*
* Never use this function except from a derived class. Use the
* constructor of a derived class, create() or Git::getObject() instead.
*/
public function __construct($repo, $type)
{
$this->repo = $repo;
$this->type = $type;
}
/**
* @brief Populate this object with values from its string representation.
*
* Note that the types of $this and the serialized object in $data have to
* match.
*
* @param $data (string) The serialized representation of an object, as
* it would be stored by git.
*/
public function unserialize($data)
{
$this->name = $this->hash($data);
$this->_unserialize($data);
}
/**
* @brief Get the string representation of an object.
*
* @return The serialized representation of the object, as it would be
* stored by git.
*/
public function serialize()
{
return $this->_serialize();
}
/**
* @brief Update the SHA-1 name of an object.
*
* You need to call this function after making changes to attributes in
* order to have getName() return the correct hash.
*/
public function rehash()
{
$this->name = $this->hash($this->serialize());
}
/**
* @brief Write this object in its serialized form to the git repository
* given at creation time.
*/
public function write()
{
$sha1 = sha1_hex($this->name);
$path = sprintf('%s'.DIRECTORY_SEPARATOR.'objects'.DIRECTORY_SEPARATOR.'%s'.DIRECTORY_SEPARATOR.'%s', $this->repo->dir, substr($sha1, 0, 2), substr($sha1, 2));
if (file_exists($path))
return FALSE;
$dir = dirname($path);
if (!is_dir($dir))
mkdir(dirname($path), 0770);
$f = fopen($path, 'ab');
flock($f, LOCK_EX);
ftruncate($f, 0);
$data = $this->serialize();
$data = Git::getTypeName($this->type).' '.strlen($data)."\0".$data;
fwrite($f, gzcompress($data));
fclose($f);
return TRUE;
}
}
//__________________________________________________________________________________
// git commit
class GitCommit extends GitObject
{
/**
* @brief (string) The tree referenced by this commit, as binary sha1
* string.
*/
public $tree;
/**
* @brief (array of string) Parent commits of this commit, as binary sha1
* strings.
*/
public $parents;
/**
* @brief (GitCommitStamp) The author of this commit.
*/
public $author;
/**
* @brief (GitCommitStamp) The committer of this commit.
*/
public $committer;
/**
* @brief (string) Commit summary, i.e. the first line of the commit message.
*/
public $summary;
/**
* @brief (string) Everything after the first line of the commit message.
*/
public $detail;
public function __construct($repo)
{
parent::__construct($repo, Git::OBJ_COMMIT);
}
public function _unserialize($data)
{
$lines = explode("\n", $data);
unset($data);
$meta = array('parent' => array());
while (($line = array_shift($lines)) != '')
{
$parts = explode(' ', $line, 2);
if (!isset($meta[$parts[0]]))
$meta[$parts[0]] = array($parts[1]);
else
$meta[$parts[0]][] = $parts[1];
}
$this->tree = sha1_bin($meta['tree'][0]);
$this->parents = array_map('\qoob\utils\sha1_bin', $meta['parent']);
$this->author = new GitCommitStamp;
$this->author->unserialize($meta['author'][0]);
$this->committer = new GitCommitStamp;
$this->committer->unserialize($meta['committer'][0]);
$this->summary = array_shift($lines);
$this->detail = implode("\n", $lines);
$this->history = NULL;
}
public function _serialize()
{
$s = '';
$s .= sprintf("tree %s\n", sha1_hex($this->tree));
foreach ($this->parents as $parent)
$s .= sprintf("parent %s\n", sha1_hex($parent));
$s .= sprintf("author %s\n", $this->author->serialize());
$s .= sprintf("committer %s\n", $this->committer->serialize());
$s .= "\n".$this->summary."\n".$this->detail;
return $s;
}
/**
* @brief Get commit history in topological order.
*
* @return (array of GitCommit)
*/
public function getHistory()
{
if ($this->history)
return $this->history;
/* count incoming edges */
$inc = array();
$queue = array($this);
while (($commit = array_shift($queue)) !== NULL)
{
foreach ($commit->parents as $parent)
{
if (!isset($inc[$parent]))
{
$inc[$parent] = 1;
$queue[] = $this->repo->getObject($parent);
}
else
$inc[$parent]++;
}
}
$queue = array($this);
$r = array();
while (($commit = array_pop($queue)) !== NULL)
{
array_unshift($r, $commit);
foreach ($commit->parents as $parent)
{
if (--$inc[$parent] == 0)
$queue[] = $this->repo->getObject($parent);
}
}
$this->history = $r;
return $r;
}
/**
* @brief Get the tree referenced by this commit.
*
* @return The GitTree referenced by this commit.
*/
public function getTree()
{
return $this->repo->getObject($this->tree);
}
/**
* @copybrief GitTree::find()
*
* This is a convenience function calling GitTree::find() on the commit's
* tree.
*
* @copydetails GitTree::find()
*/
public function find($path)
{
return $this->getTree()->find($path);
}
static public function treeDiff($a, $b)
{
return GitTree::treeDiff($a ? $a->getTree() : NULL, $b ? $b->getTree() : NULL);
}
}
//__________________________________________________________________________________
// git commit stamp
class GitCommitStamp
{
public $name;
public $email;
public $time;
public $offset;
public function unserialize($data)
{
assert(preg_match('/^(.+?)\s+<(.+?)>\s+(\d+)\s+([+-]\d{4})$/', $data, $m));
$this->name = $m[1];
$this->email = $m[2];
$this->time = intval($m[3]);
$off = intval($m[4]);
$this->offset = ($off/100) * 3600 + ($off%100) * 60;
}
public function serialize()
{
if ($this->offset%60)
throw new \Exception('cannot serialize sub-minute timezone offset', 500);
return sprintf('%s <%s> %d %+05d', $this->name, $this->email, $this->time, ($this->offset/3600)*100 + ($this->offset/60)%60);
}
}
//__________________________________________________________________________________
// git tree
class GitTreeError extends \Exception {}
class GitTreeInvalidPathError extends GitTreeError {}
class GitTree extends GitObject
{
public $nodes = array();
public function __construct($repo)
{
parent::__construct($repo, Git::OBJ_TREE);
}
public function _unserialize($data)
{
$this->nodes = array();
$start = 0;
while ($start < strlen($data))
{
$node = new \stdClass;
$pos = strpos($data, "\0", $start);
list($node->mode, $node->name) = explode(' ', substr($data, $start, $pos-$start), 2);
$node->mode = intval($node->mode, 8);
$node->is_dir = !!($node->mode & 040000);
$node->is_submodule = ($node->mode == 57344);
$node->object = substr($data, $pos+1, 20);
$start = $pos+21;
$this->nodes[$node->name] = $node;
}
unset($data);
}
protected static function nodecmp(&$a, &$b)
{
return strcmp($a->name, $b->name);
}
public function _serialize()
{
$s = '';
/* git requires nodes to be sorted */
uasort($this->nodes, array('GitTree', 'nodecmp'));
foreach ($this->nodes as $node)
$s .= sprintf("%s %s\0%s", base_convert($node->mode, 10, 8), $node->name, $node->object);
return $s;
}
/**
* @brief Find the tree or blob at a certain path.
*
* @throws GitTreeInvalidPathError The path was found to be invalid. This
* can happen if you are trying to treat a file like a directory (i.e.
* @em foo/bar where @em foo is a file).
*
* @param $path (string) The path to look for, relative to this tree.
* @return The GitTree or GitBlob at the specified path, or NULL if none
* could be found.
*/
public function find($path)
{
if (!is_array($path))
$path = explode('/', $path);
while ($path && !$path[0])
array_shift($path);
if (!$path)
return $this->getName();
if (!isset($this->nodes[$path[0]]))
return NULL;
$cur = $this->nodes[$path[0]]->object;
array_shift($path);
while ($path && !$path[0])
array_shift($path);
if (!$path)
return $cur;
else
{
$cur = $this->repo->getObject($cur);
if (!($cur instanceof GitTree))
throw new GitTreeInvalidPathError;
return $cur->find($path);
}
}
/**
* @brief Recursively list the contents of a tree.
*
* @return (array mapping string to string) An array where the keys are
* paths relative to the current tree, and the values are SHA-1 names of
* the corresponding blobs in binary representation.
*/
public function listRecursive()
{
$r = array();
foreach ($this->nodes as $node)
{
if ($node->is_dir)
{
if ($node->is_submodule)
{
$r[$node->name. ':submodule'] = $node->object;
}
else
{
$subtree = $this->repo->getObject($node->object);
foreach ($subtree->listRecursive() as $entry => $blob)
{
$r[$node->name . '/' . $entry] = $blob;
}
}
}
else
{
$r[$node->name] = $node->object;
}
}
return $r;
}
/**
* @brief Updates a node in this tree.
*
* Missing directories in the path will be created automatically.
*
* @param $path (string) Path to the node, relative to this tree.
* @param $mode Git mode to set the node to. 0 if the node shall be
* cleared, i.e. the tree or blob shall be removed from this path.
* @param $object (string) Binary SHA-1 hash of the object that shall be
* placed at the given path.
*
* @return (array of GitObject) An array of GitObject%s that were newly
* created while updating the specified node. Those need to be written to
* the repository together with the modified tree.
*/
public function updateNode($path, $mode, $object)
{
if (!is_array($path))
$path = explode('/', $path);
$name = array_shift($path);
if (count($path) == 0)
{
/* create leaf node */
if ($mode)
{
$node = new \stdClass;
$node->mode = $mode;
$node->name = $name;
$node->object = $object;
$node->is_dir = !!($mode & 040000);
$this->nodes[$node->name] = $node;
}
else
unset($this->nodes[$name]);
return array();
}
else
{
/* descend one level */
if (isset($this->nodes[$name]))
{
$node = $this->nodes[$name];
if (!$node->is_dir)
throw new GitTreeInvalidPathError;
$subtree = clone $this->repo->getObject($node->object);
}
else
{
/* create new tree */
$subtree = new GitTree($this->repo);
$node = new \stdClass;
$node->mode = 040000;
$node->name = $name;
$node->is_dir = TRUE;
$this->nodes[$node->name] = $node;
}
$pending = $subtree->updateNode($path, $mode, $object);
$subtree->rehash();
$node->object = $subtree->getName();
$pending[] = $subtree;
return $pending;
}
}
const TREEDIFF_A = 0x01;
const TREEDIFF_B = 0x02;
const TREEDIFF_REMOVED = self::TREEDIFF_A;
const TREEDIFF_ADDED = self::TREEDIFF_B;
const TREEDIFF_CHANGED = 0x03;
static public function treeDiff($a_tree, $b_tree)
{
$a_blobs = $a_tree ? $a_tree->listRecursive() : array();
$b_blobs = $b_tree ? $b_tree->listRecursive() : array();
$a_files = array_keys($a_blobs);
$b_files = array_keys($b_blobs);
$changes = array();
sort($a_files);
sort($b_files);
$a = $b = 0;
while ($a < count($a_files) || $b < count($b_files))
{
if ($a < count($a_files) && $b < count($b_files))
$cmp = strcmp($a_files[$a], $b_files[$b]);
else
$cmp = 0;
if ($b >= count($b_files) || $cmp < 0)
{
$changes[$a_files[$a]] = self::TREEDIFF_REMOVED;
$a++;
}
else if ($a >= count($a_files) || $cmp > 0)
{
$changes[$b_files[$b]] = self::TREEDIFF_ADDED;
$b++;
}
else
{
if ($a_blobs[$a_files[$a]] != $b_blobs[$b_files[$b]])
$changes[$a_files[$a]] = self::TREEDIFF_CHANGED;
$a++;
$b++;
}
}
return $changes;
}
}
//__________________________________________________________________________________
// git blob
class GitBlob extends GitObject
{
/**
* @brief The data contained in this blob.
*/
public $data = NULL;
public function __construct($repo)
{
parent::__construct($repo, Git::OBJ_BLOB);
}
public function _unserialize($data)
{
$this->data = $data;
}
public function _serialize()
{
return $this->data;
}
}
//__________________________________________________________________________________
// binary class
final class Binary
{
static public function uint16($str, $pos=0)
{
return ord($str{$pos+0}) << 8 | ord($str{$pos+1});
}
static public function uint32($str, $pos=0)
{
$a = unpack('Nx', substr($str, $pos, 4));
return $a['x'];
}
static public function nuint32($n, $str, $pos=0)
{
$r = array();
for ($i = 0; $i < $n; $i++, $pos += 4)
$r[] = Binary::uint32($str, $pos);
return $r;
}
static public function fuint32($f) { return Binary::uint32(fread($f, 4)); }
static public function nfuint32($n, $f) { return Binary::nuint32($n, fread($f, 4*$n)); }
static public function git_varint($str, &$pos=0)
{
$r = 0;
$c = 0x80;
for ($i = 0; $c & 0x80; $i += 7)
{
$c = ord($str{$pos++});
$r |= (($c & 0x7F) << $i);
}
return $r;
}
}
?>