]> source.dussan.org Git - nextcloud-server.git/commitdiff
Add Craigs granite library to 3rdparty and files_versioning. Still not working and...
authorFrank Karlitschek <karlitschek@kde.org>
Mon, 9 Apr 2012 20:10:29 +0000 (22:10 +0200)
committerFrank Karlitschek <karlitschek@kde.org>
Mon, 9 Apr 2012 20:10:29 +0000 (22:10 +0200)
21 files changed:
3rdparty/granite/git/blob.php [new file with mode: 0644]
3rdparty/granite/git/commit.php [new file with mode: 0644]
3rdparty/granite/git/object/index.php [new file with mode: 0644]
3rdparty/granite/git/object/loose.php [new file with mode: 0644]
3rdparty/granite/git/object/packed.php [new file with mode: 0644]
3rdparty/granite/git/object/raw.php [new file with mode: 0644]
3rdparty/granite/git/repository.php [new file with mode: 0644]
3rdparty/granite/git/tag.php [new file with mode: 0644]
3rdparty/granite/git/tree.php [new file with mode: 0644]
3rdparty/granite/git/tree/node.php [new file with mode: 0644]
apps/files_versioning/ajax/gethead.php [new file with mode: 0644]
apps/files_versioning/ajax/sethead.php [new file with mode: 0644]
apps/files_versioning/appinfo/app.php [new file with mode: 0644]
apps/files_versioning/appinfo/info.xml [new file with mode: 0644]
apps/files_versioning/css/settings.css [new file with mode: 0644]
apps/files_versioning/js/settings.js [new file with mode: 0644]
apps/files_versioning/lib_granite.php [new file with mode: 0644]
apps/files_versioning/settings.php [new file with mode: 0644]
apps/files_versioning/templates/settings.php [new file with mode: 0644]
apps/files_versioning/versionstorage.php [new file with mode: 0644]
apps/files_versioning/versionwrapper.php [new file with mode: 0644]

diff --git a/3rdparty/granite/git/blob.php b/3rdparty/granite/git/blob.php
new file mode 100644 (file)
index 0000000..781a697
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+/**
+ * Blob - provides a Git blob object
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git;
+use \Granite\Git\Object\Raw as Raw;
+use \InvalidArgumentException as InvalidArgumentException;
+use \finfo as finfo;
+/**
+ * **Granite\Git\Blob** represents the raw content of an object in a Git repository,
+ * typically a **file**. This class provides methods related to the handling of
+ * blob content, mimetypes, sizes and write support.
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Blob
+{
+
+    /**
+     * Stores the SHA-1 id of the object requested; accessed through the `sha()`
+     * method where it is recalculated based on the blob content.
+     */
+    private $sha = null;
+    /**
+     * The raw binary string of the file contents.
+     */
+    private $content = "";
+    /**
+     * The path to the repository location.
+     */
+    private $path;
+
+    /**
+     * Fetches a raw Git object and parses the result. Throws an
+     * InvalidArgumentException if the object is not of the correct type,
+     * or cannot be found.
+     *
+     * @param string $path The path to the repository root.
+     * @param string $sha  The SHA-1 id of the requested object, or `null` if
+     *                     creating a new blob object.
+     *
+     * @throws InvalidArgumentException If the SHA-1 id provided is not a blob.
+     */
+    public function __construct($path, $sha = NULL)
+    {
+        $this->path = $path;
+        if ($sha !== NULL) {
+            $this->sha = $sha;
+            $object = Raw::factory($path, $sha);
+
+            if ($object->type() !== Raw::OBJ_BLOB) {
+                throw new InvalidArgumentException(
+                    "The object $sha is not a blob, type is {$object->type()}"
+                );
+            }
+
+            $this->content = $object->content();
+            unset($object);
+       }
+    }
+
+    /**
+     * Sets or returns the raw file content, depending whether the parameter is
+     * provided.
+     *
+     * @param string $content The object content to set, or `null` if requesting the
+     *                        current content.
+     *
+     * @return string The raw binary string of the file contents.
+     */
+    public function content($content = NULL)
+    {
+        if ($content == NULL) {
+            return $this->content;
+        }
+        $this->content = $content;
+    }
+
+    /**
+     * Returns the size of the file content in bytes, equivalent to
+     * `strlen($blob->content())`.
+     *
+     * @return int The size of the object in bytes.
+     */
+    public function size()
+    {
+        return strlen($this->content);
+    }
+
+    /**
+     * Updates and returns the SHA-1 id of the object, based on it's contents.
+     *
+     * @return int The SHA-1 id of the object.
+     */
+    public function sha()
+    {
+        $sha = hash_init('sha1');
+        $header = 'blob ' . strlen($this->content) . "\0";
+        hash_update($sha, $header);
+        hash_update($sha, $this->content);
+        $this->sha = hash_final($sha);
+        return $this->sha;
+    }
+
+    /**
+     * Returns the mimetype of the object, using `finfo()` to determine the mimetype
+     * of the string.
+     *
+     * @return string The object mimetype.
+     * @see http://php.net/manual/en/function.finfo-open.php
+     */
+    public function mimetype()
+    {
+        $finfo = new finfo(FILEINFO_MIME);
+        return $finfo->buffer($this->content);
+    }
+
+    /**
+     * Encode and compress the object content, saving it to a 'loose' file.
+     *
+     * @return boolean True on success, false on failure.
+     */
+    public function write()
+    {
+        $sha = $this->sha(TRUE);
+        $path = $this->path
+            . 'objects'
+            . DIRECTORY_SEPARATOR
+            . substr($sha, 0, 2)
+            . DIRECTORY_SEPARATOR
+            . substr($sha, 2);
+        // FIXME: currently writes loose objects only
+        if (file_exists($path)) {
+            return FALSE;
+        }
+
+        if (!is_dir(dirname($path))) {
+            mkdir(dirname($path), 0777, TRUE);
+        }
+
+        $loose = fopen($path, 'wb');
+        $data = 'blob ' . strlen($this->content) . "\0" . $this->content;
+        $write = fwrite($loose, gzcompress($data));
+        fclose($loose);
+
+        return ($write !== FALSE);
+    }
+
+}
diff --git a/3rdparty/granite/git/commit.php b/3rdparty/granite/git/commit.php
new file mode 100644 (file)
index 0000000..51077e8
--- /dev/null
@@ -0,0 +1,232 @@
+<?php
+/**
+ * Commit - provides a 'commit' object
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git;
+use \Granite\Git\Object\Raw as Raw;
+use \InvalidArgumentException as InvalidArgumentException;
+
+/**
+ * Commit represents a full commit object
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Commit
+{
+
+    /**
+     * The path to the repository root
+     */
+    private $path;
+    /**
+     * The SHA-1 id of the requested commit
+     */
+    private $sha;
+    /**
+     * The size of the commit in bytes
+     */
+    private $size;
+    /**
+     * The commit message
+     */
+    private $message;
+    /**
+     * The full committer string
+     */
+    private $committer;
+    /**
+     * The full author string
+     */
+    private $author;
+    /**
+     * The SHA-1 ids of the parent commits
+     */
+    private $parents = array();
+
+    /**
+     * Fetches a raw Git object and parses the result. Throws an
+     * InvalidArgumentException if the object is not of the correct type,
+     * or cannot be found.
+     *
+     * @param string $path The path to the repository root
+     * @param string $sha  The SHA-1 id of the requested object
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct($path, $sha = NULL)
+    {
+        $this->path = $path;
+        if ($sha !== NULL) {
+            $this->sha = $sha;
+            $object = Raw::factory($path, $sha);
+            $this->size = $object->size();
+
+            if ($object->type() !== Raw::OBJ_COMMIT) {
+                throw new InvalidArgumentException(
+                    "The object $sha is not a commit, type is " . $object->type()
+                );
+            }
+
+            // Parse headers and commit message (delimited with "\n\n")
+            list($headers, $this->message) = explode("\n\n", $object->content(), 2);
+            $headers = explode("\n", $headers);
+
+            foreach ($headers as $header) {
+                list($header, $value) = explode(' ', $header, 2);
+                if ($header == 'parent') {
+                    $this->parents[] = $value;
+                } else {
+                    $this->$header = $value;
+                }
+            }
+
+            $this->tree = new Tree($this->path, $this->tree);
+        }
+    }
+
+    /**
+     * Returns the message stored in the commit
+     *
+     * @return string The commit message
+     */
+    public function message($message = NULL)
+    {
+        if ($message !== NULL) {
+            $this->message = $message;
+            return $this;
+        }
+        return $this->message;
+    }
+
+    /**
+     * Returns the commiter string
+     *
+     * @return string The committer string
+     */
+    public function committer($committer = NULL)
+    {
+        if ($committer !== NULL) {
+            $this->committer = $committer;
+            return $this;
+        }
+        return $this->committer;
+    }
+
+    /**
+     * Returns the author string
+     *
+     * @return string The author string
+     */
+    public function author($author = NULL)
+    {
+        if ($author !== NULL) {
+            $this->author = $author;
+            return $this;
+        }
+        return $this->author;
+    }
+
+    /**
+     * Returns the parents of the commit, or an empty array if none
+     *
+     * @return array The parents of the commit
+     */
+    public function parents($parents = NULL)
+    {
+        if ($parents !== NULL) {
+            $this->parents = $parents;
+            return $this;
+        }
+        return $this->parents;
+    }
+
+    /**
+     * Returns a tree object associated with the commit
+     *
+     * @return Tree
+     */
+    public function tree(Tree $tree = NULL)
+    {
+        if ($tree !== NULL) {
+            $this->tree = $tree;
+            return $this;
+        }
+        return $this->tree;
+    }
+
+    /**
+     * Returns the size of the commit in bytes (Git header + data)
+     *
+     * @return int
+     */
+    public function size()
+    {
+        return $this->size;
+    }
+
+    /**
+     * Returns the size of the commit in bytes (Git header + data)
+     *
+     * @return int
+     */
+    public function sha()
+    {
+        $this->sha = hash('sha1', $this->_raw());
+        return $this->sha;
+    }
+
+    public function write()
+    {
+        $sha = $this->sha();
+        $path = $this->path
+            . 'objects'
+            . DIRECTORY_SEPARATOR
+            . substr($sha, 0, 2)
+            . DIRECTORY_SEPARATOR
+            . substr($sha, 2);
+        // FIXME: currently writes loose objects only
+        if (file_exists($path)) {
+            return FALSE;
+        }
+
+        if (!is_dir(dirname($path))) {
+            mkdir(dirname($path), 0777, TRUE);
+        }
+
+        $loose = fopen($path, 'wb');
+        $data = $this->_raw();
+        $write = fwrite($loose, gzcompress($data));
+        fclose($loose);
+
+        return ($write !== FALSE);
+    }
+
+    public function _raw()
+    {
+        $data = 'tree ' . $this->tree->sha() . "\n";
+        foreach ($this->parents as $parent)
+        {
+            $data .= "parent $parent\n";
+        }
+        $data .= 'author ' . $this->author . "\n";
+        $data .= 'committer ' . $this->committer . "\n\n";
+        $data .= $this->message;
+
+        $data = 'commit ' . strlen($data) . "\0" . $data;
+        return $data;
+    }
+
+}
diff --git a/3rdparty/granite/git/object/index.php b/3rdparty/granite/git/object/index.php
new file mode 100644 (file)
index 0000000..239706d
--- /dev/null
@@ -0,0 +1,210 @@
+<?php
+/**
+ * Index - provides an 'index' object for packfile indexes
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git\Object;
+use \UnexpectedValueException as UnexpectedValueException;
+
+/**
+ * Index represents a packfile index
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Index
+{
+    const INDEX_MAGIC = "\377tOc";
+
+    /**
+     * The full path to the packfile index
+     */
+    private $path;
+    /**
+     * The offset at which the fanout begins, version 2+ indexes have a 2-byte header
+     */
+    private $offset = 8;
+    /**
+     * The size of the SHA-1 entries, version 1 stores 4-byte offsets alongside to
+     * total 24 bytes, version 2+ stores offsets separately
+     */
+    private $size = 20;
+    /**
+     * The version of the index file format, versions 1 and 2 are in use and
+     * currently supported
+     */
+    private $version;
+
+    /**
+     * Fetches a raw Git object and parses the result
+     *
+     * @param string $path     The path to the repository root
+     * @param string $packname The name of the packfile index to read
+     */
+    public function __construct($path, $packname)
+    {
+        $this->path = $path
+            . 'objects'
+            . DIRECTORY_SEPARATOR
+            . 'pack'
+            . DIRECTORY_SEPARATOR
+            . 'pack-' . $packname . '.idx';
+
+        $this->version = $this->_readVersion();
+        if ($this->version !== 1 && $this->version !== 2) {
+            throw new UnexpectedValueException(
+                "Unsupported index version (version $version)"
+            );
+        }
+
+        if ($this->version == 1) {
+            $this->offset = 0; // Version 1 index has no header/version
+            $this->size = 24; // Offsets + SHA-1 ids are stored together
+        }
+    }
+
+    /**
+     * Returns the offset of the object stored in the index
+     *
+     * @param string $sha The SHA-1 id of the object being requested
+     *
+     * @return int The offset of the object in the packfile
+     */
+    public function find($sha)
+    {
+        $index = fopen($this->path, 'rb');
+        $offset = false; // Offset for object in packfile not found by default
+
+        // Read the fanout to skip to the start char in the sorted SHA-1 list
+        list($start, $after) = $this->_readFanout($index, $sha);
+
+        if ($start == $after) {
+            fclose($index);
+            return false; // Object is apparently located in a 0-length section
+        }
+
+        // Seek $offset + 255 4-byte fanout entries and read 256th entry
+        fseek($index, $this->offset + 4 * 255);
+        $totalObjects = $this->_uint32($index);
+
+        // Look up the SHA-1 id of the object
+        // TODO: Binary search
+        fseek($index, $this->offset + 1024 + $this->size * $start);
+        for ($i = $start; $i < $after; $i++) {
+            if ($this->version == 1) {
+                $offset = $this->_uint32($index);
+            }
+
+            $name = fread($index, 20);
+            if ($name == pack('H40', $sha)) {
+                break; // Found it
+            }
+        }
+
+        if ($i == $after) {
+            fclose($index);
+            return false; // Scanned entire section, couldn't find it
+        }
+
+        if ($this->version == 2) {
+            // Jump to the offset location and read it
+            fseek($index, 1032 + 24 * $totalObjects + 4 * $i);
+            $offset = $this->_uint32($index);
+            if ($offset & 0x80000000) {
+                // Offset is a 64-bit integer; packfile is larger than 2GB
+                fclose($index);
+                throw new UnexpectedValueException(
+                    "Packfile larger than 2GB, currently unsupported"
+                );
+            }
+        }
+
+        fclose($index);
+        return $offset;
+    }
+
+    /**
+     * Converts a binary string into a 32-bit unsigned integer
+     *
+     * @param handle $file Binary string to convert
+     *
+     * @return int Integer value
+     */
+    private function _uint32($file)
+    {
+        $val = unpack('Nx', fread($file, 4));
+        return $val['x'];
+    }
+
+    /**
+     * Reads the fanout for a particular SHA-1 id
+     *
+     * Largely modified from Glip, with some reference to Grit - largely because I
+     * can't see how to re-implement this in PHP
+     *
+     * @param handle $file   File handle to the index file
+     * @param string $sha    The SHA-1 id to search for
+     * @param int    $offset The offset at which the fanout begins
+     *
+     * @return array Array containing integer 'start' and
+     *               'past-the-end' locations
+     */
+    private function _readFanout($file, $sha)
+    {
+        $sha = pack('H40', $sha);
+        fseek($file, $this->offset);
+        if ($sha{0} == "\00") {
+            /**
+             * First character is 0, read first fanout entry to provide
+             * 'past-the-end' location (since first fanout entry provides start
+             * point for '1'-prefixed SHA-1 ids)
+             */
+            $start = 0;
+            fseek($file, $this->offset); // Jump to start of fanout, $offset bytes in
+            $after = $this->_uint32($file);
+        } else {
+            /**
+             * Take ASCII value of first character, minus one to get the fanout
+             * position of the offset (minus one because the fanout does not
+             * contain an entry for "\00"), multiplied by four bytes per entry
+             */
+            fseek($file, $this->offset + (ord($sha{0}) - 1) * 4);
+            $start = $this->_uint32($file);
+            $after = $this->_uint32($file);
+        }
+
+        return array($start, $after);
+    }
+
+    /**
+     * Returns the version number of the index file, or 1 if there is no version
+     * information
+     *
+     * @return int
+     */
+    private function _readVersion()
+    {
+        $file = fopen($this->path, 'rb');
+        $magic = fread($file, 4);
+        $version = $this->_uint32($file);
+
+        if ($magic !== self::INDEX_MAGIC) {
+            $version = 1;
+        }
+
+        fclose($file);
+        return $version;
+    }
+
+}
diff --git a/3rdparty/granite/git/object/loose.php b/3rdparty/granite/git/object/loose.php
new file mode 100644 (file)
index 0000000..32f8948
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Loose - provides a 'loose object' object
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git\Object;
+use \UnexpectedValueException as UnexpectedValueException;
+
+/**
+ * Loose represents a loose object in the Git repository
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Loose extends Raw
+{
+
+    /**
+     * Reads an object from a loose object file based on the SHA-1 id
+     *
+     * @param string $path The path to the repository root
+     * @param string $sha  The SHA-1 id of the requested object
+     *
+     * @throws UnexpectedValueException If the type is not 'commit', 'tree',
+     * 'tag' or 'blob'
+     */
+    public function __construct($path, $sha)
+    {
+        $this->sha = $sha;
+
+        $loose_path = $path
+                      . 'objects/'
+                      . substr($sha, 0, 2)
+                      . '/'
+                      . substr($sha, 2);
+
+        if (!file_exists($loose_path)) {
+            throw new InvalidArgumentException("Cannot open loose object file for $sha");
+        }
+
+        $raw = gzuncompress(file_get_contents($loose_path));
+        $data = explode("\0", $raw, 2);
+
+        $header = $data[0];
+        $this->content = $data[1];
+
+        list($this->type, $this->size) = explode(' ', $header);
+
+        switch ($this->type) {
+            case 'commit':
+                $this->type = Raw::OBJ_COMMIT;
+            break;
+            case 'tree':
+                $this->type = Raw::OBJ_TREE;
+            break;
+            case 'blob':
+                $this->type = Raw::OBJ_BLOB;
+            break;
+            case 'tag':
+                $this->type = Raw::OBJ_TAG;
+            break;
+            default:
+                throw new UnexpectedValueException(
+                     "Unexpected type '{$this->type}'"
+                );
+            break;
+        }
+    }
+
+}
diff --git a/3rdparty/granite/git/object/packed.php b/3rdparty/granite/git/object/packed.php
new file mode 100644 (file)
index 0000000..7e8d663
--- /dev/null
@@ -0,0 +1,304 @@
+<?php
+/**
+ * Packed - provides a 'packed object' object
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git\Object;
+use \UnexpectedValueException as UnexpectedValueException;
+
+/**
+ * Packed represents a packed object in the Git repository
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Packed extends Raw
+{
+
+    /**
+     * The name of the packfile being read
+     */
+    private $_packfile;
+
+    /**
+     * Added to the object size to make a 'best-guess' effort at how much compressed
+     * data to read - should be reimplemented, ideally with streams.
+     */
+    const OBJ_PADDING = 512;
+
+    /**
+     * Reads the object data from the compressed data at $offset in $packfile
+     *
+     * @param string $packfile The path to the packfile
+     * @param int    $offset   The offset of the object data
+     */
+    public function __construct($packfile, $offset)
+    {
+        $this->_packfile = $packfile;
+
+        list($this->type, $this->size, $this->content)
+            = $this->_readPackedObject($offset);
+    }
+
+    /**
+     * Reads the object data at $this->_offset
+     *
+     * @param int $offset Offset of the object header
+     *
+     * @return array Containing the type, size and object data
+     */
+    private function _readPackedObject($offset)
+    {
+        $file = fopen($this->_packfile, 'rb');
+        fseek($file, $offset);
+        // Read the type and uncompressed size from the object header
+        list($type, $size) = $this->_readHeader($file, $offset);
+        $object_offset = ftell($file);
+
+        if ($type == self::OBJ_OFS_DELTA || $type == self::OBJ_REF_DELTA) {
+            return $this->_unpackDeltified(
+                $file, $offset, $object_offset, $type, $size
+            );
+        }
+
+        $content = gzuncompress(fread($file, $size + self::OBJ_PADDING), $size);
+
+        return array($type, $size, $content);
+    }
+
+    /**
+     * Reads a packed object header, returning the type and the size. For more
+     * detailed information, refer to the @see tag.
+     *
+     * From the @see tag: "Each byte is really 7 bits of data, with the first bit
+     * being used to say if that hunk is the last one or not before the data starts.
+     * If the first bit is a 1, you will read another byte, otherwise the data starts
+     * next. The first 3 bits in the first byte specifies the type of data..."
+     *
+     * @param handle $file   File handle to read
+     * @param int    $offset Offset of the object header
+     *
+     * @return array Containing the type and the size
+     * @see http://book.git-scm.com/7_the_packfile.html
+     */
+    private function _readHeader($file, $offset)
+    {
+        // Read the object header byte-by-byte
+        fseek($file, $offset);
+        $byte = ord(fgetc($file));
+        /**
+         * Bit-shift right by four, then ignore the first bit with a bitwise AND
+         * This gives us the object type in binary:
+         *  001    commit           self::OBJ_COMMIT
+         *  010    tree             self::OBJ_TREE
+         *  011    blob             self::OBJ_BLOB
+         *  100    tag              self::OBJ_TAG
+         *  110    offset delta     self::OBJ_OFS_DELTA
+         *  111    ref delta        self::OBJ_REF_DELTA
+         *
+         *  (000 is undefined, 101 is not currently in use)
+         * See http://book.git-scm.com/7_the_packfile.html for details
+         */
+        $type = ($byte >> 4) & 0x07;
+
+        // Read the last four bits of the first byte, used to find the size
+        $size = $byte & 0x0F;
+
+        /**
+         * $shift initially set to four, since we use the last four bits of the first
+         * byte
+         *
+         * $byte & 0x80 checks the initial bit is set to 1 (i.e. keep reading data)
+         *
+         * Finally, $shift is incremented by seven for each consecutive byte (because
+         * we ignore the initial bit)
+         */
+        for ($shift = 4; $byte & 0x80; $shift += 7) {
+            $byte = ord(fgetc($file));
+            /**
+             * The size is ANDed against 0x7F to strip the initial bit, then
+             * bitshifted by left $shift (4 or 7, depending on whether it's the
+             * initial byte) and ORed against the existing binary $size. This
+             * continuously increments the $size variable.
+             */
+            $size |= (($byte & 0x7F) << $shift);
+        }
+
+        return array($type, $size);
+    }
+
+    /**
+     * Unpacks a deltified object located at $offset in $file
+     *
+     * @param handle $file          File handle to read
+     * @param int    $offset        Offset of the object data
+     * @param int    $object_offset Offset of the object data, past the header
+     * @param int    $type          The object type, either OBJ_REF_DELTA
+                                    or OBJ_OFS_DELTA
+     * @param int    $size          The expected size of the uncompressed data
+     *
+     * @return array Containing the type, size and object data
+     */
+    private function _unpackDeltified($file, $offset, $object_offset, $type, $size)
+    {
+        fseek($file, $object_offset);
+
+        if ($type == self::OBJ_REF_DELTA) {
+
+            $base_sha = bin2hex(fread($file, 20));
+
+            $path = substr($this->_packfile, 0, strpos($this->_packfile, '.git')+5);
+            $base = Raw::factory($path, $base_sha);
+            $type = $base->type();
+            $base = $base->content();
+
+            $delta = gzuncompress(
+                fread($file, $size + self::OBJ_PADDING), $size
+            );
+
+            $content = $this->_applyDelta($base, $delta);
+
+        } elseif ($type == self::OBJ_OFS_DELTA) {
+
+            // 20 = maximum varint size according to Glip
+            $data = fread($file, $size + self::OBJ_PADDING + 20);
+
+            list($base_offset, $length) = $this->_bigEndianNumber($data);
+
+            $delta = gzuncompress(substr($data, $length), $size);
+            unset($data);
+
+            $base_offset = $offset - $base_offset;
+            list($type, $size, $base) = $this->_readPackedObject($base_offset);
+
+            $content = $this->_applyDelta($base, $delta);
+
+        } else {
+            throw new UnexpectedValueException(
+                "Unknown type $type for deltified object"
+            );
+        }
+
+        return array($type, strlen($content), $content);
+    }
+
+    /**
+     * Applies the $delta byte-sequence to $base and returns the
+     * resultant binary string.
+     *
+     * This code is modified from Grit (see below), the Ruby
+     * implementation used for GitHub under an MIT license.
+     *
+     * @param string $base  The base string for the delta to be applied to
+     * @param string $delta The delta string to apply
+     *
+     * @return string The patched binary string
+     * @see
+     * https://github.com/mojombo/grit/blob/master/lib/grit/git-ruby/internal/pack.rb
+     */
+    private function _applyDelta($base, $delta)
+    {
+        $pos = 0;
+        $src_size = $this->_varint($delta, $pos);
+        $dst_size = $this->_varint($delta, $pos);
+
+        if ($src_size !== strlen($base)) {
+            throw new UnexpectedValueException(
+                'Expected base delta size ' . strlen($base) . ' does not match the expected '
+                . "value $src_size"
+            );
+        }
+
+        $dest = "";
+        while ($pos < strlen($delta)) {
+            $byte = ord($delta{$pos++});
+
+      if ($byte & 0x80) {
+        /* copy a part of $base */
+        $offset = 0;
+        if ($byte & 0x01) $offset = ord($delta{$pos++});
+        if ($byte & 0x02) $offset |= ord($delta{$pos++}) <<  8;
+        if ($byte & 0x04) $offset |= ord($delta{$pos++}) << 16;
+        if ($byte & 0x08) $offset |= ord($delta{$pos++}) << 24;
+        $length = 0;
+        if ($byte & 0x10) $length = ord($delta{$pos++});
+        if ($byte & 0x20) $length |= ord($delta{$pos++}) <<  8;
+        if ($byte & 0x40) $length |= ord($delta{$pos++}) << 16;
+        if ($length == 0) $length = 0x10000;
+        $dest .= substr($base, $offset, $length);
+      } else {
+        /* take the next $byte bytes as they are */
+        $dest .= substr($delta, $pos, $byte);
+        $pos += $byte;
+      }
+        }
+
+        if (strlen($dest) !== $dst_size) {
+            throw new UnexpectedValueException(
+                "Deltified string expected to be $dst_size bytes, but actually "
+                . strlen($dest) . ' bytes'
+            );
+        }
+
+        return $dest;
+    }
+
+    /**
+     * Parse a Git varint (variable-length integer). Used in the `_applyDelta()`
+     * method to read the delta header.
+     *
+     * @param string $string The string to parse
+     * @param int    &$pos   The position in the string to read from
+     *
+     * @return int The integer value
+     */
+    private function _varint($string, &$pos = 0)
+    {
+        $varint = 0;
+        $bitmask = 0x80;
+        for ($i = 0; $bitmask & 0x80; $i += 7) {
+            $bitmask = ord($string{$pos++});
+            $varint |= (($bitmask & 0x7F) << $i);
+        }
+        return $varint;
+    }
+
+    /**
+     * Decodes a big endian modified base 128 number (refer to @see tag); this only
+     * appears to be used in one place, the offset delta in packfiles. The offset
+     * is the number of bytes to seek back from the start of the delta object to find
+     * the base object.
+     *
+     * This code has been implemented using the C code given in the @see tag below.
+     *
+     * @param string &$data The data to read from and decode the number
+     *
+     * @return Array Containing the base offset (number of bytes to seek back) and
+     * the length to use when reading the delta
+     * @see http://git.rsbx.net/Documents/Git_Data_Formats.txt
+     */
+    private function _bigEndianNumber(&$data)
+    {
+        $i = 0;
+        $byte = ord($data{$i++});
+        $number = $byte & 0x7F;
+        while ($byte & 0x80) {
+            $byte = ord($data{$i++});
+            $number = (($number + 1) << 7) | ($byte & 0x7F);
+        }
+
+        return array($number, $i);
+    }
+
+}
diff --git a/3rdparty/granite/git/object/raw.php b/3rdparty/granite/git/object/raw.php
new file mode 100644 (file)
index 0000000..56f363c
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Raw - provides a raw Git object
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git\Object;
+use \InvalidArgumentException as InvalidArgumentException;
+
+/**
+ * Raw represents a raw Git object, using Index to locate
+ * packed objects.
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Raw
+{
+    /**
+     * Integer values for Git objects
+     * @see http://book.git-scm.com/7_the_packfile.html
+     */
+    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;
+
+    /**
+     * The SHA-1 id of the requested object
+     */
+    protected $sha;
+    /**
+     * The type of the requested object (see class constants)
+     */
+    protected $type;
+    /**
+     * The binary string content of the requested object
+     */
+    protected $content;
+
+    /**
+     * Returns an instance of a raw Git object
+     *
+     * @param string $path The path to the repository root
+     * @param string $sha  The SHA-1 id of the requested object
+     *
+     * @return Packed|Loose
+     */
+    public static function factory($path, $sha)
+    {
+        $loose_path = $path
+                      . 'objects/'
+                      . substr($sha, 0, 2)
+                      . '/'
+                      . substr($sha, 2);
+        if (file_exists($loose_path)) {
+            return new Loose($path, $sha);
+        } else {
+            return self::_findPackedObject($path, $sha);
+        }
+    }
+
+    /**
+     * Returns the raw content of the Git object requested
+     *
+     * @return string Raw object content
+     */
+    public function content()
+    {
+        return $this->content;
+    }
+
+    /**
+     * Returns the size of the Git object
+     *
+     * @return int The size of the object in bytes
+     */
+    public function size()
+    {
+        return strlen($this->content);
+    }
+
+    /**
+     * Returns the type of the object as either commit, tag, blob or tree
+     *
+     * @return string The object type
+     */
+    public function type()
+    {
+        return $this->type;
+    }
+
+    /**
+     * Searches a packfile for the SHA id and reads the object from the packfile
+     *
+     * @param string $path The path to the repository
+     * @param string $sha  The SHA-1 id of the object being requested
+     *
+     * @throws \InvalidArgumentException
+     * @return array An array containing the type, size and object data
+     */
+    private static function _findPackedObject($path, $sha)
+    {
+        $packfiles = glob(
+            $path
+            . 'objects'
+            . DIRECTORY_SEPARATOR
+            . 'pack'
+            . DIRECTORY_SEPARATOR
+            . 'pack-*.pack'
+        );
+
+        $offset = false;
+        foreach ($packfiles as $packfile) {
+            $packname = substr(basename($packfile, '.pack'), 5);
+            $idx = new Index($path, $packname);
+            $offset = $idx->find($sha);
+
+            if ($offset !== false) {
+                break; // Found it
+            }
+        }
+
+        if ($offset == false) {
+            throw new InvalidArgumentException("Could not find packed object $sha");
+        }
+
+        $packname = $path
+            . 'objects'
+            . DIRECTORY_SEPARATOR
+            . 'pack'
+            . DIRECTORY_SEPARATOR
+            . 'pack-' . $packname . '.pack';
+        $object = new Packed($packname, $offset);
+
+        return $object;
+    }
+
+}
+
+?>
diff --git a/3rdparty/granite/git/repository.php b/3rdparty/granite/git/repository.php
new file mode 100644 (file)
index 0000000..30b58a3
--- /dev/null
@@ -0,0 +1,293 @@
+<?php
+/**
+ * Repository - provides a 'repository' object with a set of helper methods
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git;
+use \InvalidArgumentException as InvalidArgumentException;
+use \UnexpectedValueException as UnexpectedValueException;
+
+/**
+ * Repository represents a Git repository, providing a variety of methods for
+ * fetching objects from SHA-1 ids or the tip of a branch with `head()`
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Repository
+{
+
+    /**
+     * The path to the repository root
+     */
+    private $_path;
+    /**
+     * The indexed version of a commit, ready to write with `commit()`
+     */
+    private $idx_commit;
+    /**
+     * The indexed version of a tree, modified to with `add()` and `remove()`
+     */
+    private $idx_tree;
+
+    /**
+     * Sets the repository path
+     *
+     * @param string $path The path to the repository root (i.e. /repo/.git/)
+     */
+    public function __construct($path)
+    {
+        if (!is_dir($path)) {
+            throw new InvalidArgumentException("Unable to find directory $path");
+        } elseif (!is_readable($path)) {
+            throw new InvalidArgumentException("Unable to read directory $path");
+        } elseif (!is_dir($path . DIRECTORY_SEPARATOR . 'objects')
+            || !is_dir($path . DIRECTORY_SEPARATOR . 'refs')
+        ) {
+            throw new UnexpectedValueException(
+                "Invalid directory, could not find 'objects' or 'refs' in $path"
+            );
+        }
+
+        $this->_path = $path;
+        $this->idx_commit = $this->factory('commit');
+        $this->idx_tree = $this->factory('tree');
+    }
+
+    /**
+     * Returns an object from the Repository of the given type, with the given
+     * SHA-1 id, or false if it cannot be found
+     *
+     * @param string $type The type (blob, commit, tag or tree) of object being
+     * requested
+     * @param string $sha  The SHA-1 id of the object (or the name of a tag)
+     *
+     * @return Blob|Commit|Tag|Tree
+     */
+    public function factory($type, $sha = null)
+    {
+        if (!in_array($type, array('blob', 'commit', 'tag', 'tree'))) {
+            throw new InvalidArgumentException("Invalid type: $type");
+        }
+
+        if ($type == 'tag') {
+            $sha = $this->_ref('tags' . DIRECTORY_SEPARATOR . $sha);
+        }
+        $type = 'Granite\\Git\\' . ucwords($type);
+
+        return new $type($this->_path, $sha);
+    }
+
+    /**
+     * Returns a Commit object representing the HEAD commit
+     *
+     * @param string $branch The branch name to lookup, defaults to 'master'
+     *
+     * @return Commit An object representing the HEAD commit
+     */
+    public function head($branch = 'master', $value = NULL)
+    {
+        if ($value == NULL)
+            return $this->factory(
+                'commit', $this->_ref('heads' . DIRECTORY_SEPARATOR . $branch)
+            );
+
+        file_put_contents(
+            $this->_path . DIRECTORY_SEPARATOR
+            . 'refs' . DIRECTORY_SEPARATOR
+            . 'heads' . DIRECTORY_SEPARATOR . 'master',
+            $value
+        );
+    }
+
+    /**
+     * Returns a string representing the repository's location, which may or may
+     * not be initialised
+     *
+     * @return string A string representing the repository's location
+     */
+    public function path()
+    {
+        return $this->_path;
+    }
+
+    /**
+     * Returns an array of the local branches under `refs/heads`
+     *
+     * @return array
+     */
+    public function tags()
+    {
+      return $this->_refs('tags');
+    }
+
+    /**
+     * Returns an array of the local tags under `refs/tags`
+     *
+     * @return array
+     */
+    public function branches()
+    {
+      return $this->_refs('heads');
+    }
+
+    private function _refs($type)
+    {
+      $dir = $this->_path . 'refs' . DIRECTORY_SEPARATOR . $type;
+      $refs = glob($dir . DIRECTORY_SEPARATOR . '*');
+      foreach ($refs as &$ref) {
+        $ref = basename($ref);
+      }
+      return $refs;
+    }
+
+    /**
+     * Initialises a Git repository
+     *
+     * @return boolean Returns true on success, false on error
+     */
+    public static function init($path)
+    {
+      $path .= '/';
+      if (!is_dir($path)) {
+        mkdir($path);
+      } elseif (is_dir($path . 'objects')) {
+        return false;
+      }
+
+      mkdir($path . 'objects');
+      mkdir($path . 'objects/info');
+      mkdir($path . 'objects/pack');
+      mkdir($path . 'refs');
+      mkdir($path . 'refs/heads');
+      mkdir($path . 'refs/tags');
+
+      file_put_contents($path . 'HEAD', 'ref: refs/heads/master');
+
+      return true;
+    }
+
+    /**
+     * Writes the indexed commit to disk, with blobs added/removed via `add()` and
+     * `rm()`
+     *
+     * @param string $message The commit message
+     * @param string $author  The author name
+     *
+     * @return boolean True on success, or false on failure
+     */
+    public function commit($message, $author)
+    {
+        $user_string = $username . ' ' . time() . ' +0000';
+
+        try {
+            $parents = array($this->repo->head()->sha());
+        } catch (InvalidArgumentException $e) {
+            $parents = array();
+        }
+
+        $this->idx_commit->message($message);
+        $this->idx_commit->author($user_string);
+        $this->idx_commit->committer($user_string);
+        $this->idx_commit->tree($this->idx_tree);
+        $commit->parents($parents);
+
+        $this->idx_tree->write();
+        $this->idx_commit->write();
+
+        $this->repo->head('master', $this->idx_commit->sha());
+
+        $this->idx_commit = $this->factory('commit');
+        $this->idx_tree = $this->factory('tree');
+    }
+
+    /**
+     * Adds a file to the indexed commit, to be written to disk with `commit()`
+     *
+     * @param string           $filename The filename to save it under
+     * @param Granite\Git\Blob $blob     The raw blob object to add to the tree
+     */
+    public function add($filename, Granite\Git\Blob $blob)
+    {
+        $blob->write();
+        $nodes = $this->idx_tree->nodes();
+        $nodes[$filename] = new Granite\Git\Tree\Node($filename, '100644', $blob->sha());
+        $this->idx_tree->nodes($nodes);
+    }
+
+    /**
+     * Removes a file from the indexed commit
+     */
+    public function rm($filename)
+    {
+        $nodes = $this->idx_tree->nodes();
+        unset($nodes[$filename]);
+        $this->idx_tree->nodes($nodes);
+    }
+
+    /**
+     * Returns an SHA-1 id of the ref resource
+     *
+     * @param string $ref The ref name to lookup
+     *
+     * @return string An SHA-1 id of the ref resource
+     */
+    private function _ref($ref)
+    {
+        // All refs are stored in `.git/refs`
+        $file = $this->_path . 'refs' . DIRECTORY_SEPARATOR . $ref;
+
+        if (file_exists($file)) {
+            return trim(file_get_contents($file));
+        }
+
+        $sha = $this->_packedRef($ref);
+
+        if ($sha == false) {
+            throw new InvalidArgumentException("The ref $ref could not be found");
+        }
+
+        return $sha;
+    }
+
+    /**
+     * Returns an SHA-1 id of the ref resource, or false if it cannot be found
+     *
+     * @param string $ref The ref name to lookup
+     *
+     * @return string An SHA-1 id of the ref resource
+     */
+    private function _packedRef($ref)
+    {
+        $sha = false;
+        if (file_exists($this->_path . 'packed-refs')) {
+            $file = fopen($this->_path . 'packed-refs', 'r');
+
+            while (($line = fgets($file)) !== false) {
+                $info = explode(' ', $line);
+                if (count($info) == 2
+                    && trim($info[1]) == 'refs' . DIRECTORY_SEPARATOR . $ref
+                ) {
+                    $sha = trim($info[0]);
+                    break;
+                }
+            }
+
+            fclose($file);
+        }
+
+        return $sha;
+    }
+
+}
diff --git a/3rdparty/granite/git/tag.php b/3rdparty/granite/git/tag.php
new file mode 100644 (file)
index 0000000..e26ddaf
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Tag - provides a 'tag' object
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git;
+
+/**
+ * Tag represents a full tag object
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Tag
+{
+
+    public function __construct($path, $sha)
+    {
+      $this->sha = $sha;
+    }
+
+    public function sha()
+    {
+      return $this->sha;
+    }
+
+}
diff --git a/3rdparty/granite/git/tree.php b/3rdparty/granite/git/tree.php
new file mode 100644 (file)
index 0000000..2de7227
--- /dev/null
@@ -0,0 +1,198 @@
+<?php
+/**
+ * Tree - provides a 'tree' object
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git;
+use \Granite\Git\Tree\Node as Node;
+
+/**
+ * Tree represents a full tree object, with nodes pointing to other tree objects
+ * and file blobs
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Tree
+{
+
+    /**
+     * The SHA-1 id of the requested tree
+     */
+    private $sha;
+    /**
+     * The nodes/entries for the requested tree
+     */
+    private $nodes = array();
+    /**
+     * The path to the repository
+     */
+    private $path;
+
+    /**
+     * Reads a tree object by fetching the raw object
+     *
+     * @param string $path The path to the repository root
+     * @param string $sha  The SHA-1 id of the requested object
+     */
+    public function __construct($path, $sha = NULL,  $dbg = FALSE)
+    {
+        $this->path = $path;
+        if ($sha !== NULL) {
+            $object = Object\Raw::factory($path, $sha);
+            $this->sha = $sha;
+
+            if ($object->type() !== Object\Raw::OBJ_TREE) {
+                throw new \InvalidArgumentException(
+                    "The object $sha is not a tree, type is " . $object->type()
+                );
+            }
+
+            $content = $object->content();
+            file_put_contents('/tmp/tree_from_real_repo'.time(), $content);
+            $nodes = array();
+
+            for ($i = 0; $i < strlen($content); $i = $data_start + 21) {
+                $data_start = strpos($content, "\0", $i);
+                $info = substr($content, $i, $data_start-$i);
+                list($mode, $name) = explode(' ', $info, 2);
+                // Read the object SHA-1 id
+                $sha = bin2hex(substr($content, $data_start + 1, 20));
+
+                $this->nodes[$name] = new Node($name, $mode, $sha);
+            }
+        }
+    }
+
+    /**
+     * Returns an array of Tree and Granite\Git\Blob objects,
+     * representing subdirectories and files
+     *
+     * @return array Array of Tree and Granite\Git\Blob objects
+     */
+    public function nodes($nodes = null)
+    {
+        if ($nodes == null) {
+            return $this->nodes;
+        }
+        $this->nodes = $nodes;
+    }
+
+    /**
+     * Adds a blob or a tree to the list of nodes
+     *
+     * @param string $name The basename (filename) of the blob or tree
+     * @param string $mode The mode of the blob or tree (see above)
+     * @param string $sha  The SHA-1 id of the blob or tree to add
+     */
+    public function add($name, $mode, $sha)
+    {
+        $this->nodes[$name] = new Node($name, $mode, $sha);
+        uasort($this->nodes, array($this, '_sort'));
+    }
+
+    public function write()
+    {
+        $sha = $this->sha();
+        $path = $this->path
+            . 'objects'
+            . DIRECTORY_SEPARATOR
+            . substr($sha, 0, 2)
+            . DIRECTORY_SEPARATOR
+            . substr($sha, 2);
+        // FIXME: currently writes loose objects only
+        if (file_exists($path)) {
+            return FALSE;
+        }
+
+        if (!is_dir(dirname($path))) {
+            mkdir(dirname($path), 0777, TRUE);
+        }
+
+        $loose = fopen($path, 'wb');
+        $data = $this->_raw();
+        $data = 'tree ' . strlen($data) . "\0" . $data;
+        $write = fwrite($loose, gzcompress($data));
+        fclose($loose);
+
+        return ($write !== FALSE);
+    }
+
+    /**
+     * Returns the SHA-1 id of the Tree
+     *
+     * @return string SHA-1 id of the Tree
+     */
+    public function sha()
+    {
+        $data = $this->_raw();
+        $raw = 'tree ' . strlen($data) . "\0" . $data;
+        $this->sha = hash('sha1', $raw);
+        return $this->sha;
+    }
+
+    /**
+     * Generates the raw object content to be saved to disk
+     */
+    public function _raw()
+    {
+        uasort($this->nodes, array($this, '_sort'));
+        $data = '';
+        foreach ($this->nodes as $node)
+        {
+            $data .= base_convert($node->mode(), 10, 8) . ' ' . $node->name() . "\0";
+            $data .= pack('H40', $node->sha());
+        }
+        file_put_contents('/tmp/tree_made'.time(), $data);
+        return $data;
+    }
+
+    /**
+     * Sorts the node entries in a tree, general sort method adapted from original
+     * Git C code (see @see tag below).
+     *
+     * @return 1, 0 or -1 if the first entry is greater than, the same as, or less
+     *         than the second, respectively.
+     * @see https://github.com/gitster/git/blob/master/read-cache.c Around line 352,
+     *      the `base_name_compare` function
+     */
+    public function _sort(&$a, &$b)
+    {
+        $length = strlen($a->name()) < strlen($b->name()) ? strlen($a->name()) : strlen($b->name());
+
+        $cmp = strncmp($a->name(), $b->name(), $length);
+        if ($cmp) {
+            return $cmp;
+        }
+
+        $suffix1 = $a->name();
+        $suffix1 = (strlen($suffix1) > $length) ? $suffix1{$length} : FALSE;
+        $suffix2 = $b->name();
+        $suffix2 = (strlen($suffix2) > $length) ? $suffix2{$length} : FALSE;
+        if (!$suffix1 && $a->isDirectory()) {
+            $suffix1 = '/';
+        }
+        if (!$suffix2 && $b->isDirectory()) {
+            $suffix2 = '/';
+        }
+        if ($suffix1 < $suffix2) {
+            return -1;
+        } elseif ($suffix1  > $suffix2) {
+            return 1;
+        }
+
+        return 0;
+    }
+
+}
diff --git a/3rdparty/granite/git/tree/node.php b/3rdparty/granite/git/tree/node.php
new file mode 100644 (file)
index 0000000..f99eb1a
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Node - provides a tree node object for tree entries
+ *
+ * PHP version 5.3
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+
+namespace Granite\Git\Tree;
+
+/**
+ * Node represents an entry in a Tree
+ *
+ * @category Git
+ * @package  Granite
+ * @author   Craig Roberts <craig0990@googlemail.com>
+ * @license  http://www.opensource.org/licenses/mit-license.php MIT Expat License
+ * @link     http://craig0990.github.com/Granite/
+ */
+class Node
+{
+
+    /**
+     * Name of the file, directory or submodule
+     */
+    private $_name;
+    /**
+     * Mode of the object, in octal
+     */
+    private $_mode;
+    /**
+     * SHA-1 id of the tree
+     */
+    private $_sha;
+    /**
+     * Boolean value for whether the entry represents a directory
+     */
+    private $_is_dir;
+    /**
+     * Boolean value for whether the entry represents a submodule
+     */
+    private $_is_submodule;
+
+    /**
+     * Sets up a Node class with properties corresponding to the $mode parameter
+     *
+     * @param string $name The name of the object (file, directory or submodule name)
+     * @param int    $mode The mode of the object, retrieved from the repository
+     * @param string $sha  The SHA-1 id of the object
+     */
+    public function __construct($name, $mode, $sha)
+    {
+        $this->_name = $name;
+        $this->_mode = intval($mode, 8);
+        $this->_sha = $sha;
+
+        $this->_is_dir = (bool) ($this->_mode & 0x4000);
+        $this->_is_submodule = ($this->_mode == 0xE000);
+    }
+
+    /**
+     * Returns a boolean value indicating whether the node is a directory
+     *
+     * @return boolean
+     */
+    public function isDirectory()
+    {
+        return $this->_is_dir;
+    }
+
+    /**
+     * Returns a boolean value indicating whether the node is a submodule
+     *
+     * @return boolean
+     */
+    public function isSubmodule()
+    {
+        return $this->_is_submodule;
+    }
+
+    /**
+     * Returns the object name
+     *
+     * @return string
+     */
+    public function name()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Returns the object's SHA-1 id
+     *
+     * @return string
+     */
+    public function sha()
+    {
+        return $this->_sha;
+    }
+
+    /**
+     * Returns the octal value of the file mode
+     *
+     * @return int
+     */
+    public function mode()
+    {
+        return $this->_mode;
+    }
+
+    public function type()
+    {
+        if ($this->isDirectory()) {
+            return 'tree';
+        } elseif ($this->isSubmodule()) {
+            return 'commit';
+        } else {
+            return 'blob';
+        }
+    }
+}
diff --git a/apps/files_versioning/ajax/gethead.php b/apps/files_versioning/ajax/gethead.php
new file mode 100644 (file)
index 0000000..cc93b7a
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Copyright (c) 2011 Craig Roberts craig0990@googlemail.com
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ */
+require_once('../../../lib/base.php');
+
+OC_JSON::checkLoggedIn();
+// Fetch current commit (or HEAD if not yet set)
+$head = OC_Preferences::getValue(OC_User::getUser(), 'files_versioning', 'head', 'HEAD');
+OC_JSON::encodedPrint(array("head" => $head));
diff --git a/apps/files_versioning/ajax/sethead.php b/apps/files_versioning/ajax/sethead.php
new file mode 100644 (file)
index 0000000..d1b2df9
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+/**
+ * Copyright (c) 2011 Craig Roberts craig0990@googlemail.com
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ */
+require_once('../../../lib/base.php');
+OC_JSON::checkLoggedIn();
+if(isset($_POST["file_versioning_head"])){
+       OC_Preferences::setValue(OC_User::getUser(), 'files_versioning', 'head', $_POST["file_versioning_head"]);
+       OC_JSON::success();
+}else{
+       OC_JSON::error();
+}
diff --git a/apps/files_versioning/appinfo/app.php b/apps/files_versioning/appinfo/app.php
new file mode 100644 (file)
index 0000000..24a8701
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+// Include required files
+require_once('apps/files_versioning/versionstorage.php');
+require_once('apps/files_versioning/versionwrapper.php');
+// Register streamwrapper for versioned:// paths
+stream_wrapper_register('versioned', 'OC_VersionStreamWrapper');
+
+// Add an entry in the app list for versioning and backup
+OC_App::register( array(
+  'order' => 10,
+  'id' => 'files_versioning',
+  'name' => 'Versioning and Backup' ));
+
+// Include stylesheets for the settings page
+OC_Util::addStyle( 'files_versioning', 'settings' );
+OC_Util::addScript('files_versioning','settings');
+
+// Register a settings section in the Admin > Personal page
+OC_APP::registerPersonal('files_versioning','settings');
diff --git a/apps/files_versioning/appinfo/info.xml b/apps/files_versioning/appinfo/info.xml
new file mode 100644 (file)
index 0000000..d5546be
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<info>
+    <id>files_versioning</id>
+    <name>Versioning and Backup</name>
+    <version>1.0.0</version>
+    <licence>GPLv2</licence>
+    <author>Craig Roberts</author>
+    <require>3</require>
+    <description>Versions files using Git repositories, providing a simple backup facility. Currently in *beta* and explicitly without warranty of any kind.</description>
+    <default_enable/>
+       <types>
+               <filesystem/>
+       </types>
+</info>
diff --git a/apps/files_versioning/css/settings.css b/apps/files_versioning/css/settings.css
new file mode 100644 (file)
index 0000000..afe2cd5
--- /dev/null
@@ -0,0 +1,3 @@
+#file_versioning_commit_chzn {
+    width: 15em;
+}
diff --git a/apps/files_versioning/js/settings.js b/apps/files_versioning/js/settings.js
new file mode 100644 (file)
index 0000000..8dd13ba
--- /dev/null
@@ -0,0 +1,25 @@
+$(document).ready(function(){
+    $('#file_versioning_head').chosen();
+
+    $.getJSON(OC.filePath('files_versioning', 'ajax', 'gethead.php'), function(jsondata, status) {
+
+        if (jsondata.head == 'HEAD') {
+            // Most recent commit, do nothing
+        } else {
+            $("#file_versioning_head").val(jsondata.head);
+            // Trigger the chosen update call
+            // See http://harvesthq.github.com/chosen/
+            $("#file_versioning_head").trigger("liszt:updated");
+        }
+    });
+
+    $('#file_versioning_head').change(function() {
+
+        var data = $(this).serialize();
+        $.post( OC.filePath('files_versioning', 'ajax', 'sethead.php'), data, function(data){
+               if(data == 'error'){
+                       console.log('Saving new HEAD failed');
+               }
+        });
+    });
+});
diff --git a/apps/files_versioning/lib_granite.php b/apps/files_versioning/lib_granite.php
new file mode 100644 (file)
index 0000000..c69c62d
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/blob.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/commit.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/repository.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/tag.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/tree.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/tree/node.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/object/index.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/object/raw.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/object/loose.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/object/packed.php');
diff --git a/apps/files_versioning/settings.php b/apps/files_versioning/settings.php
new file mode 100644 (file)
index 0000000..94af587
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+// Get the full path to the repository folder (FIXME: hard-coded to 'Backup')
+$path = OC_Config::getValue('datadirectory', OC::$SERVERROOT.'/data')
+        . DIRECTORY_SEPARATOR
+        . OC_User::getUser()
+        . DIRECTORY_SEPARATOR
+        . 'files'
+        . DIRECTORY_SEPARATOR
+        . 'Backup'
+        . DIRECTORY_SEPARATOR
+        . '.git'
+        . DIRECTORY_SEPARATOR;
+
+$repository = new Granite\Git\Repository($path);
+
+$commits = array();
+// Fetch most recent 50 commits (FIXME - haven't tested this much)
+$commit = $repository->head();
+for ($i = 0; $i < 50; $i++) {
+    $commits[] = $commit;
+    $parents = $commit->parents();
+    if (count($parents) > 0) {
+        $parent = $parents[0];
+    } else {
+        break;
+    }
+
+    $commit = $repository->factory('commit', $parent);
+}
+
+$tmpl = new OC_Template( 'files_versioning', 'settings');
+$tmpl->assign('commits', $commits);
+return $tmpl->fetchPage();
diff --git a/apps/files_versioning/templates/settings.php b/apps/files_versioning/templates/settings.php
new file mode 100644 (file)
index 0000000..17f4cc7
--- /dev/null
@@ -0,0 +1,12 @@
+<fieldset id="status_list" class="personalblock">
+       <strong>Versioning and Backup</strong><br>
+       <p><em>Please note: Backing up large files (around 16MB+) will cause your backup history to grow very large, very quickly.</em></p>
+       <label class="bold">Backup Folder</label>
+       <select name="file_versioning_head" id="file_versioning_head">
+       <?php
+        foreach ($_['commits'] as $commit):
+            echo '<option value="' . $commit->sha() . '">' . $commit->message() . '</option>';
+        endforeach;
+       ?>
+       </select>
+</fieldset>
diff --git a/apps/files_versioning/versionstorage.php b/apps/files_versioning/versionstorage.php
new file mode 100644 (file)
index 0000000..d083e62
--- /dev/null
@@ -0,0 +1,386 @@
+<?php
+/**
+ * ownCloud file storage implementation for Git repositories
+ * @author Craig Roberts
+ * @copyright 2012 Craig Roberts craig0990@googlemail.com
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Include Granite
+require_once('lib_granite.php');
+
+// Create a top-level 'Backup' directory if it does not already exist
+$user = OC_User::getUser();
+if (OC_Filesystem::$loaded and !OC_Filesystem::is_dir('/Backup')) {
+    OC_Filesystem::mkdir('/Backup');
+    OC_Preferences::setValue(OC_User::getUser(), 'files_versioning', 'head', 'HEAD');
+}
+
+// Generate the repository path (currently using 'full' repositories, as opposed to bare ones)
+$repo_path = DIRECTORY_SEPARATOR
+    . OC_User::getUser()
+    . DIRECTORY_SEPARATOR
+    . 'files'
+    . DIRECTORY_SEPARATOR
+    . 'Backup';
+
+// Mount the 'Backup' folder using the versioned storage provider below
+OC_Filesystem::mount('OC_Filestorage_Versioned', array('repo'=>$repo_path), $repo_path . DIRECTORY_SEPARATOR);
+
+class OC_Filestorage_Versioned extends OC_Filestorage {
+
+    /**
+     * Holds an instance of Granite\Git\Repository
+     */
+    protected $repo;
+
+    /**
+     * Constructs a new OC_Filestorage_Versioned instance, expects an associative
+     * array with a `repo` key set to the path of the repository's `.git` folder
+     *
+     * @param array $parameters An array containing the key `repo` pointing to the
+     *                             repository path.
+     */
+    public function __construct($parameters) {
+        // Get the full path to the repository folder
+        $path = OC_Config::getValue('datadirectory', OC::$SERVERROOT.'/data')
+                . $parameters['repo']
+                . DIRECTORY_SEPARATOR
+                . '.git'
+                . DIRECTORY_SEPARATOR;
+
+        try {
+            // Attempt to load the repository
+            $this->repo = new Granite\Git\Repository($path);
+        } catch (InvalidArgumentException $e) {
+            // $path is not a valid Git repository, we must create one
+            Granite\Git\Repository::init($path);
+
+            // Load the newly-initialised repository
+            $this->repo = new Granite\Git\Repository($path);
+
+            /**
+             * Create an initial commit with a README file
+             * FIXME: This functionality should be transferred to the Granite library
+             */
+            $blob = new Granite\Git\Blob($this->repo->path());
+            $blob->content('Your Backup directory is now ready for use.');
+
+            // Create a new tree to hold the README file
+            $tree = $this->repo->factory('tree');
+            // Create a tree node to represent the README blob
+            $tree_node = new Granite\Git\Tree\Node('README', '100644', $blob->sha());
+            $tree->nodes(array($tree_node->name() => $tree_node));
+
+            // Create an initial commit
+            $commit = new Granite\Git\Commit($this->repo->path());
+            $user_string = OC_User::getUser() . ' ' . time() . ' +0000';
+            $commit->author($user_string);
+            $commit->committer($user_string);
+            $commit->message('Initial commit');
+            $commit->tree($tree);
+
+            // Write it all to disk
+            $blob->write();
+            $tree->write();
+            $commit->write();
+
+            // Update the HEAD for the 'master' branch
+            $this->repo->head('master', $commit->sha());
+        }
+
+        // Update the class pointer to the HEAD
+        $head = OC_Preferences::getValue(OC_User::getUser(), 'files_versioning', 'head', 'HEAD');
+
+        // Load the most recent commit if the preference is not set
+        if ($head == 'HEAD') {
+            $this->head = $this->repo->head()->sha();
+        } else {
+            $this->head = $head;
+        }
+    }
+
+    public function mkdir($path) {
+        if (mkdir("versioned:/{$this->repo->path()}$path#{$this->head}")) {
+            $this->head = $this->repo->head()->sha();
+            OC_Preferences::setValue(OC_User::getUser(), 'files_versioning', 'head', $head);
+            return true;
+        }
+
+        return false;
+    }
+
+    public function rmdir($path) {
+
+    }
+
+    /**
+     * Returns a directory handle to the requested path, or FALSE on failure
+     *
+     * @param string $path The directory path to open
+     *
+     * @return boolean|resource A directory handle, or FALSE on failure
+     */
+    public function opendir($path) {
+        return opendir("versioned:/{$this->repo->path()}$path#{$this->head}");
+    }
+
+    /**
+     * Returns TRUE if $path is a directory, or FALSE if not
+     *
+     * @param string $path The path to check
+     *
+     * @return boolean
+     */
+    public function is_dir($path) {
+        return $this->filetype($path) == 'dir';
+    }
+
+    /**
+     * Returns TRUE if $path is a file, or FALSE if not
+     *
+     * @param string $path The path to check
+     *
+     * @return boolean
+     */
+    public function is_file($path) {
+        return $this->filetype($path) == 'file';
+    }
+
+    public function stat($path)
+       {
+           return stat("versioned:/{$this->repo->path()}$path#{$this->head}");
+    }
+
+    /**
+     * Returns the strings 'dir' or 'file', depending on the type of $path
+     *
+     * @param string $path The path to check
+     *
+     * @return string Returns 'dir' if a directory, 'file' otherwise
+     */
+    public function filetype($path) {
+           if ($path == "" || $path == "/") {
+               return 'dir';
+        } else {
+            if (substr($path, -1) == '/') {
+                $path = substr($path, 0, -1);
+            }
+
+            $node = $this->tree_search($this->repo, $this->repo->factory('commit', $this->head)->tree(), $path);
+
+            // Does it exist, or is it new?
+            if ($node == null) {
+                // New file
+                return 'file';
+            } else {
+                // Is it a tree?
+                try {
+                    $this->repo->factory('tree', $node);
+                    return 'dir';
+                } catch (InvalidArgumentException $e) {
+                    // Nope, must be a blob
+                    return 'file';
+                }
+            }
+        }
+    }
+
+    public function filesize($path) {
+        return filesize("versioned:/{$this->repo->path()}$path#{$this->head}");
+    }
+
+    /**
+     * Returns a boolean value representing whether $path is readable
+     *
+     * @param string $path The path to check
+     *(
+     * @return boolean Whether or not the path is readable
+     */
+    public function is_readable($path) {
+        return true;
+    }
+
+    /**
+     * Returns a boolean value representing whether $path is writable
+     *
+     * @param string $path The path to check
+     *(
+     * @return boolean Whether or not the path is writable
+     */
+    public function is_writable($path) {
+
+        $head = OC_Preferences::getValue(OC_User::getUser(), 'files_versioning', 'head', 'HEAD');
+        if ($head !== 'HEAD' && $head !== $this->repo->head()->sha()) {
+            // Cannot modify previous commits
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns a boolean value representing whether $path exists
+     *
+     * @param string $path The path to check
+     *(
+     * @return boolean Whether or not the path exists
+     */
+    public function file_exists($path) {
+        return file_exists("versioned:/{$this->repo->path()}$path#{$this->head}");
+    }
+
+    /**
+     * Returns an integer value representing the inode change time
+     * (NOT IMPLEMENTED)
+     *
+     * @param string $path The path to check
+     *(
+     * @return int Timestamp of the last inode change
+     */
+    public function filectime($path) {
+        return -1;
+    }
+
+    /**
+     * Returns an integer value representing the file modification time
+     *
+     * @param string $path The path to check
+     *(
+     * @return int Timestamp of the last file modification
+     */
+    public function filemtime($path) {
+        return filemtime("versioned:/{$this->repo->path()}$path#{$this->head}");
+    }
+
+    public function file_get_contents($path) {
+        return file_get_contents("versioned:/{$this->repo->path()}$path#{$this->head}");
+    }
+
+    public function file_put_contents($path, $data) {
+        $success = file_put_contents("versioned:/{$this->repo->path()}$path#{$this->head}", $data);
+        if ($success !== false) {
+            // Update the HEAD in the preferences
+            OC_Preferences::setValue(OC_User::getUser(), 'files_versioning', 'head', $this->repo->head()->sha());
+            return $success;
+        }
+
+        return false;
+    }
+
+    public function unlink($path) {
+
+    }
+
+    public function rename($path1, $path2) {
+
+    }
+
+    public function copy($path1, $path2) {
+
+    }
+
+    public function fopen($path, $mode) {
+           return fopen("versioned:/{$this->repo->path()}$path#{$this->head}", $mode);
+    }
+
+    public function getMimeType($path) {
+        if ($this->filetype($path) == 'dir') {
+               return 'httpd/unix-directory';
+           } elseif ($this->filesize($path) == 0) {
+               // File's empty, returning text/plain allows opening in the web editor
+               return 'text/plain';
+           } else {
+            $finfo = new finfo(FILEINFO_MIME_TYPE);
+            /**
+             * We need to represent the repository path, the file path, and the
+             * revision, which can be simply achieved with a convention of using
+             * `.git` in the repository directory (bare or not) and the '#part'
+             * segment of a URL to specify the revision. For example
+             *
+             * versioned://var/www/myrepo.git/docs/README.md#HEAD ('bare' repo)
+             * versioned://var/www/myrepo/.git/docs/README.md#HEAD ('full' repo)
+             * versioned://var/www/myrepo/.git/docs/README.md#6a8f...8a54 ('full' repo and SHA-1 commit ID)
+             */
+            $mime = $finfo->buffer(file_get_contents("versioned:/{$this->repo->path()}$path#{$this->head}"));
+            return $mime;
+           }
+    }
+
+    /**
+     * Generates a hash based on the file contents
+     *
+     * @param string $type The hashing algorithm to use (e.g. 'md5', 'sha256', etc.)
+     * @param string $path The file to be hashed
+     * @param boolean $raw Outputs binary data if true, lowercase hex digits otherwise
+     *
+     * @return string Hashed string representing the file contents
+     */
+    public function hash($type, $path, $raw) {
+        return hash($type, file_get_contents($path), $raw);
+    }
+
+    public function free_space($path) {
+    }
+
+    public function search($query) {
+
+    }
+
+    public function touch($path, $mtime=null) {
+
+    }
+
+
+    public function getLocalFile($path) {
+    }
+
+    /**
+     * Recursively searches a tree for a path, returning FALSE if is not found
+     * or an SHA-1 id if it is found.
+     *
+     * @param string  $repo  The repository containing the tree object
+     * @param string  $tree  The tree object to search
+     * @param string  $path  The path to search for (relative to the tree)
+     * @param int     $depth The depth of the current search (for recursion)
+     *
+     * @return string|boolean The SHA-1 id of the sub-tree
+     */
+    private function tree_search($repo, $tree, $path, $depth = 0)
+    {
+        $paths = array_values(explode(DIRECTORY_SEPARATOR, $path));
+
+        $current_path = $paths[$depth];
+
+        $nodes = $tree->nodes();
+        foreach ($nodes as $node) {
+            if ($node->name() == $current_path) {
+
+                if (count($paths)-1 == $depth) {
+                    // Stop, found it
+                    return $node->sha();
+                }
+
+                // Recurse if necessary
+                if ($node->isDirectory()) {
+                    $tree = $this->repo->factory('tree', $node->sha());
+                    return $this->tree_search($repo, $tree, $path, $depth + 1);
+                }
+            }
+        }
+
+        return false;
+    }
+
+}
diff --git a/apps/files_versioning/versionwrapper.php b/apps/files_versioning/versionwrapper.php
new file mode 100644 (file)
index 0000000..b83a4fd
--- /dev/null
@@ -0,0 +1,686 @@
+<?php
+
+final class OC_VersionStreamWrapper {
+
+    /**
+     * Determines whether or not to log debug messages with `OC_Log::write()`
+     */
+    private $debug = true;
+
+    /**
+     * The name of the ".empty" files created in new directories
+     */
+    const EMPTYFILE = '.empty';
+
+    /**
+     * Stores the current position for `readdir()` etc. calls
+     */
+    private $dir_position = 0;
+
+    /**
+     * Stores the current position for `fread()`, `fseek()` etc. calls
+     */
+    private $file_position = 0;
+
+    /**
+     * Stores the current directory tree for `readdir()` etc. directory traversal
+     */
+    private $tree;
+
+    /**
+     * Stores the current file for `fread()`, `fseek()`, etc. calls
+     */
+    private $blob;
+
+    /**
+     * Stores the current commit for `fstat()`, `stat()`, etc. calls
+     */
+    private $commit;
+
+    /**
+     * Stores the current path for `fwrite()`, `file_put_contents()` etc. calls
+     */
+    private $path;
+
+    /**
+     * Close directory handle
+     */
+    public function dir_closedir() {
+        unset($this->tree);
+        return true;
+    }
+
+    /**
+     * Open directory handle
+     */
+    public function dir_opendir($path, $options) {
+        // Parse the URL into a repository directory, file path and commit ID
+        list($this->repo, $repo_file, $this->commit) = $this->parse_url($path);
+
+        if ($repo_file == '' || $repo_file == '/') {
+            // Set the tree property for the future `readdir()` etc. calls
+            $this->tree = array_values($this->commit->tree()->nodes());
+            return true;
+        } elseif ($this->tree_search($this->repo, $this->commit->tree(), $repo_file) !== false) {
+            // Something exists at this path, is it a directory though?
+            try {
+                $tree = $this->repo->factory(
+                    'tree',
+                    $this->tree_search($this->repo, $this->commit->tree(), $repo_file)
+                );
+                $this->tree = array_values($tree->nodes());
+                return true;
+            } catch (InvalidArgumentException $e) {
+                // Trying to call `opendir()` on a file, return false below
+            }
+        }
+
+        // Unable to find the directory, return false
+        return false;
+    }
+
+    /**
+     * Read entry from directory handle
+     */
+    public function dir_readdir() {
+        return isset($this->tree[$this->dir_position])
+               ? $this->tree[$this->dir_position++]->name()
+               : false;
+    }
+
+    /**
+     * Rewind directory handle
+     */
+    public function dir_rewinddir() {
+        $this->dir_position = 0;
+    }
+
+    /**
+     * Create a directory
+     * Git doesn't track empty directories, so a ".empty" file is added instead
+     */
+    public function mkdir($path, $mode, $options) {
+        // Parse the URL into a repository directory, file path and commit ID
+        list($this->repo, $repo_file, $this->commit) = $this->parse_url($path);
+
+        // Create an empty file for Git
+        $empty = new Granite\Git\Blob($this->repo->path());
+        $empty->content('');
+        $empty->write();
+
+        if (dirname($repo_file) == '.') {
+            // Adding a new directory to the root tree
+            $tree = $this->repo->head()->tree();
+        } else {
+            $tree = $this->repo->factory('tree', $this->tree_search(
+                    $this->repo, $this->repo->head()->tree(), dirname($repo_file)
+                )
+            );
+        }
+
+        // Create our new tree, with our empty file
+        $dir = $this->repo->factory('tree');
+        $nodes = array();
+        $nodes[self::EMPTYFILE] = new Granite\Git\Tree\Node(self::EMPTYFILE, '100644', $empty->sha());
+        $dir->nodes($nodes);
+        $dir->write();
+
+        // Add our new tree to its parent
+        $nodes = $tree->nodes();
+        $nodes[basename($repo_file)] = new Granite\Git\Tree\Node(basename($repo_file), '040000', $dir->sha());
+        $tree->nodes($nodes);
+        $tree->write();
+
+        // We need to recursively update each parent tree, since they are all
+        // hashed and the changes will cascade back up the chain
+
+        // So, we're currently at the bottom-most directory
+        $current_dir = dirname($repo_file);
+        $previous_tree = $tree;
+
+        if ($current_dir !== '.') {
+            do {
+                // Determine the parent directory
+                $previous_dir = $current_dir;
+                $current_dir = dirname($current_dir);
+
+                $current_tree = $current_dir !== '.'
+                                ? $this->repo->factory(
+                                      'tree', $this->tree_search(
+                                          $this->repo,
+                                          $this->repo->head()->tree(),
+                                          $current_dir
+                                      )
+                                  )
+                                : $this->repo->head()->tree();
+
+                $current_nodes = $current_tree->nodes();
+                $current_nodes[basename($previous_dir)] = new Granite\Git\Tree\Node(
+                    basename($previous_dir), '040000', $previous_tree->sha()
+                );
+                $current_tree->nodes($current_nodes);
+                $current_tree->write();
+
+                $previous_tree = $current_tree;
+            } while ($current_dir !== '.');
+
+            $tree = $previous_tree;
+        }
+
+        // Create a new commit to represent this write
+        $commit = $this->repo->factory('commit');
+        $username = OC_User::getUser();
+        $user_string = $username . ' ' . time() . ' +0000';
+        $commit->author($user_string);
+        $commit->committer($user_string);
+        $commit->message("$username created the `$repo_file` directory, " . date('d F Y H:i', time()) . '.');
+        $commit->parents(array($this->repo->head()->sha()));
+        $commit->tree($tree);
+
+        // Write it to disk
+        $commit->write();
+
+        // Update the HEAD for the 'master' branch
+        $this->repo->head('master', $commit->sha());
+
+        return true;
+    }
+
+    /**
+     * Renames a file or directory
+     */
+    public function rename($path_from, $path_to) {
+
+    }
+
+    /**
+     * Removes a directory
+     */
+    public function rmdir($path, $options) {
+
+    }
+
+    /**
+     * Retrieve the underlaying resource (NOT IMPLEMENTED)
+     */
+    public function stream_cast($cast_as) {
+        return false;
+    }
+
+    /**
+     * Close a resource
+     */
+    public function stream_close() {
+        unset($this->blob);
+        return true;
+    }
+
+    /**
+     * Tests for end-of-file on a file pointer
+     */
+    public function stream_eof() {
+        return !($this->file_position < strlen($this->blob));
+    }
+
+    /**
+     * Flushes the output (NOT IMPLEMENTED)
+     */
+    public function stream_flush() {
+        return false;
+    }
+
+    /**
+     * Advisory file locking (NOT IMPLEMENTED)
+     */
+    public function stream_lock($operation) {
+        return false;
+    }
+
+    /**
+     * Change stream options (NOT IMPLEMENTED)
+     * Called in response to `chgrp()`, `chown()`, `chmod()` and `touch()`
+     */
+    public function stream_metadata($path, $option, $var) {
+        return false;
+    }
+
+    /**
+     * Opens file or URL
+     */
+    public function stream_open($path, $mode, $options, &$opened_path) {
+        // Store the path, so we can use it later in `stream_write()` if necessary
+        $this->path = $path;
+        // Parse the URL into a repository directory, file path and commit ID
+        list($this->repo, $repo_file, $this->commit) = $this->parse_url($path);
+
+        $file = $this->tree_search($this->repo, $this->commit->tree(), $repo_file);
+        if ($file !== false) {
+            try {
+                $this->blob = $this->repo->factory('blob', $file)->content();
+                return true;
+            } catch (InvalidArgumentException $e) {
+                // Trying to open a directory, return false below
+            }
+        } elseif ($mode !== 'r') {
+            // All other modes allow opening for reading and writing, clearly
+            // some 'write' files may not exist yet...
+            return true;
+        }
+
+        // File could not be found or is not actually a file
+        return false;
+    }
+
+    /**
+     * Read from stream
+     */
+    public function stream_read($count) {
+        // Fetch the remaining set of bytes
+        $bytes = substr($this->blob, $this->file_position, $count);
+
+        // If EOF or empty string, return false
+        if ($bytes == '' || $bytes == false) {
+            return false;
+        }
+
+        // If $count does not extend past EOF, add $count to stream offset
+        if ($this->file_position + $count < strlen($this->blob)) {
+            $this->file_position += $count;
+        } else {
+            // Otherwise return all remaining bytes
+            $this->file_position = strlen($this->blob);
+        }
+
+        return $bytes;
+    }
+
+    /**
+     * Seeks to specific location in a stream
+     */
+    public function stream_seek($offset, $whence = SEEK_SET) {
+        $new_offset = false;
+
+        switch ($whence)
+        {
+            case SEEK_SET:
+                $new_offset = $offset;
+            break;
+            case SEEK_CUR:
+                $new_offset = $this->file_position += $offset;
+            break;
+            case SEEK_END:
+                $new_offset = strlen($this->blob) + $offset;
+            break;
+        }
+
+        $this->file_position = $offset;
+
+        return ($new_offset !== false);
+    }
+
+    /**
+     * Change stream options (NOT IMPLEMENTED)
+     */
+    public function stream_set_option($option, $arg1, $arg2) {
+        return false;
+    }
+
+    /**
+     * Retrieve information about a file resource (NOT IMPLEMENTED)
+     */
+    public function stream_stat() {
+
+    }
+
+    /**
+     * Retrieve the current position of a stream
+     */
+    public function stream_tell() {
+        return $this->file_position;
+    }
+
+    /**
+     * Truncate stream
+     */
+    public function stream_truncate($new_size) {
+
+    }
+
+    /**
+     * Write to stream
+     * FIXME: Could use heavy refactoring
+     */
+    public function stream_write($data) {
+        /**
+         * FIXME: This also needs to be added to Granite, in the form of `add()`,
+         * `rm()` and `commit()` calls
+         */
+
+        // Parse the URL into a repository directory, file path and commit ID
+        list($this->repo, $repo_file, $this->commit) = $this->parse_url($this->path);
+
+        $node = $this->tree_search($this->repo, $this->commit->tree(), $repo_file);
+
+        if ($node !== false) {
+            // File already exists, attempting modification of existing tree
+            try {
+                $this->repo->factory('blob', $node);
+
+                // Create our new blob with the provided $data
+                $blob = $this->repo->factory('blob');
+                $blob->content($data);
+                $blob->write();
+
+                // We know the tree exists, so strip the filename from the path and
+                // find it...
+
+                if (dirname($repo_file) == '.' || dirname($repo_file) == '') {
+                    // Root directory
+                    $tree = $this->repo->head()->tree();
+                } else {
+                    // Sub-directory
+                    $tree = $this->repo->factory('tree', $this->tree_search(
+                            $this->repo,
+                            $this->repo->head()->tree(),
+                            dirname($repo_file)
+                        )
+                    );
+                }
+
+                // Replace the old blob with our newly modified one
+                $tree_nodes = $tree->nodes();
+                $tree_nodes[basename($repo_file)] = new Granite\Git\Tree\Node(
+                    basename($repo_file), '100644', $blob->sha()
+                );
+                $tree->nodes($tree_nodes);
+                $tree->write();
+
+                // We need to recursively update each parent tree, since they are all
+                // hashed and the changes will cascade back up the chain
+
+                // So, we're currently at the bottom-most directory
+                $current_dir = dirname($repo_file);
+                $previous_tree = $tree;
+
+                if ($current_dir !== '.') {
+                    do {
+                        // Determine the parent directory
+                        $previous_dir = $current_dir;
+                        $current_dir = dirname($current_dir);
+
+                        $current_tree = $current_dir !== '.'
+                                        ? $this->repo->factory(
+                                              'tree', $this->tree_search(
+                                                  $this->repo,
+                                                  $this->repo->head()->tree(),
+                                                  $current_dir
+                                              )
+                                          )
+                                        : $this->repo->head()->tree();
+
+                        $current_nodes = $current_tree->nodes();
+                        $current_nodes[basename($previous_dir)] = new Granite\Git\Tree\Node(
+                            basename($previous_dir), '040000', $previous_tree->sha()
+                        );
+                        $current_tree->nodes($current_nodes);
+                        $current_tree->write();
+
+                        $previous_tree = $current_tree;
+                    } while ($current_dir !== '.');
+                }
+
+                // Create a new commit to represent this write
+                $commit = $this->repo->factory('commit');
+                $username = OC_User::getUser();
+                $user_string = $username . ' ' . time() . ' +0000';
+                $commit->author($user_string);
+                $commit->committer($user_string);
+                $commit->message("$username modified the `$repo_file` file, " . date('d F Y H:i', time()) . '.');
+                $commit->parents(array($this->repo->head()->sha()));
+                $commit->tree($previous_tree);
+
+                // Write it to disk
+                $commit->write();
+
+                // Update the HEAD for the 'master' branch
+                $this->repo->head('master', $commit->sha());
+
+                // If we made it this far, write was successful - update the stream
+                // position and return the number of bytes written
+                $this->file_position += strlen($data);
+                return strlen($data);
+
+            } catch (InvalidArgumentException $e) {
+                // Attempting to write to a directory or other error, fail
+                return 0;
+            }
+        } else {
+            // File does not exist, needs to be created
+
+            // Create our new blob with the provided $data
+            $blob = $this->repo->factory('blob');
+            $blob->content($data);
+            $blob->write();
+
+            if (dirname($repo_file) == '.') {
+                // Trying to add a new file to the root tree, nice and easy
+                $tree = $this->repo->head()->tree();
+                $tree_nodes = $tree->nodes();
+                $tree_nodes[basename($repo_file)] = new Granite\Git\Tree\Node(
+                    basename($repo_file), '100644', $blob->sha()
+                );
+                $tree->nodes($tree_nodes);
+                $tree->write();
+            } else {
+                // Trying to add a new file to a subdirectory, try and find it
+                $tree = $this->repo->factory('tree', $this->tree_search(
+                        $this->repo, $this->repo->head()->tree(), dirname($repo_file)
+                    )
+                );
+
+                // Add the blob to the tree
+                $nodes = $tree->nodes();
+                $nodes[basename($repo_file)] =  new Granite\Git\Tree\Node(
+                    basename($repo_file), '100644', $blob->sha()
+                );
+                $tree->nodes($nodes);
+                $tree->write();
+
+                // We need to recursively update each parent tree, since they are all
+                // hashed and the changes will cascade back up the chain
+
+                // So, we're currently at the bottom-most directory
+                $current_dir = dirname($repo_file);
+                $previous_tree = $tree;
+
+                if ($current_dir !== '.') {
+                    do {
+                        // Determine the parent directory
+                        $previous_dir = $current_dir;
+                        $current_dir = dirname($current_dir);
+
+                        $current_tree = $current_dir !== '.'
+                                        ? $this->repo->factory(
+                                              'tree', $this->tree_search(
+                                                  $this->repo,
+                                                  $this->repo->head()->tree(),
+                                                  $current_dir
+                                              )
+                                          )
+                                        : $this->repo->head()->tree();
+
+                        $current_nodes = $current_tree->nodes();
+                        $current_nodes[basename($previous_dir)] = new Granite\Git\Tree\Node(
+                            basename($previous_dir), '040000', $previous_tree->sha()
+                        );
+                        $current_tree->nodes($current_nodes);
+                        $current_tree->write();
+
+                        $previous_tree = $current_tree;
+                    } while ($current_dir !== '.');
+
+                    $tree = $previous_tree;
+                }
+            }
+
+            // Create a new commit to represent this write
+            $commit = $this->repo->factory('commit');
+            $username = OC_User::getUser();
+            $user_string = $username . ' ' . time() . ' +0000';
+            $commit->author($user_string);
+            $commit->committer($user_string);
+            $commit->message("$username created the `$repo_file` file, " . date('d F Y H:i', time()) . '.');
+            $commit->parents(array($this->repo->head()->sha()));
+            $commit->tree($tree); // Top-level tree (NOT the newly modified tree)
+
+            // Write it to disk
+            $commit->write();
+
+            // Update the HEAD for the 'master' branch
+            $this->repo->head('master', $commit->sha());
+
+            // If we made it this far, write was successful - update the stream
+            // position and return the number of bytes written
+            $this->file_position += strlen($data);
+            return strlen($data);
+        }
+
+        // Write failed
+        return 0;
+    }
+
+    /**
+     * Delete a file
+     */
+    public function unlink($path) {
+
+    }
+
+    /**
+     * Retrieve information about a file
+     */
+    public function url_stat($path, $flags) {
+        // Parse the URL into a repository directory, file path and commit ID
+        list($this->repo, $repo_file, $this->commit) = $this->parse_url($path);
+
+        $node = $this->tree_search($this->repo, $this->commit->tree(), $repo_file);
+
+        if ($node == false && $this->commit->sha() == $this->repo->head()->sha()) {
+            // A new file - no information available
+            $size = 0;
+            $mtime = -1;
+        } else {
+
+            // Is it a directory?
+            try {
+                $this->repo->factory('tree', $node);
+                $size = 4096; // FIXME
+            } catch (InvalidArgumentException $e) {
+                // Must be a file
+                $size = strlen(file_get_contents($path));
+            }
+
+            // Parse the timestamp from the commit message
+               preg_match('/[0-9]{10}+/', $this->commit->committer(), $matches);
+               $mtime = $matches[0];
+        }
+
+           $stat["dev"] = "";
+           $stat["ino"] = "";
+           $stat["mode"] = "";
+           $stat["nlink"] = "";
+           $stat["uid"] = "";
+           $stat["gid"] = "";
+           $stat["rdev"] = "";
+           $stat["size"] = $size;
+           $stat["atime"] = $mtime;
+           $stat["mtime"] = $mtime;
+           $stat["ctime"] = $mtime;
+           $stat["blksize"] = "";
+           $stat["blocks"] = "";
+
+           return $stat;
+    }
+
+    /**
+     * Debug function for development purposes
+     */
+    private function debug($message, $level = OC_Log::DEBUG)
+    {
+        if ($this->debug) {
+            OC_Log::write('files_versioning', $message, $level);
+        }
+    }
+
+    /**
+     * Parses a URL of the form:
+     * `versioned://path/to/git/repository/.git/path/to/file#SHA-1-commit-id`
+     * FIXME: Will throw an InvalidArgumentException if $path is invaid
+     *
+     * @param string $path The path to parse
+     *
+     * @return array An array containing an instance of Granite\Git\Repository,
+     *               the file path, and an instance of Granite\Git\Commit
+     * @throws InvalidArgumentException If the repository cannot be loaded
+     */
+    private function parse_url($path)
+    {
+        preg_match('/\/([A-Za-z0-9\/]+\.git\/)([A-Za-z0-9\/\.\/]*)(#([A-Fa-f0-9]+))*/', $path, $matches);
+
+        // Load up the repo
+        $repo = new \Granite\Git\Repository($matches[1]);
+        // Parse the filename (stripping any trailing slashes)
+        $repo_file = $matches[2];
+        if (substr($repo_file, -1) == '/') {
+            $repo_file = substr($repo_file, 0, -1);
+        }
+
+        // Default to HEAD if no commit is provided
+        $repo_commit = isset($matches[4])
+                       ? $matches[4]
+                       : $repo->head()->sha();
+
+        // Load the relevant commit
+        $commit = $repo->factory('commit', $repo_commit);
+
+        return array($repo, $repo_file, $commit);
+    }
+
+    /**
+     * Recursively searches a tree for a path, returning FALSE if is not found
+     * or an SHA-1 id if it is found.
+     *
+     * @param string  $repo  The repository containing the tree object
+     * @param string  $tree  The tree object to search
+     * @param string  $path  The path to search for (relative to the tree)
+     * @param int     $depth The depth of the current search (for recursion)
+     *
+     * @return string|boolean The SHA-1 id of the sub-tree
+     */
+    private function tree_search($repo, $tree, $path, $depth = 0)
+    {
+        $paths = array_values(explode(DIRECTORY_SEPARATOR, $path));
+
+        $current_path = $paths[$depth];
+
+        $nodes = $tree->nodes();
+        foreach ($nodes as $node) {
+            if ($node->name() == $current_path) {
+
+                if (count($paths)-1 == $depth) {
+                    // Stop, found it
+                    return $node->sha();
+                }
+
+                // Recurse if necessary
+                if ($node->isDirectory()) {
+                    $tree = $this->repo->factory('tree', $node->sha());
+                    return $this->tree_search($repo, $tree, $path, $depth + 1);
+                }
+            }
+        }
+
+        return false;
+    }
+
+}