commit 8ac96041ddc9548fb6943f89ad21951c066c3015 Author: Sven Slootweg Date: Sat May 5 08:38:25 2012 +0200 Initial commit diff --git a/lib/binary.class.php b/lib/binary.class.php new file mode 100644 index 0000000..06f360a --- /dev/null +++ b/lib/binary.class.php @@ -0,0 +1,61 @@ +. + */ + +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; + } +} + diff --git a/lib/git.class.php b/lib/git.class.php new file mode 100644 index 0000000..8828fa8 --- /dev/null +++ b/lib/git.class.php @@ -0,0 +1,438 @@ +. + */ + +require_once('binary.class.php'); +require_once('git_object.class.php'); +require_once('git_blob.class.php'); +require_once('git_commit.class.php'); +require_once('git_commit_stamp.class.php'); +require_once('git_tree.class.php'); + +/** + * @relates Git + * @brief Convert a SHA-1 hash from hexadecimal to binary representation. + * + * @param $hex (string) The hash in hexadecimal representation. + * @returns (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. + * @returns (string) The hash in hexadecimal representation. + */ +function sha1_hex($bin) +{ + return bin2hex($bin); +} + +class Git +{ + public $dir; + + 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) + { + $this->dir = realpath($dir); + if ($this->dir === FALSE || !@is_dir($this->dir)) + throw new Exception(sprintf('not a directory: %s', $dir)); + + $this->packs = array(); + $dh = opendir(sprintf('%s/objects/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); + } + } + + /** + * @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; + 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) + * @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) + { + $index = fopen(sprintf('%s/objects/pack/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'); + } + + fclose($index); + return array($pack_name, $off); + } + else + throw new Exception('unsupported pack index format'); + } + 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 + * @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 */ + $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)); + + 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) + { + 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/objects/%s/%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/objects/pack/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'); + + $r = $this->unpackObject($pack, $object_offset); + fclose($pack); + } + else + throw new Exception(sprintf('object not found: %s', sha1_hex($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); + $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. + * @returns (string) The tip of the branch (binary sha1). + */ + public function getTip($branch='master') + { + $subpath = sprintf('refs/heads/%s', $branch); + $path = sprintf('%s/%s', $this->dir, $subpath); + if (file_exists($path)) + return sha1_bin(file_get_contents($path)); + $path = sprintf('%s/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)); + } +} + diff --git a/lib/git_blob.class.php b/lib/git_blob.class.php new file mode 100644 index 0000000..da6983e --- /dev/null +++ b/lib/git_blob.class.php @@ -0,0 +1,45 @@ +. + */ + +require_once('git_object.class.php'); + +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; + } +} + diff --git a/lib/git_commit.class.php b/lib/git_commit.class.php new file mode 100644 index 0000000..0252a36 --- /dev/null +++ b/lib/git_commit.class.php @@ -0,0 +1,174 @@ +. + */ + +require_once('git_object.class.php'); +require_once('git_commit_stamp.class.php'); + +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('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. + * + * @returns (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. + * + * @returns 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); + } +} + diff --git a/lib/git_commit_stamp.class.php b/lib/git_commit_stamp.class.php new file mode 100644 index 0000000..9bc075b --- /dev/null +++ b/lib/git_commit_stamp.class.php @@ -0,0 +1,45 @@ +. + */ + +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'); + return sprintf('%s <%s> %d %+05d', $this->name, $this->email, $this->time, ($this->offset/3600)*100 + ($this->offset/60)%60); + } +} + diff --git a/lib/git_object.class.php b/lib/git_object.class.php new file mode 100644 index 0000000..c4c67c3 --- /dev/null +++ b/lib/git_object.class.php @@ -0,0 +1,154 @@ +. + */ + +class GitObject +{ + /** + * @brief (Git) The repository this object belongs to. + */ + public $repo; + protected $type; + protected $name = NULL; + + /** + * @brief Get the object's cached SHA-1 hash value. + * + * @returns (string) The hash value (binary sha1). + */ + public function getName() { return $this->name; } + + /** + * @brief Get the object's type. + * + * @returns (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). + * @returns 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)); + } + + /** + * @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. + * @returns (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. + * + * @returns 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/objects/%s/%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; + } +} + diff --git a/lib/git_tree.class.php b/lib/git_tree.class.php new file mode 100644 index 0000000..f7a0495 --- /dev/null +++ b/lib/git_tree.class.php @@ -0,0 +1,267 @@ +. + */ + +class GitTreeError extends Exception {} +class GitTreeInvalidPathError extends GitTreeError {} + +require_once('git_object.class.php'); + +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. + * @returns 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. + * + * @returns (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. + * + * @returns (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; + } +} + diff --git a/lib/glip.php b/lib/glip.php new file mode 100644 index 0000000..a414f36 --- /dev/null +++ b/lib/glip.php @@ -0,0 +1,24 @@ +. + */ + +$old_include_path = set_include_path(dirname(__FILE__)); +require_once('git.class.php'); +set_include_path($old_include_path); + diff --git a/test.php b/test.php new file mode 100644 index 0000000..197837d --- /dev/null +++ b/test.php @@ -0,0 +1,3 @@ +