commit
f17eea506a
@ -1,162 +0,0 @@ |
||||
<?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); |
||||
} |
||||
|
||||
} |
||||
@ -1,232 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
@ -1,210 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
@ -1,81 +0,0 @@ |
||||
<?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; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,304 +0,0 @@ |
||||
<?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); |
||||
} |
||||
|
||||
} |
||||
@ -1,153 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
|
||||
?> |
||||
@ -1,293 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
@ -1,38 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
@ -1,198 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
@ -1,126 +0,0 @@ |
||||
<?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'; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,24 @@ |
||||
<?php |
||||
/** |
||||
* Copyright (c) 2012 Georg Ehrke <ownclouddev at georgswebsite dot de> |
||||
* This file is licensed under the Affero General Public License version 3 or |
||||
* later. |
||||
* See the COPYING-README file. |
||||
*/ |
||||
|
||||
require_once('../../../lib/base.php'); |
||||
OC_JSON::checkLoggedIn(); |
||||
OC_Util::checkAppEnabled('contacts'); |
||||
$upload_max_filesize = OC_Helper::computerFileSize(ini_get('upload_max_filesize')); |
||||
$post_max_size = OC_Helper::computerFileSize(ini_get('post_max_size')); |
||||
$maxUploadFilesize = min($upload_max_filesize, $post_max_size); |
||||
|
||||
$freeSpace=OC_Filesystem::free_space('/'); |
||||
$freeSpace=max($freeSpace,0); |
||||
$maxUploadFilesize = min($maxUploadFilesize ,$freeSpace); |
||||
|
||||
$tmpl = new OC_Template('contacts', 'part.importaddressbook'); |
||||
$tmpl->assign('uploadMaxFilesize', $maxUploadFilesize); |
||||
$tmpl->assign('uploadMaxHumanFilesize', OC_Helper::humanFileSize($maxUploadFilesize)); |
||||
$tmpl->printpage(); |
||||
?> |
||||
@ -0,0 +1,50 @@ |
||||
<?php |
||||
/** |
||||
* ownCloud - Addressbook |
||||
* |
||||
* @author Thomas Tanghus |
||||
* @copyright 2012 Thomas Tanghus <thomas@tanghus.net> |
||||
* |
||||
* 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/>. |
||||
* |
||||
*/ |
||||
// Init owncloud |
||||
require_once('../../../lib/base.php'); |
||||
|
||||
// Check if we are a user |
||||
OC_JSON::checkLoggedIn(); |
||||
OC_JSON::checkAppEnabled('contacts'); |
||||
function bailOut($msg) { |
||||
OC_JSON::error(array('data' => array('message' => $msg))); |
||||
OC_Log::write('contacts','ajax/uploadimport.php: '.$msg, OC_Log::ERROR); |
||||
exit(); |
||||
} |
||||
function debug($msg) { |
||||
OC_Log::write('contacts','ajax/uploadimport.php: '.$msg, OC_Log::DEBUG); |
||||
} |
||||
|
||||
// If it is a Drag'n'Drop transfer it's handled here. |
||||
$fn = (isset($_SERVER['HTTP_X_FILE_NAME']) ? $_SERVER['HTTP_X_FILE_NAME'] : false); |
||||
if($fn) { |
||||
$view = OC_App::getStorage('contacts'); |
||||
$tmpfile = md5(rand()); |
||||
if($view->file_put_contents('/'.$tmpfile, file_get_contents('php://input'))) { |
||||
debug($fn.' uploaded'); |
||||
OC_JSON::success(array('data' => array('path'=>'', 'file'=>$tmpfile))); |
||||
} else { |
||||
bailOut(OC_Contacts_App::$l10n->t('Error uploading contacts to storage.')); |
||||
} |
||||
} |
||||
|
||||
?> |
||||
@ -1,12 +0,0 @@ |
||||
<?php |
||||
/** |
||||
* Copyright (c) 2011 Craig Roberts craig0990@googlemail.com |
||||
* This file is licensed under the Affero General Public License version 3 or |
||||
* later. |
||||
*/ |
||||
|
||||
|
||||
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)); |
||||
@ -1,14 +0,0 @@ |
||||
<?php |
||||
/** |
||||
* Copyright (c) 2011 Craig Roberts craig0990@googlemail.com |
||||
* This file is licensed under the Affero General Public License version 3 or |
||||
* later. |
||||
*/ |
||||
|
||||
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(); |
||||
} |
||||
@ -1,20 +0,0 @@ |
||||
<?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'); |
||||
@ -1,12 +0,0 @@ |
||||
<?xml version="1.0"?> |
||||
<info> |
||||
<id>files_versioning</id> |
||||
<name>Versioning and Backup</name> |
||||
<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> |
||||
<types> |
||||
<filesystem/> |
||||
</types> |
||||
</info> |
||||
@ -1,3 +0,0 @@ |
||||
#file_versioning_commit_chzn { |
||||
width: 15em; |
||||
} |
||||
@ -1,25 +0,0 @@ |
||||
$(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'); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,12 +0,0 @@ |
||||
<?php |
||||
|
||||
require_once('granite/git/blob.php'); |
||||
require_once('granite/git/commit.php'); |
||||
require_once('granite/git/repository.php'); |
||||
require_once('granite/git/tag.php'); |
||||
require_once('granite/git/tree.php'); |
||||
require_once('granite/git/tree/node.php'); |
||||
require_once('granite/git/object/index.php'); |
||||
require_once('granite/git/object/raw.php'); |
||||
require_once('granite/git/object/loose.php'); |
||||
require_once('granite/git/object/packed.php'); |
||||
@ -1,34 +0,0 @@ |
||||
<?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(); |
||||
@ -1,12 +0,0 @@ |
||||
<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> |
||||
@ -1,386 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
@ -1,686 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,19 @@ |
||||
<?php |
||||
|
||||
require_once('apps/files_versions/versions.php'); |
||||
|
||||
// Add an entry in the app list |
||||
OC_App::register( array( |
||||
'order' => 10, |
||||
'id' => 'files_versions', |
||||
'name' => 'Versioning' )); |
||||
|
||||
OC_APP::registerAdmin('files_versions', 'settings'); |
||||
|
||||
// Listen to write signals |
||||
OC_Hook::connect(OC_Filesystem::CLASSNAME, OC_Filesystem::signal_post_write, "OCA_Versions\Storage", "write_hook"); |
||||
|
||||
|
||||
|
||||
|
||||
?> |
||||
@ -0,0 +1,12 @@ |
||||
<?xml version="1.0"?> |
||||
<info> |
||||
<id>files_versions</id> |
||||
<name>Versions</name> |
||||
<licence>AGPL</licence> |
||||
<author>Frank Karlitschek</author> |
||||
<require>3</require> |
||||
<description>Versioning of files</description> |
||||
<types> |
||||
<filesystem/> |
||||
</types> |
||||
</info> |
||||
@ -0,0 +1,2 @@ |
||||
|
||||
|
||||
@ -0,0 +1,60 @@ |
||||
<?php |
||||
|
||||
/** |
||||
* ownCloud - History page of the Versions App |
||||
* |
||||
* @author Frank Karlitschek |
||||
* @copyright 2011 Frank Karlitschek karlitschek@kde.org |
||||
* |
||||
* 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 Lesser General Public |
||||
* License along with this library. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
require_once('../../lib/base.php'); |
||||
|
||||
OC_Util::checkLoggedIn(); |
||||
|
||||
if (isset($_GET['path'])) { |
||||
|
||||
$path = $_GET['path']; |
||||
$path = strip_tags($path); |
||||
|
||||
// roll back to old version if button clicked |
||||
if(isset($_GET['revert'])) { |
||||
\OCA_Versions\Storage::rollback($path,$_GET['revert']); |
||||
} |
||||
|
||||
// show the history only if there is something to show |
||||
if(OCA_Versions\Storage::isversioned($path)) { |
||||
|
||||
$count=5; //show the newest revisions |
||||
$versions=OCA_Versions\Storage::getversions($path,$count); |
||||
|
||||
$tmpl = new OC_Template('files_versions', 'history', 'user'); |
||||
$tmpl->assign('path', $path); |
||||
$tmpl->assign('versions', array_reverse($versions)); |
||||
$tmpl->printPage(); |
||||
}else{ |
||||
$tmpl = new OC_Template('files_versions', 'history', 'user'); |
||||
$tmpl->assign('path', $path); |
||||
$tmpl->assign('message', 'No old versions available'); |
||||
$tmpl->printPage(); |
||||
} |
||||
}else{ |
||||
$tmpl = new OC_Template('files_versions', 'history', 'user'); |
||||
$tmpl->assign('message', 'No path specified'); |
||||
$tmpl->printPage(); |
||||
} |
||||
|
||||
|
||||
?> |
||||
@ -0,0 +1,2 @@ |
||||
|
||||
|
||||
@ -0,0 +1,10 @@ |
||||
<?php |
||||
|
||||
OC_Util::checkAdminUser(); |
||||
|
||||
OC_Util::addScript( 'files_versions', 'versions' ); |
||||
|
||||
$tmpl = new OC_Template( 'files_versions', 'settings'); |
||||
|
||||
return $tmpl->fetchPage(); |
||||
?> |
||||
@ -0,0 +1,18 @@ |
||||
<?php |
||||
if(isset($_['message'])){ |
||||
|
||||
|
||||
if(isset($_['path'])) echo('<strong>File: '.$_['path']).'</strong><br>'; |
||||
echo('<strong>'.$_['message']).'</strong><br>'; |
||||
|
||||
}else{ |
||||
|
||||
echo('<strong>Versions of '.$_['path']).'</strong><br>'; |
||||
echo('<p><em>You can click on the revert button to revert to the specific verson.</em></p><br />'); |
||||
foreach ($_['versions'] as $v){ |
||||
echo(' '.OC_Util::formatDate($v).' <a href="history.php?path='.urlencode($_['path']).'&revert='.$v.'" class="button">revert</a><br /><br />'); |
||||
} |
||||
|
||||
} |
||||
|
||||
?> |
||||
@ -0,0 +1,7 @@ |
||||
<form id="external"> |
||||
<fieldset class="personalblock"> |
||||
<strong>Versions</strong><br /> |
||||
|
||||
Configuration goes here... |
||||
</fieldset> |
||||
</form> |
||||
@ -0,0 +1,216 @@ |
||||
<?php |
||||
/** |
||||
* Copyright (c) 2012 Frank Karlitschek <frank@owncloud.org> |
||||
* This file is licensed under the Affero General Public License version 3 or |
||||
* later. |
||||
* See the COPYING-README file. |
||||
*/ |
||||
|
||||
/** |
||||
* Versions |
||||
* |
||||
* A class to handle the versioning of files. |
||||
*/ |
||||
|
||||
namespace OCA_Versions; |
||||
|
||||
class Storage { |
||||
|
||||
|
||||
// config.php configuration: |
||||
// - files_versions |
||||
// - files_versionsfolder |
||||
// - files_versionsblacklist |
||||
// - files_versionsmaxfilesize |
||||
// - files_versionsinterval |
||||
// - files_versionmaxversions |
||||
// |
||||
// todo: |
||||
// - port to oc_filesystem to enable network transparency |
||||
// - check if it works well together with encryption |
||||
// - do configuration web interface |
||||
// - implement expire all function. And find a place to call it ;-) |
||||
// - add transparent compression. first test if it´s worth it. |
||||
|
||||
const DEFAULTENABLED=true; |
||||
const DEFAULTFOLDER='versions'; |
||||
const DEFAULTBLACKLIST='avi mp3 mpg mp4'; |
||||
const DEFAULTMAXFILESIZE=1048576; // 10MB |
||||
const DEFAULTMININTERVAL=300; // 5 min |
||||
const DEFAULTMAXVERSIONS=50; |
||||
|
||||
/** |
||||
* init the versioning and create the versions folder. |
||||
*/ |
||||
public static function init() { |
||||
if(\OC_Config::getValue('files_versions', Storage::DEFAULTENABLED)=='true') { |
||||
// create versions folder |
||||
$foldername=\OC_Config::getValue('datadirectory').'/'. \OC_User::getUser() .'/'.\OC_Config::getValue('files_versionsfolder', Storage::DEFAULTFOLDER); |
||||
if(!is_dir($foldername)){ |
||||
mkdir($foldername); |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* listen to write event. |
||||
*/ |
||||
public static function write_hook($params) { |
||||
if(\OC_Config::getValue('files_versions', Storage::DEFAULTENABLED)=='true') { |
||||
$path = $params[\OC_Filesystem::signal_param_path]; |
||||
if($path<>'') Storage::store($path); |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* store a new version of a file. |
||||
*/ |
||||
public static function store($filename) { |
||||
if(\OC_Config::getValue('files_versions', Storage::DEFAULTENABLED)=='true') { |
||||
$versionsfoldername=\OC_Config::getValue('datadirectory').'/'. \OC_User::getUser() .'/'.\OC_Config::getValue('files_versionsfolder', Storage::DEFAULTFOLDER); |
||||
$filesfoldername=\OC_Config::getValue('datadirectory').'/'. \OC_User::getUser() .'/files'; |
||||
Storage::init(); |
||||
|
||||
// check if filename is a directory |
||||
if(is_dir($filesfoldername.$filename)){ |
||||
return false; |
||||
} |
||||
|
||||
// check filetype blacklist |
||||
$blacklist=explode(' ',\OC_Config::getValue('files_versionsblacklist', Storage::DEFAULTBLACKLIST)); |
||||
foreach($blacklist as $bl) { |
||||
$parts=explode('.', $filename); |
||||
$ext=end($parts); |
||||
if(strtolower($ext)==$bl) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
// check filesize |
||||
if(filesize($filesfoldername.$filename)>\OC_Config::getValue('files_versionsmaxfilesize', Storage::DEFAULTMAXFILESIZE)){ |
||||
return false; |
||||
} |
||||
|
||||
|
||||
// check mininterval |
||||
$matches=glob($versionsfoldername.$filename.'.v*'); |
||||
sort($matches); |
||||
$parts=explode('.v',end($matches)); |
||||
if((end($parts)+Storage::DEFAULTMININTERVAL)>time()){ |
||||
return false; |
||||
} |
||||
|
||||
|
||||
// create all parent folders |
||||
$info=pathinfo($filename); |
||||
@mkdir($versionsfoldername.$info['dirname'],0700,true); |
||||
|
||||
|
||||
// store a new version of a file |
||||
copy($filesfoldername.$filename,$versionsfoldername.$filename.'.v'.time()); |
||||
|
||||
// expire old revisions |
||||
Storage::expire($filename); |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* rollback to an old version of a file. |
||||
*/ |
||||
public static function rollback($filename,$revision) { |
||||
if(\OC_Config::getValue('files_versions', Storage::DEFAULTENABLED)=='true') { |
||||
$versionsfoldername=\OC_Config::getValue('datadirectory').'/'. \OC_User::getUser() .'/'.\OC_Config::getValue('files_versionsfolder', Storage::DEFAULTFOLDER); |
||||
$filesfoldername=\OC_Config::getValue('datadirectory').'/'. \OC_User::getUser() .'/files'; |
||||
// rollback |
||||
@copy($versionsfoldername.$filename.'.v'.$revision,$filesfoldername.$filename); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* check if old versions of a file exist. |
||||
*/ |
||||
public static function isversioned($filename) { |
||||
if(\OC_Config::getValue('files_versions', Storage::DEFAULTENABLED)=='true') { |
||||
$versionsfoldername=\OC_Config::getValue('datadirectory').'/'. \OC_User::getUser() .'/'.\OC_Config::getValue('files_versionsfolder', Storage::DEFAULTFOLDER); |
||||
|
||||
// check for old versions |
||||
$matches=glob($versionsfoldername.$filename.'.v*'); |
||||
if(count($matches)>1){ |
||||
return true; |
||||
}else{ |
||||
return false; |
||||
} |
||||
}else{ |
||||
return(false); |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* get a list of old versions of a file. |
||||
*/ |
||||
public static function getversions($filename,$count=0) { |
||||
if(\OC_Config::getValue('files_versions', Storage::DEFAULTENABLED)=='true') { |
||||
$versionsfoldername=\OC_Config::getValue('datadirectory').'/'. \OC_User::getUser() .'/'.\OC_Config::getValue('files_versionsfolder', Storage::DEFAULTFOLDER); |
||||
$versions=array(); |
||||
|
||||
// fetch for old versions |
||||
$matches=glob($versionsfoldername.$filename.'.v*'); |
||||
sort($matches); |
||||
foreach($matches as $ma) { |
||||
$parts=explode('.v',$ma); |
||||
$versions[]=(end($parts)); |
||||
} |
||||
|
||||
// only show the newest commits |
||||
if($count<>0 and (count($versions)>$count)) { |
||||
$versions=array_slice($versions,count($versions)-$count); |
||||
} |
||||
|
||||
return($versions); |
||||
|
||||
|
||||
}else{ |
||||
return(array()); |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* expire old versions of a file. |
||||
*/ |
||||
public static function expire($filename) { |
||||
if(\OC_Config::getValue('files_versions', Storage::DEFAULTENABLED)=='true') { |
||||
|
||||
$versionsfoldername=\OC_Config::getValue('datadirectory').'/'. \OC_User::getUser() .'/'.\OC_Config::getValue('files_versionsfolder', Storage::DEFAULTFOLDER); |
||||
|
||||
// check for old versions |
||||
$matches=glob($versionsfoldername.$filename.'.v*'); |
||||
if(count($matches)>\OC_Config::getValue('files_versionmaxversions', Storage::DEFAULTMAXVERSIONS)){ |
||||
$numbertodelete=count($matches-\OC_Config::getValue('files_versionmaxversions', Storage::DEFAULTMAXVERSIONS)); |
||||
|
||||
// delete old versions of a file |
||||
$deleteitems=array_slice($matches,0,$numbertodelete); |
||||
foreach($deleteitems as $de){ |
||||
unlink($versionsfoldername.$filename.'.v'.$de); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* expire all old versions. |
||||
*/ |
||||
public static function expireall($filename) { |
||||
// todo this should go through all the versions directories and delete all the not needed files and not needed directories. |
||||
// useful to be included in a cleanup cronjob. |
||||
} |
||||
|
||||
|
||||
} |
||||
Loading…
Reference in new issue