Move git objects with load strategies into their own directories
Move git objects with load strategies into their own directories

--- a/include/AutoLoader.class.php
+++ b/include/AutoLoader.class.php
@@ -55,6 +55,14 @@
 			$path = 'git/taglist/';
 		} else if (strncmp($classname, 'HeadList', 8) === 0) {
 			$path = 'git/headlist/';
+		} else if (($classname == 'Blob') || (strncmp($classname, 'BlobLoad', 8) === 0)) {
+			$path = 'git/blob/';
+		} else if (($classname == 'Commit') || (strncmp($classname, 'CommitLoad', 10) === 0)) {
+			$path = 'git/commit/';
+		} else if (($classname == 'Tag') || (strncmp($classname, 'TagLoad', 7) === 0)) {
+			$path = 'git/tag/';
+		} else if (($classname == 'Tree') || (strncmp($classname, 'TreeLoad', 8) === 0)) {
+			$path = 'git/tree/';
 		} else if (strpos($classname, 'Cache') !== false) {
 			$path = 'cache/';
 		} else if (in_array($classname, array(

--- a/include/git/Blob.class.php
+++ /dev/null
@@ -1,297 +1,1 @@
-<?php
-/**
- * Represents a single blob
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2010 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_Blob extends GitPHP_FilesystemObject implements GitPHP_Observable_Interface, GitPHP_Cacheable_Interface
-{
 
-	/**
-	 * The blob data
-	 *
-	 * @var string
-	 */
-	protected $data;
-
-	/**
-	 * Whether data has been read
-	 *
-	 * @var boolean
-	 */
-	protected $dataRead = false;
-
-	/**
-	 * The blob size
-	 *
-	 * @var int
-	 */
-	protected $size = null;
-
-	/**
-	 * Whether data has been encoded for serialization
-	 *
-	 * @var boolean
-	 */
-	protected $dataEncoded = false;
-
-	/**
-	 * Observers
-	 *
-	 * @var array
-	 */
-	protected $observers = array();
-
-	/**
-	 * Data load strategy
-	 *
-	 * @var GitPHP_BlobLoadStrategy_Interface
-	 */
-	protected $strategy;
-
-	/**
-	 * Instantiates object
-	 *
-	 * @param GitPHP_Project $project the project
-	 * @param string $hash object hash
-	 * @param GitPHP_BlobLoadStrategy_Interface $strategy load strategy
-	 */
-	public function __construct($project, $hash, GitPHP_BlobLoadStrategy_Interface $strategy)
-	{
-		parent::__construct($project, $hash);
-
-		if (!$strategy)
-			throw new Exception('Blob load strategy is required');
-
-		$this->SetStrategy($strategy);
-	}
-
-	/**
-	 * Gets the blob data
-	 *
-	 * @param boolean $explode true to explode data into an array of lines
-	 * @return string|string[] blob data
-	 */
-	public function GetData($explode = false)
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		if ($this->dataEncoded)
-			$this->DecodeData();
-
-		if ($explode)
-			return explode("\n", $this->data);
-		else
-			return $this->data;
-	}
-
-	/**
-	 * Set the load strategy
-	 *
-	 * @param GitPHP_BlobLoadStrategy_Interface $strategy load strategy
-	 */
-	public function SetStrategy(GitPHP_BlobLoadStrategy_Interface $strategy)
-	{
-		if (!$strategy)
-			return;
-
-		$this->strategy = $strategy;
-	}
-
-	/**
-	 * Reads the blob data
-	 */
-	private function ReadData()
-	{
-		$this->dataRead = true;
-
-		$this->data = $this->strategy->Load($this);
-
-		$this->dataEncoded = false;
-
-		foreach ($this->observers as $observer) {
-			$observer->ObjectChanged($this, GitPHP_Observer_Interface::CacheableDataChange);
-		}
-	}
-
-	/**
-	 * Gets a file type from its octal mode
-	 *
-	 * @param string $octMode octal mode
-	 * @param boolean $local true if caller wants localized type
-	 * @return string file type
-	 */
-	public static function FileType($octMode, $local = false)
-	{
-		$mode = octdec($octMode);
-		if (($mode & 0x4000) == 0x4000) {
-			if ($local) {
-				return __('directory');
-			} else {
-				return 'directory';
-			}
-		} else if (($mode & 0xA000) == 0xA000) {
-			if ($local) {
-				return __('symlink');
-			} else {
-				return 'symlink';
-			}
-		} else if (($mode & 0x8000) == 0x8000) {
-			if ($local) {
-				return __('file');
-			} else {
-				return 'file';
-			}
-		}
-
-		if ($local) {
-			return __('unknown');
-		} else {
-			return 'unknown';
-		}
-	}
-
-	/**
-	 * Gets the blob size
-	 *
-	 * @return integer size
-	 */
-	public function GetSize()
-	{
-		if ($this->size !== null) {
-			return $this->size;
-		}
-
-		return strlen($this->GetData());
-	}
-
-	/**
-	 * Sets the blob size
-	 *
-	 * @param integer $size size
-	 */
-	public function SetSize($size)
-	{
-		$this->size = $size;
-	}
-
-	/**
-	 * Tests if this blob is a binary file
-	 *
-	 * @return boolean true if binary file
-	 */
-	public function IsBinary()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		$data = $this->GetData();
-		if (strlen($data) > 8000)
-			$data = substr($data, 0, 8000);
-
-		return strpos($data, chr(0)) !== false;
-	}
-
-	/**
-	 * Encodes data so it can be serialized safely
-	 */
-	private function EncodeData()
-	{
-		if ($this->dataEncoded)
-			return;
-
-		$this->data = base64_encode($this->data);
-
-		$this->dataEncoded = true;
-	}
-
-	/**
-	 * Decodes data after unserialization
-	 */
-	private function DecodeData()
-	{
-		if (!$this->dataEncoded)
-			return;
-
-		$this->data = base64_decode($this->data);
-
-		$this->dataEncoded = false;
-	}
-
-	/**
-	 * Add a new observer
-	 *
-	 * @param GitPHP_Observer_Interface $observer observer
-	 */
-	public function AddObserver($observer)
-	{
-		if (!$observer)
-			return;
-
-		if (array_search($observer, $this->observers) !== false)
-			return;
-
-		$this->observers[] = $observer;
-	}
-
-	/**
-	 * Remove an observer
-	 *
-	 * @param GitPHP_Observer_Interface $observer observer
-	 */
-	public function RemoveObserver($observer)
-	{
-		if (!$observer)
-			return;
-
-		$key = array_search($observer, $this->observers);
-
-		if ($key === false)
-			return;
-
-		unset($this->observers[$key]);
-	}
-
-	/**
-	 * Called to prepare the object for serialization
-	 *
-	 * @return string[] list of properties to serialize
-	 */
-	public function __sleep()
-	{
-		if (!$this->dataEncoded)
-			$this->EncodeData();
-
-		$properties = array('data', 'dataRead', 'dataEncoded');
-
-		return array_merge($properties, parent::__sleep());
-	}
-
-	/**
-	 * Gets the cache key to use for this object
-	 *
-	 * @return string cache key
-	 */
-	public function GetCacheKey()
-	{
-		return GitPHP_Blob::CacheKey($this->project->GetProject(), $this->hash);
-	}
-
-	/**
-	 * Generates a blob cache key
-	 *
-	 * @param string $proj project
-	 * @param string $hash hash
-	 * @return string cache key
-	 */
-	public static function CacheKey($proj, $hash)
-	{
-		return 'project|' . $proj . '|blob|' . $hash;
-	}
-
-}
-

--- a/include/git/BlobLoadStrategy.interface.php
+++ /dev/null
@@ -1,20 +1,1 @@
-<?php
-/**
- * Interface for blob data load strategies
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-interface GitPHP_BlobLoadStrategy_Interface
-{
-	/**
-	 * Gets the data for a blob
-	 *
-	 * @param GitPHP_Blob $blob blob
-	 * @return string blob data
-	 */
-	public function Load($blob);
-}
 

--- a/include/git/BlobLoad_Git.class.php
+++ /dev/null
@@ -1,50 +1,1 @@
-<?php
-/**
- * Blob load strategy using git exe
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_BlobLoad_Git implements GitPHP_BlobLoadStrategy_Interface
-{
-	/**
-	 * Executable
-	 *
-	 * @var GitPHP_GitExe
-	 */
-	protected $exe;
 
-	/**
-	 * Constructor
-	 *
-	 * @param GitPHP_GitExe $exe executable
-	 */
-	public function __construct($exe)
-	{
-		if (!$exe)
-			throw new Exception('Git exe is required');
-
-		$this->exe = $exe;
-	}
-
-	/**
-	 * Gets the data for a blob
-	 *
-	 * @param GitPHP_Blob $blob blob
-	 * @return string blob data
-	 */
-	public function Load($blob)
-	{
-		if (!$blob)
-			return;
-
-		$args = array();
-		$args[] = 'blob';
-		$args[] = $blob->GetHash();
-
-		return $this->exe->Execute($blob->GetProject()->GetPath(), GIT_CAT_FILE, $args);
-	}
-}
-

--- a/include/git/BlobLoad_Raw.class.php
+++ /dev/null
@@ -1,46 +1,1 @@
-<?php
-/**
- * Blob load strategy using raw git objects
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_BlobLoad_Raw implements GitPHP_BlobLoadStrategy_Interface
-{
-	/**
-	 * Object loader
-	 *
-	 * @var GitPHP_GitObjectLoader
-	 */
-	protected $objectLoader;
 
-	/**
-	 * Constructor
-	 *
-	 * @param GitPHP_GitObjectLoader $objectLoader object loader
-	 */
-	public function __construct($objectLoader)
-	{
-		if (!$objectLoader)
-			throw new Exception('Git object loader is required');
-
-		$this->objectLoader = $objectLoader;
-	}
-
-	/**
-	 * Gets the data for a blob
-	 *
-	 * @param GitPHP_Blob $blob blob
-	 * @return string blob data
-	 */
-	public function Load($blob)
-	{
-		if (!$blob)
-			return;
-
-		return $this->objectLoader->GetObject($blob->GetHash());
-	}
-}
-

--- a/include/git/Commit.class.php
+++ /dev/null
@@ -1,653 +1,1 @@
-<?php
-/**
- * Represents a single commit
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2010 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_Commit extends GitPHP_GitObject implements GitPHP_Observable_Interface, GitPHP_Cacheable_Interface
-{
 
-	/**
-	 * Whether data for this commit has been read
-	 *
-	 * @var boolean
-	 */
-	protected $dataRead = false;
-
-	/**
-	 * Array of parent commits
-	 *
-	 * @var string[]
-	 */
-	protected $parents = array();
-
-	/**
-	 * Tree hash for this commit
-	 *
-	 * @var string
-	 */
-	protected $tree;
-
-	/**
-	 * Author for this commit
-	 *
-	 * @var string
-	 */
-	protected $author;
-
-	/**
-	 * Author's epoch
-	 *
-	 * @var string
-	 */
-	protected $authorEpoch;
-
-	/**
-	 * Author's timezone
-	 *
-	 * @var string
-	 */
-	protected $authorTimezone;
-
-	/**
-	 * Committer for this commit
-	 *
-	 * @var string
-	 */
-	protected $committer;
-
-	/**
-	 * Committer's epoch
-	 *
-	 * @var string
-	 */
-	protected $committerEpoch;
-
-	/**
-	 * Committer's timezone
-	 *
-	 * @var string
-	 */
-	protected $committerTimezone;
-
-	/**
-	 * The commit title
-	 *
-	 * @var string
-	 */
-	protected $title;
-
-	/**
-	 * The commit comment
-	 *
-	 * @var string
-	 */
-	protected $comment = array();
-
-	/**
-	 * Whether tree filenames have been read
-	 *
-	 * @var boolean
-	 */
-	protected $readTree = false;
-
-	/**
-	 * The tag containing the changes in this commit
-	 *
-	 * @var string
-	 */
-	protected $containingTag = null;
-
-	/**
-	 * Whether the containing tag has been looked up
-	 *
-	 * @var boolean
-	 */
-	protected $containingTagRead = false;
-
-	/**
-	 * Observers
-	 *
-	 * @var array
-	 */
-	protected $observers = array();
-
-	/**
-	 * Data load strategy
-	 *
-	 * @var GitPHP_CommitLoadStrategy_Interface
-	 */
-	protected $strategy;
-
-	/**
-	 * Instantiates object
-	 *
-	 * @param GitPHP_Project $project the project
-	 * @param string $hash object hash
-	 * @param GitPHP_CommitLoadStrategy_Interface $strategy load strategy
-	 */
-	public function __construct($project, $hash, GitPHP_CommitLoadStrategy_Interface $strategy)
-	{
-		parent::__construct($project, $hash);
-
-		if (!$strategy)
-			throw new Exception('Commit load strategy is required');
-
-		$this->SetStrategy($strategy);
-	}
-
-	/**
-	 * Set the load strategy
-	 *
-	 * @param GitPHP_CommitLoadStrategy_Interface $strategy load strategy
-	 */
-	public function SetStrategy(GitPHP_CommitLoadStrategy_Interface $strategy)
-	{
-		if (!$strategy)
-			return;
-
-		$this->strategy = $strategy;
-	}
-
-	/**
-	 * Gets the hash for this commit (overrides base)
-	 *
-	 * @param boolean $abbreviate true to abbreviate hash
-	 * @return string object hash
-	 */
-	public function GetHash($abbreviate = false)
-	{
-		if ($abbreviate && $this->strategy->LoadsAbbreviatedHash()) {
-			if (!$this->dataRead)
-				$this->ReadData();
-		}
-
-		return parent::GetHash($abbreviate);
-	}
-
-	/**
-	 * Gets the main parent of this commit
-	 *
-	 * @return GitPHP_Commit|null commit object for parent
-	 */
-	public function GetParent()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		if (isset($this->parents[0])) {
-			return $this->GetProject()->GetCommit($this->parents[0]);
-		}
-
-		return null;
-	}
-
-	/**
-	 * Gets an array of parent objects for this commit
-	 *
-	 * @return GitPHP_Commit[] array of commit objects
-	 */
-	public function GetParents()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		$parents = array();
-		foreach ($this->parents as $parent) {
-			$parents[] = $this->GetProject()->GetCommit($parent);
-		}
-
-		return $parents;
-	}
-
-	/**
-	 * Gets the tree for this commit
-	 *
-	 * @return GitPHP_Tree tree object
-	 */
-	public function GetTree()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		if (empty($this->tree))
-			return null;
-
-		$tree = $this->GetProject()->GetObjectManager()->GetTree($this->tree);
-		if ($tree) {
-			$tree->SetCommitHash($this->hash);
-			$tree->SetPath(null);
-		}
-
-		return $tree;
-	}
-
-	/**
-	 * Gets the author for this commit
-	 *
-	 * @return string author
-	 */
-	public function GetAuthor()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->author;
-	}
-
-	/**
-	 * Gets the author's name only
-	 *
-	 * @return string author name
-	 */
-	public function GetAuthorName()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return preg_replace('/ <.*/', '', $this->author);
-	}
-
-	/**
-	 * Gets the author's epoch
-	 *
-	 * @return string author epoch
-	 */
-	public function GetAuthorEpoch()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->authorEpoch;
-	}
-
-	/**
-	 * Gets the author's local epoch
-	 *
-	 * @return string author local epoch
-	 */
-	public function GetAuthorLocalEpoch()
-	{
-		$epoch = $this->GetAuthorEpoch();
-		$tz = $this->GetAuthorTimezone();
-		if (preg_match('/^([+\-][0-9][0-9])([0-9][0-9])$/', $tz, $regs)) {
-			$local = $epoch + ((((int)$regs[1]) + ($regs[2]/60)) * 3600);
-			return $local;
-		}
-		return $epoch;
-	}
-
-	/**
-	 * Gets the author's timezone
-	 *
-	 * @return string author timezone
-	 */
-	public function GetAuthorTimezone()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->authorTimezone;
-	}
-
-	/**
-	 * Gets the author for this commit
-	 *
-	 * @return string author
-	 */
-	public function GetCommitter()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->committer;
-	}
-
-	/**
-	 * Gets the author's name only
-	 *
-	 * @return string author name
-	 */
-	public function GetCommitterName()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return preg_replace('/ <.*/', '', $this->committer);
-	}
-
-	/**
-	 * Gets the committer's epoch
-	 *
-	 * @return string committer epoch
-	 */
-	public function GetCommitterEpoch()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->committerEpoch;
-	}
-
-	/**
-	 * Gets the committer's local epoch
-	 *
-	 * @return string committer local epoch
-	 */
-	public function GetCommitterLocalEpoch()
-	{
-		$epoch = $this->GetCommitterEpoch();
-		$tz = $this->GetCommitterTimezone();
-		if (preg_match('/^([+\-][0-9][0-9])([0-9][0-9])$/', $tz, $regs)) {
-			$local = $epoch + ((((int)$regs[1]) + ($regs[2]/60)) * 3600);
-			return $local;
-		}
-		return $epoch;
-	}
-
-	/**
-	 * Gets the author's timezone
-	 *
-	 * @return string author timezone
-	 */
-	public function GetCommitterTimezone()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->committerTimezone;
-	}
-
-	/**
-	 * Gets the commit title
-	 *
-	 * @param integer $trim length to trim to (0 for no trim)
-	 * @return string title
-	 */
-	public function GetTitle($trim = 0)
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		if ($trim > 0) {
-			if (function_exists('mb_strimwidth')) {
-				return mb_strimwidth($this->title, 0, $trim, '…');
-			} else if (strlen($this->title) > $trim) {
-				return substr($this->title, 0, $trim) . '…';
-			}
-		}
-
-		return $this->title;
-	}
-
-	/**
-	 * Gets the lines of comment
-	 *
-	 * @return string[] lines of comment
-	 */
-	public function GetComment()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->comment;
-	}
-
-	/**
-	 * Gets the lines of the comment matching the given pattern
-	 *
-	 * @param string $pattern pattern to find
-	 * @return string[] matching lines of comment
-	 */
-	public function SearchComment($pattern)
-	{
-		if (empty($pattern))
-			return $this->GetComment();
-
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return preg_grep('/' . $pattern . '/i', $this->comment);
-	}
-
-	/**
-	 * Gets the age of the commit
-	 *
-	 * @return string age
-	 */
-	public function GetAge()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		if (!empty($this->committerEpoch))
-			return time() - $this->committerEpoch;
-
-		return '';
-	}
-
-	/**
-	 * Returns whether this is a merge commit
-	 *
-	 * @return boolean true if merge commit
-	 */
-	public function IsMergeCommit()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return count($this->parents) > 1;
-	}
-
-	/**
-	 * Read the data for the commit
-	 */
-	protected function ReadData()
-	{
-		$this->dataRead = true;
-
-		list(
-			$abbreviatedHash,
-			$this->tree,
-			$this->parents,
-			$this->author,
-			$this->authorEpoch,
-			$this->authorTimezone,
-			$this->committer,
-			$this->committerEpoch,
-			$this->committerTimezone,
-			$this->title,
-			$this->comment
-		) = $this->strategy->Load($this);
-
-		if (!empty($abbreviatedHash)) {
-			$this->abbreviatedHash = $abbreviatedHash;
-			$this->abbreviatedHashLoaded = true;
-		}
-
-		foreach ($this->observers as $observer) {
-			$observer->ObjectChanged($this, GitPHP_Observer_Interface::CacheableDataChange);
-		}
-	}
-
-	/**
-	 * Gets heads that point to this commit
-	 * 
-	 * @return GitPHP_Head[] array of heads
-	 */
-	public function GetHeads()
-	{
-		$heads = array();
-
-		$projectRefs = $this->GetProject()->GetHeadList()->GetHeads();
-
-		foreach ($projectRefs as $ref) {
-			if ($ref->GetHash() == $this->hash) {
-				$heads[] = $ref;
-			}
-		}
-
-		return $heads;
-	}
-
-	/**
-	 * Gets tags that point to this commit
-	 *
-	 * @return GitPHP_Tag[] array of tags
-	 */
-	public function GetTags()
-	{
-		$tags = array();
-
-		$projectRefs = $this->GetProject()->GetTagList()->GetTags();
-
-		foreach ($projectRefs as $ref) {
-			if (($ref->GetType() == 'tag') || ($ref->GetType() == 'commit')) {
-				if ($ref->GetCommit()->GetHash() === $this->hash) {
-					$tags[] = $ref;
-				}
-			}
-		}
-
-		return $tags;
-	}
-
-	/**
-	 * Gets the tag that contains the changes in this commit
-	 *
-	 * @return GitPHP_Tag tag object
-	 */
-	public function GetContainingTag()
-	{
-		if (!$this->containingTagRead)
-			$this->ReadContainingTag();
-
-		if (empty($this->containingTag))
-			return null;
-
-		return $this->GetProject()->GetTagList()->GetTag($this->containingTag);
-	}
-
-	/**
-	 * Looks up the tag that contains the changes in this commit
-	 */
-	public function ReadContainingTag()
-	{
-		$this->containingTagRead = true;
-
-		$this->containingTag = $this->strategy->LoadContainingTag($this);
-	}
-
-	/**
-	 * Diffs this commit with its immediate parent
-	 *
-	 * @return GitPHP_TreeDiff Tree diff
-	 */
-	public function DiffToParent()
-	{
-		return new GitPHP_TreeDiff($this->GetProject(), $this->hash);
-	}
-
-	/**
-	 * Add a new observer
-	 *
-	 * @param GitPHP_Observer_Interface $observer observer
-	 */
-	public function AddObserver($observer)
-	{
-		if (!$observer)
-			return;
-
-		if (array_search($observer, $this->observers) !== false)
-			return;
-
-		$this->observers[] = $observer;
-	}
-
-	/**
-	 * Remove an observer
-	 *
-	 * @param GitPHP_Observer_Interface $observer observer
-	 */
-	public function RemoveObserver($observer)
-	{
-		if (!$observer)
-			return;
-
-		$key = array_search($observer, $this->observers);
-
-		if ($key === false)
-			return;
-
-		unset($this->observers[$key]);
-	}
-
-	/**
-	 * Called to prepare the object for serialization
-	 *
-	 * @return string[] list of properties to serialize
-	 */
-	public function __sleep()
-	{
-		$properties = array('dataRead', 'parents', 'tree', 'author', 'authorEpoch', 'authorTimezone', 'committer', 'committerEpoch', 'committerTimezone', 'title', 'comment', 'readTree');
-		return array_merge($properties, parent::__sleep());
-	}
-
-	/**
-	 * Gets the cache key to use for this object
-	 *
-	 * @return string cache key
-	 */
-	public function GetCacheKey()
-	{
-		return GitPHP_Commit::CacheKey($this->project->GetProject(), $this->hash);
-	}
-
-	/**
-	 * Compares two commits by age
-	 *
-	 * @param GitPHP_Commit $a first commit
-	 * @param GitPHP_Commit $b second commit
-	 * @return integer comparison result
-	 */
-	public static function CompareAge($a, $b)
-	{
-		if ($a->GetAge() === $b->GetAge()) {
-			// fall back on author epoch
-			return GitPHP_Commit::CompareAuthorEpoch($a, $b);
-		}
-		return ($a->GetAge() < $b->GetAge() ? -1 : 1);
-	}
-
-	/**
-	 * Compares two commits by author epoch
-	 *
-	 * @param GitPHP_Commit $a first commit
-	 * @param GitPHP_Commit $b second commit
-	 * @return integer comparison result
-	 */
-	public static function CompareAuthorEpoch($a, $b)
-	{
-		if ($a->GetAuthorEpoch() === $b->GetAuthorEpoch()) {
-			return 0;
-		}
-		return ($a->GetAuthorEpoch() > $b->GetAuthorEpoch() ? -1 : 1);
-	}
-
-	/**
-	 * Generates a commit cache key
-	 *
-	 * @param string $proj project
-	 * @param string $hash hash
-	 * @return string cache key
-	 */
-	public static function CacheKey($proj, $hash)
-	{
-		return 'project|' . $proj . '|commit|' . $hash;
-	}
-
-}
-

--- a/include/git/CommitLoadStrategy.interface.php
+++ /dev/null
@@ -1,35 +1,1 @@
-<?php
-/**
- * Interface for commit load data strategies
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-interface GitPHP_CommitLoadStrategy_Interface
-{
-	/**
-	 * Gets the data for a commit
-	 *
-	 * @param GitPHP_Commit $commit commit
-	 * @return array commit data
-	 */
-	public function Load($commit);
 
-	/**
-	 * Gets the containing tag for a commit
-	 *
-	 * @param GitPHP_Commit $commit commit
-	 * @return string containing tag
-	 */
-	public function LoadContainingTag($commit);
-
-	/**
-	 * Whether this load strategy loads the abbreviated hash
-	 *
-	 * @return boolean
-	 */
-	public function LoadsAbbreviatedHash();
-}
-

--- a/include/git/CommitLoad_Base.class.php
+++ /dev/null
@@ -1,57 +1,1 @@
-<?php
-/**
- * Base commit load strategy
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-abstract class GitPHP_CommitLoad_Base implements GitPHP_CommitLoadStrategy_Interface
-{
-	/**
-	 * Executable
-	 *
-	 * @var GitPHP_GitExe
-	 */
-	protected $exe;
 
-	/**
-	 * Constructor
-	 *
-	 * @param GitPHP_GitExe $exe executable
-	 */
-	public function __construct($exe)
-	{
-		if (!$exe)
-			throw new Exception('Git exe is required');
-
-		$this->exe = $exe;
-	}
-
-	/**
-	 * Gets the containing tag for a commit
-	 *
-	 * @param GitPHP_Commit $commit commit
-	 * @return string containing tag
-	 */
-	public function LoadContainingTag($commit)
-	{
-		if (!$commit)
-			return;
-
-		$args = array();
-		$args[] = '--tags';
-		$args[] = $commit->GetHash();
-		$revs = explode("\n", $this->exe->Execute($commit->GetProject()->GetPath(), GIT_NAME_REV, $args));
-
-		foreach ($revs as $revline) {
-			if (preg_match('/^([0-9a-fA-F]{40})\s+tags\/(.+)(\^[0-9]+|\~[0-9]+)$/', $revline, $regs)) {
-				if ($regs[1] == $commit->GetHash()) {
-					return $regs[2];
-				}
-			}
-		}
-	}
-}
-

--- a/include/git/CommitLoad_Git.class.php
+++ /dev/null
@@ -1,140 +1,1 @@
-<?php
-/**
- * Commit load strategy using git exe
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_CommitLoad_Git extends GitPHP_CommitLoad_Base
-{
-	/**
-	 * Gets the data for a commit
-	 *
-	 * @param GitPHP_Commit $commit commit
-	 * @return array commit data
-	 */
-	public function Load($commit)
-	{
-		if (!$commit)
-			return;
 
-		$abbreviatedHash = null;
-		$tree = null;
-		$parents = array();
-		$author = null;
-		$authorEpoch = null;
-		$authorTimezone = null;
-		$committer = null;
-		$committerEpoch = null;
-		$committerTimezone = null;
-		$title = null;
-		$comment = array();
-
-
-		/* get data from git_rev_list */
-		$args = array();
-		$args[] = '--header';
-		$args[] = '--parents';
-		$args[] = '--max-count=1';
-		$args[] = '--abbrev-commit';
-		$args[] = $commit->GetHash();
-		$ret = $this->exe->Execute($commit->GetProject()->GetPath(), GIT_REV_LIST, $args);
-
-		$lines = explode("\n", $ret);
-
-		if (!isset($lines[0]))
-			return;
-
-		/* In case we returned something unexpected */
-		$tok = strtok($lines[0], ' ');
-		if ((strlen($tok) == 0) || (substr_compare($commit->GetHash(), $tok, 0, strlen($tok)) !== 0)) {
-			return;
-		}
-		$abbreviatedHash = $tok;
-
-		array_shift($lines);
-
-
-		$linecount = count($lines);
-		$i = 0;
-		$encoding = null;
-
-		/* Commit header */
-		for ($i = 0; $i < $linecount; $i++) {
-			$line = $lines[$i];
-			if (preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
-				/* Tree */
-				$tree = $regs[1];
-			} else if (preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
-				/* Parent */
-				$parents[] = $regs[1];
-			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
-				/* author data */
-				$author = $regs[1];
-				$authorEpoch = $regs[2];
-				$authorTimezone = $regs[3];
-			} else if (preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
-				/* committer data */
-				$committer = $regs[1];
-				$committerEpoch = $regs[2];
-				$committerTimezone = $regs[3];
-			} else if (preg_match('/^encoding (.+)$/', $line, $regs)) {
-				$gitEncoding = trim($regs[1]);
-				if ((strlen($gitEncoding) > 0) && function_exists('mb_list_encodings')) {
-					$supportedEncodings = mb_list_encodings();
-					$encIdx = array_search(strtolower($gitEncoding), array_map('strtolower', $supportedEncodings));
-					if ($encIdx !== false) {
-						$encoding = $supportedEncodings[$encIdx];
-					}
-				}
-				$encoding = trim($regs[1]);
-			} else if (strlen($line) == 0) {
-				break;
-			}
-		}
-		
-		/* Commit body */
-		for ($i += 1; $i < $linecount; $i++) {
-			$trimmed = trim($lines[$i]);
-
-			if ((strlen($trimmed) > 0) && (strlen($encoding) > 0) && function_exists('mb_convert_encoding')) {
-				$trimmed = mb_convert_encoding($trimmed, 'UTF-8', $encoding);
-			}
-
-			if (empty($title) && (strlen($trimmed) > 0))
-				$title = $trimmed;
-			if (!empty($title)) {
-				if ((strlen($trimmed) > 0) || ($i < ($linecount-1)))
-					$comment[] = $trimmed;
-			}
-		}
-
-		return array(
-			$abbreviatedHash,
-			$tree,
-			$parents,
-			$author,
-			$authorEpoch,
-			$authorTimezone,
-			$committer,
-			$committerEpoch,
-			$committerTimezone,
-			$title,
-			$comment
-		);
-
-	}
-
-	/**
-	 * Whether this load strategy loads the abbreviated hash
-	 *
-	 * @return boolean
-	 */
-	public function LoadsAbbreviatedHash()
-	{
-		return true;
-	}
-}
-

--- a/include/git/CommitLoad_Raw.class.php
+++ /dev/null
@@ -1,144 +1,1 @@
-<?php
-/**
- * Commit load strategy using raw git objects
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_CommitLoad_Raw extends GitPHP_CommitLoad_Base
-{
-	/**
-	 * Object loader
-	 *
-	 * @var GitPHP_GitObjectLoader
-	 */
-	protected $objectLoader;
 
-	/**
-	 * Constructor
-	 *
-	 * @param GitPHP_GitObjectLoader $objectLoader object loader
-	 * @param GitPHP_GitExe $exe git exe
-	 */
-	public function __construct($objectLoader, $exe)
-	{
-		if (!$objectLoader)
-			throw new Exception('Git object loader is required');
-
-		$this->objectLoader = $objectLoader;
-
-		parent::__construct($exe);
-	}
-
-	/**
-	 * Gets the data for a commit
-	 *
-	 * @param GitPHP_Commit $commit commit
-	 * @return array commit data
-	 */
-	public function Load($commit)
-	{
-		if (!$commit)
-			return;
-
-		$abbreviatedHash = null;
-		$tree = null;
-		$parents = array();
-		$author = null;
-		$authorEpoch = null;
-		$authorTimezone = null;
-		$committer = null;
-		$committerEpoch = null;
-		$committerTimezone = null;
-		$title = null;
-		$comment = array();
-
-		$data = $this->objectLoader->GetObject($commit->GetHash());
-		if (empty($data))
-			return;
-
-		$lines = explode("\n", $data);
-
-		$linecount = count($lines);
-		$i = 0;
-		$encoding = null;
-
-		/* Commit header */
-		for ($i = 0; $i < $linecount; $i++) {
-			$line = $lines[$i];
-			if (preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
-				/* Tree */
-				$tree = $regs[1];
-			} else if (preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
-				/* Parent */
-				$parents[] = $regs[1];
-			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
-				/* author data */
-				$author = $regs[1];
-				$authorEpoch = $regs[2];
-				$authorTimezone = $regs[3];
-			} else if (preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
-				/* committer data */
-				$committer = $regs[1];
-				$committerEpoch = $regs[2];
-				$committerTimezone = $regs[3];
-			} else if (preg_match('/^encoding (.+)$/', $line, $regs)) {
-				$gitEncoding = trim($regs[1]);
-				if ((strlen($gitEncoding) > 0) && function_exists('mb_list_encodings')) {
-					$supportedEncodings = mb_list_encodings();
-					$encIdx = array_search(strtolower($gitEncoding), array_map('strtolower', $supportedEncodings));
-					if ($encIdx !== false) {
-						$encoding = $supportedEncodings[$encIdx];
-					}
-				}
-				$encoding = trim($regs[1]);
-			} else if (strlen($line) == 0) {
-				break;
-			}
-		}
-		
-		/* Commit body */
-		for ($i += 1; $i < $linecount; $i++) {
-			$trimmed = trim($lines[$i]);
-
-			if ((strlen($trimmed) > 0) && (strlen($encoding) > 0) && function_exists('mb_convert_encoding')) {
-				$trimmed = mb_convert_encoding($trimmed, 'UTF-8', $encoding);
-			}
-
-			if (empty($title) && (strlen($trimmed) > 0))
-				$title = $trimmed;
-			if (!empty($title)) {
-				if ((strlen($trimmed) > 0) || ($i < ($linecount-1)))
-					$comment[] = $trimmed;
-			}
-		}
-
-		return array(
-			$abbreviatedHash,
-			$tree,
-			$parents,
-			$author,
-			$authorEpoch,
-			$authorTimezone,
-			$committer,
-			$committerEpoch,
-			$committerTimezone,
-			$title,
-			$comment
-		);
-
-	}
-
-	/**
-	 * Whether this load strategy loads the abbreviated hash
-	 *
-	 * @return boolean
-	 */
-	public function LoadsAbbreviatedHash()
-	{
-		return false;
-	}
-}
-

--- a/include/git/Tag.class.php
+++ /dev/null
@@ -1,454 +1,1 @@
-<?php
-/**
- * Represents a single tag object
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2010 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_Tag extends GitPHP_Ref implements GitPHP_Observable_Interface, GitPHP_Cacheable_Interface
-{
-	
-	/**
-	 * Whether data for this tag has been read
-	 *
-	 * @var boolean
-	 */
-	protected $dataRead = false;
 
-	/**
-	 * The identifier for the tagged object
-	 *
-	 * @var string
-	 */
-	protected $object;
-
-	/**
-	 * The commit hash
-	 *
-	 * @var string
-	 */
-	protected $commitHash;
-
-	/**
-	 * The tagged object type
-	 *
-	 * @var string
-	 */
-	protected $type;
-
-	/**
-	 * The tagger
-	 *
-	 * @var string
-	 */
-	protected $tagger;
-
-	/**
-	 * The tagger epoch
-	 *
-	 * @var string
-	 */
-	protected $taggerEpoch;
-
-	/**
-	 * The tagger timezone
-	 *
-	 * @var string
-	 */
-	protected $taggerTimezone;
-
-	/**
-	 * The tag comment
-	 *
-	 * @var string
-	 */
-	protected $comment = array();
-
-	/**
-	 * Observers
-	 *
-	 * @var array
-	 */
-	protected $observers = array();
-
-	/**
-	 * Data load strategy
-	 *
-	 * @var GitPHP_TagLoadStrategy_Interface
-	 */
-	protected $strategy;
-
-	/**
-	 * Instantiates tag
-	 *
-	 * @param GitPHP_Project $project the project
-	 * @param string $tag tag name
-	 * @param GitPHP_TagLoadStrategy_Interface $strategy load strategy
-	 * @param string $tagHash tag hash
-	 */
-	public function __construct($project, $tag, GitPHP_TagLoadStrategy_Interface $strategy, $tagHash = '')
-	{
-		parent::__construct($project, 'tags', $tag, $tagHash);
-
-		if (!$strategy)
-			throw new Exception('Tag load strategy is required');
-
-		$this->SetStrategy($strategy);
-	}
-
-	/**
-	 * Set the load strategy
-	 *
-	 * @param GitPHP_TagLoadStrategy_Interface $strategy load strategy
-	 */
-	public function SetStrategy(GitPHP_TagLoadStrategy_Interface $strategy)
-	{
-		if (!$strategy)
-			return;
-
-		$this->strategy = $strategy;
-	}
-
-	/**
-	 * Gets the object this tag points to
-	 *
-	 * @return GitPHP_Commit|GitPHP_Tag|GitPHP_Blob object for this tag
-	 */
-	public function GetObject()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		if ($this->type == 'commit') {
-			return $this->GetProject()->GetCommit($this->object);
-		} else if ($this->type == 'tag') {
-			return $this->GetProject()->GetTagList()->GetTag($this->object);
-		} else if ($this->type == 'blob') {
-			return $this->GetProject()->GetObjectManager()->GetBlob($this->object);
-		}
-
-		return null;
-	}
-
-	/**
-	 * Gets the commit this tag points to
-	 *
-	 * @return GitPHP_Commit commit for this tag
-	 */
-	public function GetCommit()
-	{
-		if ($this->commitHash)
-			return $this->GetProject()->GetCommit($this->commitHash);
-
-		if (!$this->dataRead) {
-			$this->ReadData();
-		}
-
-		if (!$this->commitHash) {
-			if ($this->type == 'commit') {
-				$this->commitHash = $this->object;
-			} else if ($this->type == 'tag') {
-				$tag = $this->GetProject()->GetTagList()->GetTag($this->object);
-				$this->commitHash = $tag->GetCommit()->GetHash();
-			}
-		}
-
-		return $this->GetProject()->GetCommit($this->commitHash);
-	}
-
-	/**
-	 * Sets the commit this tag points to
-	 *
-	 * @param GitPHP_Commit $commit commit object 
-	 */
-	public function SetCommit($commit)
-	{
-		if (!$commit)
-			return;
-
-		$this->SetCommitHash($commit->GetHash());
-	}
-
-	/**
-	 * Sets the hash of the commit this tag points to
-	 *
-	 * @param string $hash hash
-	 */
-	public function SetCommitHash($hash)
-	{
-		if (!preg_match('/^[0-9A-Fa-f]{40}$/', $hash))
-			return;
-
-		if (!$this->commitHash)
-			$this->commitHash = $hash;
-	}
-
-	/**
-	 * Gets the tag type
-	 *
-	 * @return string tag type
-	 */
-	public function GetType()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->type;
-	}
-
-	/**
-	 * Gets the tagger
-	 *
-	 * @return string tagger
-	 */
-	public function GetTagger()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->tagger;
-	}
-
-	/**
-	 * Gets the tagger epoch
-	 *
-	 * @return string tagger epoch
-	 */
-	public function GetTaggerEpoch()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->taggerEpoch;
-	}
-
-	/**
-	 * Gets the tagger local epoch
-	 *
-	 * @return string tagger local epoch
-	 */
-	public function GetTaggerLocalEpoch()
-	{
-		$epoch = $this->GetTaggerEpoch();
-		$tz = $this->GetTaggerTimezone();
-		if (preg_match('/^([+\-][0-9][0-9])([0-9][0-9])$/', $tz, $regs)) {
-			$local = $epoch + ((((int)$regs[1]) + ($regs[2]/60)) * 3600);
-			return $local;
-		}
-		return $epoch;
-	}
-
-	/**
-	 * Gets the tagger timezone
-	 *
-	 * @return string tagger timezone
-	 */
-	public function GetTaggerTimezone()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->taggerTimezone;
-	}
-
-	/**
-	 * Gets the tag age
-	 *
-	 * @return string age
-	 */
-	public function GetAge()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return time() - $this->taggerEpoch;
-	}
-
-	/**
-	 * Gets the tag comment
-	 *
-	 * @return string[] comment lines
-	 */
-	public function GetComment()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		return $this->comment;
-	}
-
-	/**
-	 * Tests if this is a light tag (tag without tag object)
-	 *
-	 * @return boolean true if tag is light (has no object)
-	 */
-	public function LightTag()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		if (!$this->object)
-			return true;
-
-		if (($this->type == 'commit') && ($this->object == $this->GetHash())) {
-			return true;
-		}
-
-		return false;
-	}
-
-	/**
-	 * Reads the tag data
-	 */
-	protected function ReadData()
-	{
-		$this->dataRead = true;
-
-		list(
-			$this->type,
-			$this->object,
-			$commitHash,
-			$this->tagger,
-			$this->taggerEpoch,
-			$this->taggerTimezone,
-			$this->comment
-		) = $this->strategy->Load($this);
-
-		if (!empty($commitHash))
-			$this->commitHash = $commitHash;
-
-		foreach ($this->observers as $observer) {
-			$observer->ObjectChanged($this, GitPHP_Observer_Interface::CacheableDataChange);
-		}
-	}
-
-	/**
-	 * Add a new observer
-	 *
-	 * @param GitPHP_Observer_Interface $observer observer
-	 */
-	public function AddObserver($observer)
-	{
-		if (!$observer)
-			return;
-
-		if (array_search($observer, $this->observers) !== false)
-			return;
-
-		$this->observers[] = $observer;
-	}
-
-	/**
-	 * Remove an observer
-	 *
-	 * @param GitPHP_Observer_Interface $observer observer
-	 */
-	public function RemoveObserver($observer)
-	{
-		if (!$observer)
-			return;
-
-		$key = array_search($observer, $this->observers);
-
-		if ($key === false)
-			return;
-
-		unset($this->observers[$key]);
-	}
-
-	/**
-	 * Called to prepare the object for serialization
-	 *
-	 * @return string[] list of properties to serialize
-	 */
-	public function __sleep()
-	{
-		$properties = array('dataRead', 'object', 'commitHash', 'type', 'tagger', 'taggerEpoch', 'taggerTimezone', 'comment');
-		return array_merge($properties, parent::__sleep());
-	}
-
-	/**
-	 * Gets the cache key to use for this object
-	 *
-	 * @return string cache key
-	 */
-	public function GetCacheKey()
-	{
-		return GitPHP_Tag::CacheKey($this->project->GetProject(), $this->refName);
-	}
-
-	/**
-	 * Gets tag's creation epoch (tagger epoch, or committer epoch for light tags)
-	 *
-	 * @return string creation epoch
-	 */
-	public function GetCreationEpoch()
-	{
-		if (!$this->dataRead)
-			$this->ReadData();
-
-		if ($this->LightTag())
-			return $this->GetCommit()->GetCommitterEpoch();
-		else
-			return $this->taggerEpoch;
-	}
-
-	/**
-	 * Compares two tags by age
-	 *
-	 * @param GitPHP_Tag $a first tag
-	 * @param GitPHP_Tag $b second tag
-	 * @return integer comparison result
-	 */
-	public static function CompareAge($a, $b)
-	{
-		$aObj = $a->GetObject();
-		$bObj = $b->GetObject();
-		if (($aObj instanceof GitPHP_Commit) && ($bObj instanceof GitPHP_Commit)) {
-			return GitPHP_Commit::CompareAge($aObj, $bObj);
-		}
-
-		if ($aObj instanceof GitPHP_Commit)
-			return 1;
-
-		if ($bObj instanceof GitPHP_Commit)
-			return -1;
-
-		return strcmp($a->GetName(), $b->GetName());
-	}
-
-	/**
-	 * Compares to tags by creation epoch
-	 *
-	 * @param GitPHP_Tag $a first tag
-	 * @param GitPHP_Tag $b second tag
-	 * @return integer comparison result
-	 */
-	public static function CompareCreationEpoch($a, $b)
-	{
-		$aEpoch = $a->GetCreationEpoch();
-		$bEpoch = $b->GetCreationEpoch();
-
-		if ($aEpoch == $bEpoch) {
-			return 0;
-		}
-
-		return ($aEpoch < $bEpoch ? 1 : -1);
-	}
-
-	/**
-	 * Generates a tag cache key
-	 *
-	 * @param string $proj project
-	 * @param string $tag tag name
-	 * @return string cache key
-	 */
-	public static function CacheKey($proj, $tag)
-	{
-		return 'project|' . $proj . '|tag|' . $tag;
-	}
-
-}
-

--- a/include/git/TagLoadStrategy.interface.php
+++ /dev/null
@@ -1,20 +1,1 @@
-<?php
-/**
- * Interface for tag data load strategies
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-interface GitPHP_TagLoadStrategy_Interface
-{
-	/**
-	 * Gets the data for a tag
-	 *
-	 * @param GitPHP_Tag $tag tag
-	 * @return array array of tag data
-	 */
-	public function Load($tag);
-}
 

--- a/include/git/TagLoad_Git.class.php
+++ /dev/null
@@ -1,149 +1,1 @@
-<?php
-/**
- * Tag load strategy using git exe
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_TagLoad_Git implements GitPHP_TagLoadStrategy_Interface
-{
-	/**
-	 * Executable
-	 *
-	 * @var GitPHP_GitExe
-	 */
-	protected $exe;
 
-	/**
-	 * Constructor
-	 *
-	 * @param GitPHP_GitExe $exe executable
-	 */
-	public function __construct($exe)
-	{
-		if (!$exe)
-			throw new Exception('Git exe is required');
-
-		$this->exe = $exe;
-	}
-
-	/**
-	 * Gets the data for a tag
-	 *
-	 * @param GitPHP_Tag $tag tag
-	 * @return array array of tag data
-	 */
-	public function Load($tag)
-	{
-		if (!$tag)
-			return;
-
-		$type = null;
-		$object = null;
-		$commitHash = null;
-		$tagger = null;
-		$taggerEpoch = null;
-		$taggerTimezone = null;
-		$comment = array();
-
-
-		$args = array();
-		$args[] = '-t';
-		$args[] = $tag->GetHash();
-		$ret = trim($this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args));
-		
-		if ($ret === 'commit') {
-			/* light tag */
-			$object = $tag->GetHash();
-			$commitHash = $tag->GetHash();
-			$type = 'commit';
-			return array(
-				$type,
-				$object,
-				$commitHash,
-				$tagger,
-				$taggerEpoch,
-				$taggerTimezone,
-				$comment
-			);
-		}
-
-		/* get data from tag object */
-		$args = array();
-		$args[] = 'tag';
-		$args[] = $tag->GetName();
-		$ret = $this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args);
-
-		$lines = explode("\n", $ret);
-
-		if (!isset($lines[0]))
-			return;
-
-		$objectHash = null;
-
-		$readInitialData = false;
-		foreach ($lines as $i => $line) {
-			if (!$readInitialData) {
-				if (preg_match('/^object ([0-9a-fA-F]{40})$/', $line, $regs)) {
-					$objectHash = $regs[1];
-					continue;
-				} else if (preg_match('/^type (.+)$/', $line, $regs)) {
-					$type = $regs[1];
-					continue;
-				} else if (preg_match('/^tag (.+)$/', $line, $regs)) {
-					continue;
-				} else if (preg_match('/^tagger (.*) ([0-9]+) (.*)$/', $line, $regs)) {
-					$tagger = $regs[1];
-					$taggerEpoch = $regs[2];
-					$taggerTimezone = $regs[3];
-					continue;
-				}
-			}
-
-			$trimmed = trim($line);
-
-			if ((strlen($trimmed) > 0) || ($readInitialData === true)) {
-				$comment[] = $line;
-			}
-			$readInitialData = true;
-
-		}
-
-		switch ($type) {
-			case 'commit':
-				$object = $objectHash;
-				$commitHash = $objectHash;
-				break;
-			case 'tag':
-				$args = array();
-				$args[] = 'tag';
-				$args[] = $objectHash;
-				$ret = $this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args);
-				$lines = explode("\n", $ret);
-				foreach ($lines as $i => $line) {
-					if (preg_match('/^tag (.+)$/', $line, $regs)) {
-						$name = trim($regs[1]);
-						$object = $name;
-					}
-				}
-				break;
-			case 'blob':
-				$object = $objectHash;
-				break;
-		}
-
-		return array(
-			$type,
-			$object,
-			$commitHash,
-			$tagger,
-			$taggerEpoch,
-			$taggerTimezone,
-			$comment
-		);
-
-	}
-}
-

--- a/include/git/TagLoad_Raw.class.php
+++ /dev/null
@@ -1,134 +1,1 @@
-<?php
-/**
- * Tag load strategy using raw git objects
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_TagLoad_Raw implements GitPHP_TagLoadStrategy_Interface
-{
-	/**
-	 * Object loader
-	 *
-	 * @var GitPHP_GitObjectLoader
-	 */
-	protected $objectLoader;
 
-	/**
-	 * Constructor
-	 *
-	 * @param GitPHP_GitObjectLoader $objectLoader object loader
-	 */
-	public function __construct($objectLoader)
-	{
-		if (!$objectLoader)
-			throw new Exception('Object loader is required');
-
-		$this->objectLoader = $objectLoader;
-	}
-
-	/**
-	 * Gets the data for a tag
-	 *
-	 * @param GitPHP_Tag $tag tag
-	 * @return array array of tag data
-	 */
-	public function Load($tag)
-	{
-		if (!$tag)
-			return;
-
-		$type = null;
-		$object = null;
-		$commitHash = null;
-		$tagger = null;
-		$taggerEpoch = null;
-		$taggerTimezone = null;
-		$comment = array();
-
-		$data = $this->objectLoader->GetObject($tag->GetHash(), $packedType);
-		
-		if ($packedType == GitPHP_Pack::OBJ_COMMIT) {
-			/* light tag */
-			$object = $tag->GetHash();
-			$commitHash = $tag->GetHash();
-			$type = 'commit';
-			return array(
-				$type,
-				$object,
-				$commitHash,
-				$tagger,
-				$taggerEpoch,
-				$taggerTimezone,
-				$comment
-			);
-		}
-
-		$lines = explode("\n", $data);
-
-		if (!isset($lines[0]))
-			return;
-
-		$objectHash = null;
-
-		$readInitialData = false;
-		foreach ($lines as $i => $line) {
-			if (!$readInitialData) {
-				if (preg_match('/^object ([0-9a-fA-F]{40})$/', $line, $regs)) {
-					$objectHash = $regs[1];
-					continue;
-				} else if (preg_match('/^type (.+)$/', $line, $regs)) {
-					$type = $regs[1];
-					continue;
-				} else if (preg_match('/^tag (.+)$/', $line, $regs)) {
-					continue;
-				} else if (preg_match('/^tagger (.*) ([0-9]+) (.*)$/', $line, $regs)) {
-					$tagger = $regs[1];
-					$taggerEpoch = $regs[2];
-					$taggerTimezone = $regs[3];
-					continue;
-				}
-			}
-
-			$trimmed = trim($line);
-
-			if ((strlen($trimmed) > 0) || ($readInitialData === true)) {
-				$comment[] = $line;
-			}
-			$readInitialData = true;
-		}
-
-		switch ($type) {
-			case 'commit':
-				$object = $objectHash;
-				$commitHash = $objectHash;
-				break;
-			case 'tag':
-				$objectData = $this->objectLoader->GetObject($objectHash);
-				$lines = explode("\n", $objectData);
-				foreach ($lines as $i => $line) {
-					if (preg_match('/^tag (.+)$/', $line, $regs)) {
-						$name = trim($regs[1]);
-						$object = $name;
-					}
-				}
-				break;
-			case 'blob':
-				$object = $objectHash;
-				break;
-		}
-
-		return array(
-			$type,
-			$object,
-			$commitHash,
-			$tagger,
-			$taggerEpoch,
-			$taggerTimezone,
-			$comment
-		);
-	}
-}
-

--- a/include/git/Tree.class.php
+++ /dev/null
@@ -1,310 +1,1 @@
-<?php
-/**
- * Represents a single tree
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2010 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_Tree extends GitPHP_FilesystemObject implements GitPHP_Observable_Interface, GitPHP_Cacheable_Interface
-{
 
-	/**
-	 * Tree contents
-	 *
-	 * @var array
-	 */
-	protected $contents = array();
-
-	/**
-	 * Whether contents were read
-	 *
-	 * @var boolean
-	 */
-	protected $contentsRead = false;
-
-	/**
-	 * Tree hash to path mappings
-	 *
-	 * @var array
-	 */
-	protected $treePaths = array();
-
-	/**
-	 * Blob hash to path mappings
-	 *
-	 * @var array
-	 */
-	protected $blobPaths = array();
-
-	/**
-	 * Whether hash paths have been read
-	 */
-	protected $hashPathsRead = false;
-
-	/**
-	 * Observers
-	 *
-	 * @var array
-	 */
-	protected $observers = array();
-
-	/**
-	 * Data load strategy
-	 *
-	 * @var GitPHP_TreeLoadStrategy_Interface
-	 */
-	protected $strategy;
-
-	/**
-	 * Instantiates object
-	 *
-	 * @param GitPHP_Project $project the project
-	 * @param string $hash tree hash
-	 * @param GitPHP_TreeLoadStrategy_Interface $strategy load strategy
-	 */
-	public function __construct($project, $hash, $strategy)
-	{
-		parent::__construct($project, $hash);
-
-		if (!$strategy)
-			throw new Exception('Tree load strategy is required');
-
-		$this->SetStrategy($strategy);
-	}
-
-	/**
-	 * Set the load strategy
-	 *
-	 * @param GitPHP_TreeLoadStrategy_Interface $strategy load strategy
-	 */
-	public function SetStrategy(GitPHP_TreeLoadStrategy_Interface $strategy)
-	{
-		if (!$strategy)
-			return;
-
-		$this->strategy = $strategy;
-	}
-
-	/**
-	 * Sets the object path (overrides base)
-	 *
-	 * @param string $path object path
-	 */
-	public function SetPath($path)
-	{
-		if ($this->path == $path)
-			return;
-
-		if ($this->hashPathsRead) {
-			$this->treePaths = array();
-			$this->blobPaths = array();
-			$this->hashPathsRead = false;
-		}
-
-		$this->path = $path;
-	}
-
-	/**
-	 * Gets the tree contents
-	 *
-	 * @return (GitPHP_Tree|GitPHP_Blob)[] array of objects for contents
-	 */
-	public function GetContents()
-	{
-		if (!$this->contentsRead)
-			$this->ReadContents();
-
-		$contents = array();
-		$usedTrees = array();
-		$usedBlobs = array();
-
-		for ($i = 0; $i < count($this->contents); ++$i) {
-			$data = $this->contents[$i];
-			$obj = null;
-
-			if (!isset($data['hash']) || empty($data['hash']))
-				continue;
-
-			if ($data['type'] == 'tree') {
-				$obj = $this->GetProject()->GetObjectManager()->GetTree($data['hash']);
-				if (isset($usedTrees[$data['hash']])) {
-					$obj = clone $obj;
-				} else {
-					$usedTrees[$data['hash']] = 1;
-				}
-			} else if ($data['type'] == 'blob') {
-				$obj = $this->GetProject()->GetObjectManager()->GetBlob($data['hash']);
-				if (isset($usedBlobs[$data['hash']])) {
-					$obj = clone $obj;
-				} else {
-					$usedBlobs[$data['hash']] = 1;
-				}
-
-				if (isset($data['size']) && !empty($data['size'])) {
-					$obj->SetSize($data['size']);
-				}
-			} else {
-				continue;
-			}
-
-			if (isset($data['mode']) && !empty($data['mode']))
-				$obj->SetMode($data['mode']);
-
-			if (isset($data['path']) && !empty($data['path']))
-				$obj->SetPath($data['path']);
-
-			if ($this->commitHash)
-				$obj->SetCommitHash($this->commitHash);
-
-			$contents[] = $obj;
-		}
-
-		return $contents;
-	}
-
-	/**
-	 * Reads the tree contents
-	 */
-	protected function ReadContents()
-	{
-		$this->contentsRead = true;
-
-		$this->contents = $this->strategy->Load($this);
-
-		foreach ($this->observers as $observer) {
-			$observer->ObjectChanged($this, GitPHP_Observer_Interface::CacheableDataChange);
-		}
-	}
-
-	/**
-	 * Gets tree paths mapped to hashes
-	 *
-	 * @return array
-	 */
-	public function GetTreePaths()
-	{
-		if (!$this->hashPathsRead)
-			$this->ReadHashPaths();
-
-		return $this->treePaths;
-	}
-
-	/**
-	 * Gets blob paths mapped to hashes
-	 *
-	 * @return array
-	 */
-	public function GetBlobPaths()
-	{
-		if (!$this->hashPathsRead)
-			$this->ReadHashPaths();
-
-		return $this->blobPaths;
-	}
-
-	/**
-	 * Given a filepath, get its hash
-	 *
-	 * @param string $path path
-	 * @return string hash
-	 */
-	public function PathToHash($path)
-	{
-		if (empty($path))
-			return '';
-
-		if (!$this->hashPathsRead)
-			$this->ReadHashPaths();
-
-		if (isset($this->blobPaths[$path])) {
-			return $this->blobPaths[$path];
-		}
-
-		if (isset($this->treePaths[$path])) {
-			return $this->treePaths[$path];
-		}
-
-		return '';
-	}
-
-	/**
-	 * Read hash to path mappings
-	 */
-	private function ReadHashPaths()
-	{
-		$this->hashPathsRead = true;
-
-		list($this->treePaths, $this->blobPaths) = $this->strategy->LoadHashPaths($this);
-	}
-
-	/**
-	 * Add a new observer
-	 *
-	 * @param GitPHP_Observer_Interface $observer observer
-	 */
-	public function AddObserver($observer)
-	{
-		if (!$observer)
-			return;
-
-		if (array_search($observer, $this->observers) !== false)
-			return;
-
-		$this->observers[] = $observer;
-	}
-
-	/**
-	 * Remove an observer
-	 *
-	 * @param GitPHP_Observer_Interface $observer observer
-	 */
-	public function RemoveObserver($observer)
-	{
-		if (!$observer)
-			return;
-
-		$key = array_search($observer, $this->observers);
-
-		if ($key === false)
-			return;
-
-		unset($this->observers[$key]);
-	}
-
-	/**
-	 * Called to prepare the object for serialization
-	 *
-	 * @return string[] list of properties to serialize
-	 */
-	public function __sleep()
-	{
-		$properties = array('contents', 'contentsRead');
-		return array_merge($properties, parent::__sleep());
-	}
-
-	/**
-	 * Gets the cache key to use for this object
-	 *
-	 * @return string cache key
-	 */
-	public function GetCacheKey()
-	{
-		return GitPHP_Tree::CacheKey($this->project->GetProject(), $this->hash);
-	}
-
-	/**
-	 * Generates a tree cache key
-	 *
-	 * @param string $proj project
-	 * @param string $hash hash
-	 * @return string cache key
-	 */
-	public static function CacheKey($proj, $hash)
-	{
-		return 'project|' . $proj . '|tree|' . $hash;
-	}
-
-}
-

--- a/include/git/TreeLoadStrategy.interface.php
+++ /dev/null
@@ -1,28 +1,1 @@
-<?php
-/**
- * Interface for tree data load strategies
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-interface GitPHP_TreeLoadStrategy_Interface
-{
-	/**
-	 * Gets the data for a tree
-	 *
-	 * @param GitPHP_Tree $tree tree
-	 * @return array array of tree contents
-	 */
-	public function Load($tree);
 
-	/**
-	 * Gets the hash paths for a tree
-	 *
-	 * @param GitPHP_Tree $tree tre
-	 * @return array array of treepath and hashpath arrays
-	 */
-	public function LoadHashPaths($tree);
-}
-

--- a/include/git/TreeLoad_Base.class.php
+++ /dev/null
@@ -1,73 +1,1 @@
-<?php
-/**
- * Base tree load strategy
- *
- * @author CHristopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-abstract class GitPHP_TreeLoad_Base implements GitPHP_TreeLoadStrategy_Interface
-{
-	/**
-	 * Executable
-	 *
-	 * @var GitPHP_GitExe
-	 */
-	protected $exe;
 
-	/**
-	 * Constructor
-	 *
-	 * @param GitPHP_GitExe $exe executable
-	 */
-	public function __construct($exe)
-	{
-		if (!$exe)
-			throw new Exception('Git exe is required');
-
-		$this->exe = $exe;
-	}
-
-	/**
-	 * Gets the hash paths for a tree
-	 *
-	 * @param GitPHP_Tree $tree tre
-	 * @return array array of treepath and hashpath arrays
-	 */
-	public function LoadHashPaths($tree)
-	{
-		if (!$tree)
-			return;
-
-		$treePaths = array();
-		$blobPaths = array();
-
-		$args = array();
-		$args[] = '--full-name';
-		$args[] = '-r';
-		$args[] = '-t';
-		$args[] = $tree->GetHash();
-
-		$lines = explode("\n", $this->exe->Execute($tree->GetProject()->GetPath(), GIT_LS_TREE, $args));
-
-		foreach ($lines as $line) {
-			if (preg_match("/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/", $line, $regs)) {
-				switch ($regs[2]) {
-					case 'tree':
-						$treePaths[trim($regs[4])] = $regs[3];
-						break;
-					case 'blob';
-						$blobPaths[trim($regs[4])] = $regs[3];
-						break;
-				}
-			}
-		}
-
-		return array(
-			$treePaths,
-			$blobPaths
-		);
-	}
-}
-

--- a/include/git/TreeLoad_Git.class.php
+++ /dev/null
@@ -1,76 +1,1 @@
-<?php
-/**
- * Tree load strategy using git exe
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_TreeLoad_Git extends GitPHP_TreeLoad_Base
-{
-	/**
-	 * Gets the data for a tree
-	 *
-	 * @param GitPHP_Tree $tree tree
-	 * @return array array of tree contents
-	 */
-	public function Load($tree)
-	{
-		if (!$tree)
-			return;
 
-		$contents = array();
-
-		$treePath = $tree->GetPath();
-
-		$args = array();
-		$args[] = '--full-name';
-		if ($this->exe->CanShowSizeInTree())
-			$args[] = '-l';
-		$args[] = '-t';
-		$args[] = $tree->GetHash();
-		
-		$lines = explode("\n", $this->exe->Execute($tree->GetProject()->GetPath(), GIT_LS_TREE, $args));
-
-		foreach ($lines as $line) {
-			if (preg_match("/^([0-9]+) (.+) ([0-9a-fA-F]{40})(\s+[0-9]+|\s+-)?\t(.+)$/", $line, $regs)) {
-				switch($regs[2]) {
-					case 'tree':
-						$data = array();
-						$data['type'] = 'tree';
-						$data['hash'] = $regs[3];
-						$data['mode'] = $regs[1];
-
-						$path = $regs[5];
-						if (!empty($treePath))
-							$path = $treePath . '/' . $path;
-						$data['path'] = $path;
-
-						$contents[] = $data;
-						break;
-					case 'blob':
-						$data = array();
-						$data['type'] = 'blob';
-						$data['hash'] = $regs[3];
-						$data['mode'] = $regs[1];
-
-						$path = $regs[5];
-						if (!empty($treePath))
-							$path = $treePath . '/' . $path;
-						$data['path'] = $path;
-
-						$size = trim($regs[4]);
-						if (!empty($size))
-							$data['size'] = $size;
-
-						$contents[] = $data;
-						break;
-				}
-			}
-		}
-
-		return $contents;
-	}
-}
-

--- a/include/git/TreeLoad_Raw.class.php
+++ /dev/null
@@ -1,91 +1,1 @@
-<?php
-/**
- * Tree load strategy using raw git objects
- *
- * @author Christopher Han <xiphux@gmail.com>
- * @copyright Copyright (c) 2012 Christopher Han
- * @package GitPHP
- * @subpackage Git
- */
-class GitPHP_TreeLoad_Raw extends GitPHP_TreeLoad_Base
-{
-	/**
-	 * Object loader
-	 *
-	 * @var GitPHP_GitObjectLoader
-	 */
-	protected $objectLoader;
 
-	/**
-	 * Constructor
-	 *
-	 * @param GitPHP_GitObjectLoader $objectLoader object loader
-	 * @param GitPHP_GitExe $exe git exe
-	 */
-	public function __construct($objectLoader, $exe)
-	{
-		if (!$objectLoader)
-			throw new Exception('Git object loader is required');
-
-		$this->objectLoader = $objectLoader;
-
-		parent::__construct($exe);
-	}
-
-	/**
-	 * Gets the data for a tree
-	 *
-	 * @param GitPHP_Tree $tree tree
-	 * @return array array of tree contents
-	 */
-	public function Load($tree)
-	{
-		if (!$tree)
-			return;
-
-		$contents = array();
-
-		$treePath = $tree->GetPath();
-
-		$treeData = $this->objectLoader->GetObject($tree->GetHash());
-
-		$start = 0;
-		$len = strlen($treeData);
-		while ($start < $len) {
-			$pos = strpos($treeData, "\0", $start);
-
-			list($mode, $path) = explode(' ', substr($treeData, $start, $pos-$start), 2);
-			$mode = str_pad($mode, 6, '0', STR_PAD_LEFT);
-			$hash = bin2hex(substr($treeData, $pos+1, 20));
-			$start = $pos + 21;
-
-			$octmode = octdec($mode);
-
-			if ($octmode == 57344) {
-				// submodules not currently supported
-				continue;
-			}
-
-			if (!empty($treePath))
-				$path = $treePath . '/' . $path;
-
-			$data = array();
-			$data['hash'] = $hash;
-			if ($octmode & 0x4000) {
-				// tree
-				$data['type'] = 'tree';
-			} else {
-				// blob
-				$data['type'] = 'blob';
-			}
-
-			$data['mode'] = $mode;
-			$data['path'] = $path;
-
-			$contents[] = $data;
-		}
-
-		return $contents;
-	}
-}
-

--- /dev/null
+++ b/include/git/blob/Blob.class.php
@@ -1,1 +1,297 @@
-
+<?php
+/**
+ * Represents a single blob
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2010 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_Blob extends GitPHP_FilesystemObject implements GitPHP_Observable_Interface, GitPHP_Cacheable_Interface
+{
+
+	/**
+	 * The blob data
+	 *
+	 * @var string
+	 */
+	protected $data;
+
+	/**
+	 * Whether data has been read
+	 *
+	 * @var boolean
+	 */
+	protected $dataRead = false;
+
+	/**
+	 * The blob size
+	 *
+	 * @var int
+	 */
+	protected $size = null;
+
+	/**
+	 * Whether data has been encoded for serialization
+	 *
+	 * @var boolean
+	 */
+	protected $dataEncoded = false;
+
+	/**
+	 * Observers
+	 *
+	 * @var array
+	 */
+	protected $observers = array();
+
+	/**
+	 * Data load strategy
+	 *
+	 * @var GitPHP_BlobLoadStrategy_Interface
+	 */
+	protected $strategy;
+
+	/**
+	 * Instantiates object
+	 *
+	 * @param GitPHP_Project $project the project
+	 * @param string $hash object hash
+	 * @param GitPHP_BlobLoadStrategy_Interface $strategy load strategy
+	 */
+	public function __construct($project, $hash, GitPHP_BlobLoadStrategy_Interface $strategy)
+	{
+		parent::__construct($project, $hash);
+
+		if (!$strategy)
+			throw new Exception('Blob load strategy is required');
+
+		$this->SetStrategy($strategy);
+	}
+
+	/**
+	 * Gets the blob data
+	 *
+	 * @param boolean $explode true to explode data into an array of lines
+	 * @return string|string[] blob data
+	 */
+	public function GetData($explode = false)
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if ($this->dataEncoded)
+			$this->DecodeData();
+
+		if ($explode)
+			return explode("\n", $this->data);
+		else
+			return $this->data;
+	}
+
+	/**
+	 * Set the load strategy
+	 *
+	 * @param GitPHP_BlobLoadStrategy_Interface $strategy load strategy
+	 */
+	public function SetStrategy(GitPHP_BlobLoadStrategy_Interface $strategy)
+	{
+		if (!$strategy)
+			return;
+
+		$this->strategy = $strategy;
+	}
+
+	/**
+	 * Reads the blob data
+	 */
+	private function ReadData()
+	{
+		$this->dataRead = true;
+
+		$this->data = $this->strategy->Load($this);
+
+		$this->dataEncoded = false;
+
+		foreach ($this->observers as $observer) {
+			$observer->ObjectChanged($this, GitPHP_Observer_Interface::CacheableDataChange);
+		}
+	}
+
+	/**
+	 * Gets a file type from its octal mode
+	 *
+	 * @param string $octMode octal mode
+	 * @param boolean $local true if caller wants localized type
+	 * @return string file type
+	 */
+	public static function FileType($octMode, $local = false)
+	{
+		$mode = octdec($octMode);
+		if (($mode & 0x4000) == 0x4000) {
+			if ($local) {
+				return __('directory');
+			} else {
+				return 'directory';
+			}
+		} else if (($mode & 0xA000) == 0xA000) {
+			if ($local) {
+				return __('symlink');
+			} else {
+				return 'symlink';
+			}
+		} else if (($mode & 0x8000) == 0x8000) {
+			if ($local) {
+				return __('file');
+			} else {
+				return 'file';
+			}
+		}
+
+		if ($local) {
+			return __('unknown');
+		} else {
+			return 'unknown';
+		}
+	}
+
+	/**
+	 * Gets the blob size
+	 *
+	 * @return integer size
+	 */
+	public function GetSize()
+	{
+		if ($this->size !== null) {
+			return $this->size;
+		}
+
+		return strlen($this->GetData());
+	}
+
+	/**
+	 * Sets the blob size
+	 *
+	 * @param integer $size size
+	 */
+	public function SetSize($size)
+	{
+		$this->size = $size;
+	}
+
+	/**
+	 * Tests if this blob is a binary file
+	 *
+	 * @return boolean true if binary file
+	 */
+	public function IsBinary()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		$data = $this->GetData();
+		if (strlen($data) > 8000)
+			$data = substr($data, 0, 8000);
+
+		return strpos($data, chr(0)) !== false;
+	}
+
+	/**
+	 * Encodes data so it can be serialized safely
+	 */
+	private function EncodeData()
+	{
+		if ($this->dataEncoded)
+			return;
+
+		$this->data = base64_encode($this->data);
+
+		$this->dataEncoded = true;
+	}
+
+	/**
+	 * Decodes data after unserialization
+	 */
+	private function DecodeData()
+	{
+		if (!$this->dataEncoded)
+			return;
+
+		$this->data = base64_decode($this->data);
+
+		$this->dataEncoded = false;
+	}
+
+	/**
+	 * Add a new observer
+	 *
+	 * @param GitPHP_Observer_Interface $observer observer
+	 */
+	public function AddObserver($observer)
+	{
+		if (!$observer)
+			return;
+
+		if (array_search($observer, $this->observers) !== false)
+			return;
+
+		$this->observers[] = $observer;
+	}
+
+	/**
+	 * Remove an observer
+	 *
+	 * @param GitPHP_Observer_Interface $observer observer
+	 */
+	public function RemoveObserver($observer)
+	{
+		if (!$observer)
+			return;
+
+		$key = array_search($observer, $this->observers);
+
+		if ($key === false)
+			return;
+
+		unset($this->observers[$key]);
+	}
+
+	/**
+	 * Called to prepare the object for serialization
+	 *
+	 * @return string[] list of properties to serialize
+	 */
+	public function __sleep()
+	{
+		if (!$this->dataEncoded)
+			$this->EncodeData();
+
+		$properties = array('data', 'dataRead', 'dataEncoded');
+
+		return array_merge($properties, parent::__sleep());
+	}
+
+	/**
+	 * Gets the cache key to use for this object
+	 *
+	 * @return string cache key
+	 */
+	public function GetCacheKey()
+	{
+		return GitPHP_Blob::CacheKey($this->project->GetProject(), $this->hash);
+	}
+
+	/**
+	 * Generates a blob cache key
+	 *
+	 * @param string $proj project
+	 * @param string $hash hash
+	 * @return string cache key
+	 */
+	public static function CacheKey($proj, $hash)
+	{
+		return 'project|' . $proj . '|blob|' . $hash;
+	}
+
+}
+

--- /dev/null
+++ b/include/git/blob/BlobLoadStrategy.interface.php
@@ -1,1 +1,20 @@
+<?php
+/**
+ * Interface for blob data load strategies
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+interface GitPHP_BlobLoadStrategy_Interface
+{
+	/**
+	 * Gets the data for a blob
+	 *
+	 * @param GitPHP_Blob $blob blob
+	 * @return string blob data
+	 */
+	public function Load($blob);
+}
 

--- /dev/null
+++ b/include/git/blob/BlobLoad_Git.class.php
@@ -1,1 +1,50 @@
+<?php
+/**
+ * Blob load strategy using git exe
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_BlobLoad_Git implements GitPHP_BlobLoadStrategy_Interface
+{
+	/**
+	 * Executable
+	 *
+	 * @var GitPHP_GitExe
+	 */
+	protected $exe;
 
+	/**
+	 * Constructor
+	 *
+	 * @param GitPHP_GitExe $exe executable
+	 */
+	public function __construct($exe)
+	{
+		if (!$exe)
+			throw new Exception('Git exe is required');
+
+		$this->exe = $exe;
+	}
+
+	/**
+	 * Gets the data for a blob
+	 *
+	 * @param GitPHP_Blob $blob blob
+	 * @return string blob data
+	 */
+	public function Load($blob)
+	{
+		if (!$blob)
+			return;
+
+		$args = array();
+		$args[] = 'blob';
+		$args[] = $blob->GetHash();
+
+		return $this->exe->Execute($blob->GetProject()->GetPath(), GIT_CAT_FILE, $args);
+	}
+}
+

--- /dev/null
+++ b/include/git/blob/BlobLoad_Raw.class.php
@@ -1,1 +1,46 @@
+<?php
+/**
+ * Blob load strategy using raw git objects
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_BlobLoad_Raw implements GitPHP_BlobLoadStrategy_Interface
+{
+	/**
+	 * Object loader
+	 *
+	 * @var GitPHP_GitObjectLoader
+	 */
+	protected $objectLoader;
 
+	/**
+	 * Constructor
+	 *
+	 * @param GitPHP_GitObjectLoader $objectLoader object loader
+	 */
+	public function __construct($objectLoader)
+	{
+		if (!$objectLoader)
+			throw new Exception('Git object loader is required');
+
+		$this->objectLoader = $objectLoader;
+	}
+
+	/**
+	 * Gets the data for a blob
+	 *
+	 * @param GitPHP_Blob $blob blob
+	 * @return string blob data
+	 */
+	public function Load($blob)
+	{
+		if (!$blob)
+			return;
+
+		return $this->objectLoader->GetObject($blob->GetHash());
+	}
+}
+

--- /dev/null
+++ b/include/git/commit/Commit.class.php
@@ -1,1 +1,653 @@
-
+<?php
+/**
+ * Represents a single commit
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2010 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_Commit extends GitPHP_GitObject implements GitPHP_Observable_Interface, GitPHP_Cacheable_Interface
+{
+
+	/**
+	 * Whether data for this commit has been read
+	 *
+	 * @var boolean
+	 */
+	protected $dataRead = false;
+
+	/**
+	 * Array of parent commits
+	 *
+	 * @var string[]
+	 */
+	protected $parents = array();
+
+	/**
+	 * Tree hash for this commit
+	 *
+	 * @var string
+	 */
+	protected $tree;
+
+	/**
+	 * Author for this commit
+	 *
+	 * @var string
+	 */
+	protected $author;
+
+	/**
+	 * Author's epoch
+	 *
+	 * @var string
+	 */
+	protected $authorEpoch;
+
+	/**
+	 * Author's timezone
+	 *
+	 * @var string
+	 */
+	protected $authorTimezone;
+
+	/**
+	 * Committer for this commit
+	 *
+	 * @var string
+	 */
+	protected $committer;
+
+	/**
+	 * Committer's epoch
+	 *
+	 * @var string
+	 */
+	protected $committerEpoch;
+
+	/**
+	 * Committer's timezone
+	 *
+	 * @var string
+	 */
+	protected $committerTimezone;
+
+	/**
+	 * The commit title
+	 *
+	 * @var string
+	 */
+	protected $title;
+
+	/**
+	 * The commit comment
+	 *
+	 * @var string
+	 */
+	protected $comment = array();
+
+	/**
+	 * Whether tree filenames have been read
+	 *
+	 * @var boolean
+	 */
+	protected $readTree = false;
+
+	/**
+	 * The tag containing the changes in this commit
+	 *
+	 * @var string
+	 */
+	protected $containingTag = null;
+
+	/**
+	 * Whether the containing tag has been looked up
+	 *
+	 * @var boolean
+	 */
+	protected $containingTagRead = false;
+
+	/**
+	 * Observers
+	 *
+	 * @var array
+	 */
+	protected $observers = array();
+
+	/**
+	 * Data load strategy
+	 *
+	 * @var GitPHP_CommitLoadStrategy_Interface
+	 */
+	protected $strategy;
+
+	/**
+	 * Instantiates object
+	 *
+	 * @param GitPHP_Project $project the project
+	 * @param string $hash object hash
+	 * @param GitPHP_CommitLoadStrategy_Interface $strategy load strategy
+	 */
+	public function __construct($project, $hash, GitPHP_CommitLoadStrategy_Interface $strategy)
+	{
+		parent::__construct($project, $hash);
+
+		if (!$strategy)
+			throw new Exception('Commit load strategy is required');
+
+		$this->SetStrategy($strategy);
+	}
+
+	/**
+	 * Set the load strategy
+	 *
+	 * @param GitPHP_CommitLoadStrategy_Interface $strategy load strategy
+	 */
+	public function SetStrategy(GitPHP_CommitLoadStrategy_Interface $strategy)
+	{
+		if (!$strategy)
+			return;
+
+		$this->strategy = $strategy;
+	}
+
+	/**
+	 * Gets the hash for this commit (overrides base)
+	 *
+	 * @param boolean $abbreviate true to abbreviate hash
+	 * @return string object hash
+	 */
+	public function GetHash($abbreviate = false)
+	{
+		if ($abbreviate && $this->strategy->LoadsAbbreviatedHash()) {
+			if (!$this->dataRead)
+				$this->ReadData();
+		}
+
+		return parent::GetHash($abbreviate);
+	}
+
+	/**
+	 * Gets the main parent of this commit
+	 *
+	 * @return GitPHP_Commit|null commit object for parent
+	 */
+	public function GetParent()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if (isset($this->parents[0])) {
+			return $this->GetProject()->GetCommit($this->parents[0]);
+		}
+
+		return null;
+	}
+
+	/**
+	 * Gets an array of parent objects for this commit
+	 *
+	 * @return GitPHP_Commit[] array of commit objects
+	 */
+	public function GetParents()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		$parents = array();
+		foreach ($this->parents as $parent) {
+			$parents[] = $this->GetProject()->GetCommit($parent);
+		}
+
+		return $parents;
+	}
+
+	/**
+	 * Gets the tree for this commit
+	 *
+	 * @return GitPHP_Tree tree object
+	 */
+	public function GetTree()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if (empty($this->tree))
+			return null;
+
+		$tree = $this->GetProject()->GetObjectManager()->GetTree($this->tree);
+		if ($tree) {
+			$tree->SetCommitHash($this->hash);
+			$tree->SetPath(null);
+		}
+
+		return $tree;
+	}
+
+	/**
+	 * Gets the author for this commit
+	 *
+	 * @return string author
+	 */
+	public function GetAuthor()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->author;
+	}
+
+	/**
+	 * Gets the author's name only
+	 *
+	 * @return string author name
+	 */
+	public function GetAuthorName()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return preg_replace('/ <.*/', '', $this->author);
+	}
+
+	/**
+	 * Gets the author's epoch
+	 *
+	 * @return string author epoch
+	 */
+	public function GetAuthorEpoch()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->authorEpoch;
+	}
+
+	/**
+	 * Gets the author's local epoch
+	 *
+	 * @return string author local epoch
+	 */
+	public function GetAuthorLocalEpoch()
+	{
+		$epoch = $this->GetAuthorEpoch();
+		$tz = $this->GetAuthorTimezone();
+		if (preg_match('/^([+\-][0-9][0-9])([0-9][0-9])$/', $tz, $regs)) {
+			$local = $epoch + ((((int)$regs[1]) + ($regs[2]/60)) * 3600);
+			return $local;
+		}
+		return $epoch;
+	}
+
+	/**
+	 * Gets the author's timezone
+	 *
+	 * @return string author timezone
+	 */
+	public function GetAuthorTimezone()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->authorTimezone;
+	}
+
+	/**
+	 * Gets the author for this commit
+	 *
+	 * @return string author
+	 */
+	public function GetCommitter()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->committer;
+	}
+
+	/**
+	 * Gets the author's name only
+	 *
+	 * @return string author name
+	 */
+	public function GetCommitterName()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return preg_replace('/ <.*/', '', $this->committer);
+	}
+
+	/**
+	 * Gets the committer's epoch
+	 *
+	 * @return string committer epoch
+	 */
+	public function GetCommitterEpoch()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->committerEpoch;
+	}
+
+	/**
+	 * Gets the committer's local epoch
+	 *
+	 * @return string committer local epoch
+	 */
+	public function GetCommitterLocalEpoch()
+	{
+		$epoch = $this->GetCommitterEpoch();
+		$tz = $this->GetCommitterTimezone();
+		if (preg_match('/^([+\-][0-9][0-9])([0-9][0-9])$/', $tz, $regs)) {
+			$local = $epoch + ((((int)$regs[1]) + ($regs[2]/60)) * 3600);
+			return $local;
+		}
+		return $epoch;
+	}
+
+	/**
+	 * Gets the author's timezone
+	 *
+	 * @return string author timezone
+	 */
+	public function GetCommitterTimezone()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->committerTimezone;
+	}
+
+	/**
+	 * Gets the commit title
+	 *
+	 * @param integer $trim length to trim to (0 for no trim)
+	 * @return string title
+	 */
+	public function GetTitle($trim = 0)
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if ($trim > 0) {
+			if (function_exists('mb_strimwidth')) {
+				return mb_strimwidth($this->title, 0, $trim, '…');
+			} else if (strlen($this->title) > $trim) {
+				return substr($this->title, 0, $trim) . '…';
+			}
+		}
+
+		return $this->title;
+	}
+
+	/**
+	 * Gets the lines of comment
+	 *
+	 * @return string[] lines of comment
+	 */
+	public function GetComment()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->comment;
+	}
+
+	/**
+	 * Gets the lines of the comment matching the given pattern
+	 *
+	 * @param string $pattern pattern to find
+	 * @return string[] matching lines of comment
+	 */
+	public function SearchComment($pattern)
+	{
+		if (empty($pattern))
+			return $this->GetComment();
+
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return preg_grep('/' . $pattern . '/i', $this->comment);
+	}
+
+	/**
+	 * Gets the age of the commit
+	 *
+	 * @return string age
+	 */
+	public function GetAge()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if (!empty($this->committerEpoch))
+			return time() - $this->committerEpoch;
+
+		return '';
+	}
+
+	/**
+	 * Returns whether this is a merge commit
+	 *
+	 * @return boolean true if merge commit
+	 */
+	public function IsMergeCommit()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return count($this->parents) > 1;
+	}
+
+	/**
+	 * Read the data for the commit
+	 */
+	protected function ReadData()
+	{
+		$this->dataRead = true;
+
+		list(
+			$abbreviatedHash,
+			$this->tree,
+			$this->parents,
+			$this->author,
+			$this->authorEpoch,
+			$this->authorTimezone,
+			$this->committer,
+			$this->committerEpoch,
+			$this->committerTimezone,
+			$this->title,
+			$this->comment
+		) = $this->strategy->Load($this);
+
+		if (!empty($abbreviatedHash)) {
+			$this->abbreviatedHash = $abbreviatedHash;
+			$this->abbreviatedHashLoaded = true;
+		}
+
+		foreach ($this->observers as $observer) {
+			$observer->ObjectChanged($this, GitPHP_Observer_Interface::CacheableDataChange);
+		}
+	}
+
+	/**
+	 * Gets heads that point to this commit
+	 * 
+	 * @return GitPHP_Head[] array of heads
+	 */
+	public function GetHeads()
+	{
+		$heads = array();
+
+		$projectRefs = $this->GetProject()->GetHeadList()->GetHeads();
+
+		foreach ($projectRefs as $ref) {
+			if ($ref->GetHash() == $this->hash) {
+				$heads[] = $ref;
+			}
+		}
+
+		return $heads;
+	}
+
+	/**
+	 * Gets tags that point to this commit
+	 *
+	 * @return GitPHP_Tag[] array of tags
+	 */
+	public function GetTags()
+	{
+		$tags = array();
+
+		$projectRefs = $this->GetProject()->GetTagList()->GetTags();
+
+		foreach ($projectRefs as $ref) {
+			if (($ref->GetType() == 'tag') || ($ref->GetType() == 'commit')) {
+				if ($ref->GetCommit()->GetHash() === $this->hash) {
+					$tags[] = $ref;
+				}
+			}
+		}
+
+		return $tags;
+	}
+
+	/**
+	 * Gets the tag that contains the changes in this commit
+	 *
+	 * @return GitPHP_Tag tag object
+	 */
+	public function GetContainingTag()
+	{
+		if (!$this->containingTagRead)
+			$this->ReadContainingTag();
+
+		if (empty($this->containingTag))
+			return null;
+
+		return $this->GetProject()->GetTagList()->GetTag($this->containingTag);
+	}
+
+	/**
+	 * Looks up the tag that contains the changes in this commit
+	 */
+	public function ReadContainingTag()
+	{
+		$this->containingTagRead = true;
+
+		$this->containingTag = $this->strategy->LoadContainingTag($this);
+	}
+
+	/**
+	 * Diffs this commit with its immediate parent
+	 *
+	 * @return GitPHP_TreeDiff Tree diff
+	 */
+	public function DiffToParent()
+	{
+		return new GitPHP_TreeDiff($this->GetProject(), $this->hash);
+	}
+
+	/**
+	 * Add a new observer
+	 *
+	 * @param GitPHP_Observer_Interface $observer observer
+	 */
+	public function AddObserver($observer)
+	{
+		if (!$observer)
+			return;
+
+		if (array_search($observer, $this->observers) !== false)
+			return;
+
+		$this->observers[] = $observer;
+	}
+
+	/**
+	 * Remove an observer
+	 *
+	 * @param GitPHP_Observer_Interface $observer observer
+	 */
+	public function RemoveObserver($observer)
+	{
+		if (!$observer)
+			return;
+
+		$key = array_search($observer, $this->observers);
+
+		if ($key === false)
+			return;
+
+		unset($this->observers[$key]);
+	}
+
+	/**
+	 * Called to prepare the object for serialization
+	 *
+	 * @return string[] list of properties to serialize
+	 */
+	public function __sleep()
+	{
+		$properties = array('dataRead', 'parents', 'tree', 'author', 'authorEpoch', 'authorTimezone', 'committer', 'committerEpoch', 'committerTimezone', 'title', 'comment', 'readTree');
+		return array_merge($properties, parent::__sleep());
+	}
+
+	/**
+	 * Gets the cache key to use for this object
+	 *
+	 * @return string cache key
+	 */
+	public function GetCacheKey()
+	{
+		return GitPHP_Commit::CacheKey($this->project->GetProject(), $this->hash);
+	}
+
+	/**
+	 * Compares two commits by age
+	 *
+	 * @param GitPHP_Commit $a first commit
+	 * @param GitPHP_Commit $b second commit
+	 * @return integer comparison result
+	 */
+	public static function CompareAge($a, $b)
+	{
+		if ($a->GetAge() === $b->GetAge()) {
+			// fall back on author epoch
+			return GitPHP_Commit::CompareAuthorEpoch($a, $b);
+		}
+		return ($a->GetAge() < $b->GetAge() ? -1 : 1);
+	}
+
+	/**
+	 * Compares two commits by author epoch
+	 *
+	 * @param GitPHP_Commit $a first commit
+	 * @param GitPHP_Commit $b second commit
+	 * @return integer comparison result
+	 */
+	public static function CompareAuthorEpoch($a, $b)
+	{
+		if ($a->GetAuthorEpoch() === $b->GetAuthorEpoch()) {
+			return 0;
+		}
+		return ($a->GetAuthorEpoch() > $b->GetAuthorEpoch() ? -1 : 1);
+	}
+
+	/**
+	 * Generates a commit cache key
+	 *
+	 * @param string $proj project
+	 * @param string $hash hash
+	 * @return string cache key
+	 */
+	public static function CacheKey($proj, $hash)
+	{
+		return 'project|' . $proj . '|commit|' . $hash;
+	}
+
+}
+

--- /dev/null
+++ b/include/git/commit/CommitLoadStrategy.interface.php
@@ -1,1 +1,35 @@
+<?php
+/**
+ * Interface for commit load data strategies
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+interface GitPHP_CommitLoadStrategy_Interface
+{
+	/**
+	 * Gets the data for a commit
+	 *
+	 * @param GitPHP_Commit $commit commit
+	 * @return array commit data
+	 */
+	public function Load($commit);
 
+	/**
+	 * Gets the containing tag for a commit
+	 *
+	 * @param GitPHP_Commit $commit commit
+	 * @return string containing tag
+	 */
+	public function LoadContainingTag($commit);
+
+	/**
+	 * Whether this load strategy loads the abbreviated hash
+	 *
+	 * @return boolean
+	 */
+	public function LoadsAbbreviatedHash();
+}
+

--- /dev/null
+++ b/include/git/commit/CommitLoad_Base.class.php
@@ -1,1 +1,57 @@
+<?php
+/**
+ * Base commit load strategy
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+abstract class GitPHP_CommitLoad_Base implements GitPHP_CommitLoadStrategy_Interface
+{
+	/**
+	 * Executable
+	 *
+	 * @var GitPHP_GitExe
+	 */
+	protected $exe;
 
+	/**
+	 * Constructor
+	 *
+	 * @param GitPHP_GitExe $exe executable
+	 */
+	public function __construct($exe)
+	{
+		if (!$exe)
+			throw new Exception('Git exe is required');
+
+		$this->exe = $exe;
+	}
+
+	/**
+	 * Gets the containing tag for a commit
+	 *
+	 * @param GitPHP_Commit $commit commit
+	 * @return string containing tag
+	 */
+	public function LoadContainingTag($commit)
+	{
+		if (!$commit)
+			return;
+
+		$args = array();
+		$args[] = '--tags';
+		$args[] = $commit->GetHash();
+		$revs = explode("\n", $this->exe->Execute($commit->GetProject()->GetPath(), GIT_NAME_REV, $args));
+
+		foreach ($revs as $revline) {
+			if (preg_match('/^([0-9a-fA-F]{40})\s+tags\/(.+)(\^[0-9]+|\~[0-9]+)$/', $revline, $regs)) {
+				if ($regs[1] == $commit->GetHash()) {
+					return $regs[2];
+				}
+			}
+		}
+	}
+}
+

--- /dev/null
+++ b/include/git/commit/CommitLoad_Git.class.php
@@ -1,1 +1,140 @@
+<?php
+/**
+ * Commit load strategy using git exe
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_CommitLoad_Git extends GitPHP_CommitLoad_Base
+{
+	/**
+	 * Gets the data for a commit
+	 *
+	 * @param GitPHP_Commit $commit commit
+	 * @return array commit data
+	 */
+	public function Load($commit)
+	{
+		if (!$commit)
+			return;
 
+		$abbreviatedHash = null;
+		$tree = null;
+		$parents = array();
+		$author = null;
+		$authorEpoch = null;
+		$authorTimezone = null;
+		$committer = null;
+		$committerEpoch = null;
+		$committerTimezone = null;
+		$title = null;
+		$comment = array();
+
+
+		/* get data from git_rev_list */
+		$args = array();
+		$args[] = '--header';
+		$args[] = '--parents';
+		$args[] = '--max-count=1';
+		$args[] = '--abbrev-commit';
+		$args[] = $commit->GetHash();
+		$ret = $this->exe->Execute($commit->GetProject()->GetPath(), GIT_REV_LIST, $args);
+
+		$lines = explode("\n", $ret);
+
+		if (!isset($lines[0]))
+			return;
+
+		/* In case we returned something unexpected */
+		$tok = strtok($lines[0], ' ');
+		if ((strlen($tok) == 0) || (substr_compare($commit->GetHash(), $tok, 0, strlen($tok)) !== 0)) {
+			return;
+		}
+		$abbreviatedHash = $tok;
+
+		array_shift($lines);
+
+
+		$linecount = count($lines);
+		$i = 0;
+		$encoding = null;
+
+		/* Commit header */
+		for ($i = 0; $i < $linecount; $i++) {
+			$line = $lines[$i];
+			if (preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
+				/* Tree */
+				$tree = $regs[1];
+			} else if (preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
+				/* Parent */
+				$parents[] = $regs[1];
+			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+				/* author data */
+				$author = $regs[1];
+				$authorEpoch = $regs[2];
+				$authorTimezone = $regs[3];
+			} else if (preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+				/* committer data */
+				$committer = $regs[1];
+				$committerEpoch = $regs[2];
+				$committerTimezone = $regs[3];
+			} else if (preg_match('/^encoding (.+)$/', $line, $regs)) {
+				$gitEncoding = trim($regs[1]);
+				if ((strlen($gitEncoding) > 0) && function_exists('mb_list_encodings')) {
+					$supportedEncodings = mb_list_encodings();
+					$encIdx = array_search(strtolower($gitEncoding), array_map('strtolower', $supportedEncodings));
+					if ($encIdx !== false) {
+						$encoding = $supportedEncodings[$encIdx];
+					}
+				}
+				$encoding = trim($regs[1]);
+			} else if (strlen($line) == 0) {
+				break;
+			}
+		}
+		
+		/* Commit body */
+		for ($i += 1; $i < $linecount; $i++) {
+			$trimmed = trim($lines[$i]);
+
+			if ((strlen($trimmed) > 0) && (strlen($encoding) > 0) && function_exists('mb_convert_encoding')) {
+				$trimmed = mb_convert_encoding($trimmed, 'UTF-8', $encoding);
+			}
+
+			if (empty($title) && (strlen($trimmed) > 0))
+				$title = $trimmed;
+			if (!empty($title)) {
+				if ((strlen($trimmed) > 0) || ($i < ($linecount-1)))
+					$comment[] = $trimmed;
+			}
+		}
+
+		return array(
+			$abbreviatedHash,
+			$tree,
+			$parents,
+			$author,
+			$authorEpoch,
+			$authorTimezone,
+			$committer,
+			$committerEpoch,
+			$committerTimezone,
+			$title,
+			$comment
+		);
+
+	}
+
+	/**
+	 * Whether this load strategy loads the abbreviated hash
+	 *
+	 * @return boolean
+	 */
+	public function LoadsAbbreviatedHash()
+	{
+		return true;
+	}
+}
+

--- /dev/null
+++ b/include/git/commit/CommitLoad_Raw.class.php
@@ -1,1 +1,144 @@
+<?php
+/**
+ * Commit load strategy using raw git objects
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_CommitLoad_Raw extends GitPHP_CommitLoad_Base
+{
+	/**
+	 * Object loader
+	 *
+	 * @var GitPHP_GitObjectLoader
+	 */
+	protected $objectLoader;
 
+	/**
+	 * Constructor
+	 *
+	 * @param GitPHP_GitObjectLoader $objectLoader object loader
+	 * @param GitPHP_GitExe $exe git exe
+	 */
+	public function __construct($objectLoader, $exe)
+	{
+		if (!$objectLoader)
+			throw new Exception('Git object loader is required');
+
+		$this->objectLoader = $objectLoader;
+
+		parent::__construct($exe);
+	}
+
+	/**
+	 * Gets the data for a commit
+	 *
+	 * @param GitPHP_Commit $commit commit
+	 * @return array commit data
+	 */
+	public function Load($commit)
+	{
+		if (!$commit)
+			return;
+
+		$abbreviatedHash = null;
+		$tree = null;
+		$parents = array();
+		$author = null;
+		$authorEpoch = null;
+		$authorTimezone = null;
+		$committer = null;
+		$committerEpoch = null;
+		$committerTimezone = null;
+		$title = null;
+		$comment = array();
+
+		$data = $this->objectLoader->GetObject($commit->GetHash());
+		if (empty($data))
+			return;
+
+		$lines = explode("\n", $data);
+
+		$linecount = count($lines);
+		$i = 0;
+		$encoding = null;
+
+		/* Commit header */
+		for ($i = 0; $i < $linecount; $i++) {
+			$line = $lines[$i];
+			if (preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
+				/* Tree */
+				$tree = $regs[1];
+			} else if (preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
+				/* Parent */
+				$parents[] = $regs[1];
+			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+				/* author data */
+				$author = $regs[1];
+				$authorEpoch = $regs[2];
+				$authorTimezone = $regs[3];
+			} else if (preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+				/* committer data */
+				$committer = $regs[1];
+				$committerEpoch = $regs[2];
+				$committerTimezone = $regs[3];
+			} else if (preg_match('/^encoding (.+)$/', $line, $regs)) {
+				$gitEncoding = trim($regs[1]);
+				if ((strlen($gitEncoding) > 0) && function_exists('mb_list_encodings')) {
+					$supportedEncodings = mb_list_encodings();
+					$encIdx = array_search(strtolower($gitEncoding), array_map('strtolower', $supportedEncodings));
+					if ($encIdx !== false) {
+						$encoding = $supportedEncodings[$encIdx];
+					}
+				}
+				$encoding = trim($regs[1]);
+			} else if (strlen($line) == 0) {
+				break;
+			}
+		}
+		
+		/* Commit body */
+		for ($i += 1; $i < $linecount; $i++) {
+			$trimmed = trim($lines[$i]);
+
+			if ((strlen($trimmed) > 0) && (strlen($encoding) > 0) && function_exists('mb_convert_encoding')) {
+				$trimmed = mb_convert_encoding($trimmed, 'UTF-8', $encoding);
+			}
+
+			if (empty($title) && (strlen($trimmed) > 0))
+				$title = $trimmed;
+			if (!empty($title)) {
+				if ((strlen($trimmed) > 0) || ($i < ($linecount-1)))
+					$comment[] = $trimmed;
+			}
+		}
+
+		return array(
+			$abbreviatedHash,
+			$tree,
+			$parents,
+			$author,
+			$authorEpoch,
+			$authorTimezone,
+			$committer,
+			$committerEpoch,
+			$committerTimezone,
+			$title,
+			$comment
+		);
+
+	}
+
+	/**
+	 * Whether this load strategy loads the abbreviated hash
+	 *
+	 * @return boolean
+	 */
+	public function LoadsAbbreviatedHash()
+	{
+		return false;
+	}
+}
+

--- /dev/null
+++ b/include/git/tag/Tag.class.php
@@ -1,1 +1,454 @@
-
+<?php
+/**
+ * Represents a single tag object
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2010 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_Tag extends GitPHP_Ref implements GitPHP_Observable_Interface, GitPHP_Cacheable_Interface
+{
+	
+	/**
+	 * Whether data for this tag has been read
+	 *
+	 * @var boolean
+	 */
+	protected $dataRead = false;
+
+	/**
+	 * The identifier for the tagged object
+	 *
+	 * @var string
+	 */
+	protected $object;
+
+	/**
+	 * The commit hash
+	 *
+	 * @var string
+	 */
+	protected $commitHash;
+
+	/**
+	 * The tagged object type
+	 *
+	 * @var string
+	 */
+	protected $type;
+
+	/**
+	 * The tagger
+	 *
+	 * @var string
+	 */
+	protected $tagger;
+
+	/**
+	 * The tagger epoch
+	 *
+	 * @var string
+	 */
+	protected $taggerEpoch;
+
+	/**
+	 * The tagger timezone
+	 *
+	 * @var string
+	 */
+	protected $taggerTimezone;
+
+	/**
+	 * The tag comment
+	 *
+	 * @var string
+	 */
+	protected $comment = array();
+
+	/**
+	 * Observers
+	 *
+	 * @var array
+	 */
+	protected $observers = array();
+
+	/**
+	 * Data load strategy
+	 *
+	 * @var GitPHP_TagLoadStrategy_Interface
+	 */
+	protected $strategy;
+
+	/**
+	 * Instantiates tag
+	 *
+	 * @param GitPHP_Project $project the project
+	 * @param string $tag tag name
+	 * @param GitPHP_TagLoadStrategy_Interface $strategy load strategy
+	 * @param string $tagHash tag hash
+	 */
+	public function __construct($project, $tag, GitPHP_TagLoadStrategy_Interface $strategy, $tagHash = '')
+	{
+		parent::__construct($project, 'tags', $tag, $tagHash);
+
+		if (!$strategy)
+			throw new Exception('Tag load strategy is required');
+
+		$this->SetStrategy($strategy);
+	}
+
+	/**
+	 * Set the load strategy
+	 *
+	 * @param GitPHP_TagLoadStrategy_Interface $strategy load strategy
+	 */
+	public function SetStrategy(GitPHP_TagLoadStrategy_Interface $strategy)
+	{
+		if (!$strategy)
+			return;
+
+		$this->strategy = $strategy;
+	}
+
+	/**
+	 * Gets the object this tag points to
+	 *
+	 * @return GitPHP_Commit|GitPHP_Tag|GitPHP_Blob object for this tag
+	 */
+	public function GetObject()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if ($this->type == 'commit') {
+			return $this->GetProject()->GetCommit($this->object);
+		} else if ($this->type == 'tag') {
+			return $this->GetProject()->GetTagList()->GetTag($this->object);
+		} else if ($this->type == 'blob') {
+			return $this->GetProject()->GetObjectManager()->GetBlob($this->object);
+		}
+
+		return null;
+	}
+
+	/**
+	 * Gets the commit this tag points to
+	 *
+	 * @return GitPHP_Commit commit for this tag
+	 */
+	public function GetCommit()
+	{
+		if ($this->commitHash)
+			return $this->GetProject()->GetCommit($this->commitHash);
+
+		if (!$this->dataRead) {
+			$this->ReadData();
+		}
+
+		if (!$this->commitHash) {
+			if ($this->type == 'commit') {
+				$this->commitHash = $this->object;
+			} else if ($this->type == 'tag') {
+				$tag = $this->GetProject()->GetTagList()->GetTag($this->object);
+				$this->commitHash = $tag->GetCommit()->GetHash();
+			}
+		}
+
+		return $this->GetProject()->GetCommit($this->commitHash);
+	}
+
+	/**
+	 * Sets the commit this tag points to
+	 *
+	 * @param GitPHP_Commit $commit commit object 
+	 */
+	public function SetCommit($commit)
+	{
+		if (!$commit)
+			return;
+
+		$this->SetCommitHash($commit->GetHash());
+	}
+
+	/**
+	 * Sets the hash of the commit this tag points to
+	 *
+	 * @param string $hash hash
+	 */
+	public function SetCommitHash($hash)
+	{
+		if (!preg_match('/^[0-9A-Fa-f]{40}$/', $hash))
+			return;
+
+		if (!$this->commitHash)
+			$this->commitHash = $hash;
+	}
+
+	/**
+	 * Gets the tag type
+	 *
+	 * @return string tag type
+	 */
+	public function GetType()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->type;
+	}
+
+	/**
+	 * Gets the tagger
+	 *
+	 * @return string tagger
+	 */
+	public function GetTagger()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->tagger;
+	}
+
+	/**
+	 * Gets the tagger epoch
+	 *
+	 * @return string tagger epoch
+	 */
+	public function GetTaggerEpoch()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->taggerEpoch;
+	}
+
+	/**
+	 * Gets the tagger local epoch
+	 *
+	 * @return string tagger local epoch
+	 */
+	public function GetTaggerLocalEpoch()
+	{
+		$epoch = $this->GetTaggerEpoch();
+		$tz = $this->GetTaggerTimezone();
+		if (preg_match('/^([+\-][0-9][0-9])([0-9][0-9])$/', $tz, $regs)) {
+			$local = $epoch + ((((int)$regs[1]) + ($regs[2]/60)) * 3600);
+			return $local;
+		}
+		return $epoch;
+	}
+
+	/**
+	 * Gets the tagger timezone
+	 *
+	 * @return string tagger timezone
+	 */
+	public function GetTaggerTimezone()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->taggerTimezone;
+	}
+
+	/**
+	 * Gets the tag age
+	 *
+	 * @return string age
+	 */
+	public function GetAge()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return time() - $this->taggerEpoch;
+	}
+
+	/**
+	 * Gets the tag comment
+	 *
+	 * @return string[] comment lines
+	 */
+	public function GetComment()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return $this->comment;
+	}
+
+	/**
+	 * Tests if this is a light tag (tag without tag object)
+	 *
+	 * @return boolean true if tag is light (has no object)
+	 */
+	public function LightTag()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if (!$this->object)
+			return true;
+
+		if (($this->type == 'commit') && ($this->object == $this->GetHash())) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Reads the tag data
+	 */
+	protected function ReadData()
+	{
+		$this->dataRead = true;
+
+		list(
+			$this->type,
+			$this->object,
+			$commitHash,
+			$this->tagger,
+			$this->taggerEpoch,
+			$this->taggerTimezone,
+			$this->comment
+		) = $this->strategy->Load($this);
+
+		if (!empty($commitHash))
+			$this->commitHash = $commitHash;
+
+		foreach ($this->observers as $observer) {
+			$observer->ObjectChanged($this, GitPHP_Observer_Interface::CacheableDataChange);
+		}
+	}
+
+	/**
+	 * Add a new observer
+	 *
+	 * @param GitPHP_Observer_Interface $observer observer
+	 */
+	public function AddObserver($observer)
+	{
+		if (!$observer)
+			return;
+
+		if (array_search($observer, $this->observers) !== false)
+			return;
+
+		$this->observers[] = $observer;
+	}
+
+	/**
+	 * Remove an observer
+	 *
+	 * @param GitPHP_Observer_Interface $observer observer
+	 */
+	public function RemoveObserver($observer)
+	{
+		if (!$observer)
+			return;
+
+		$key = array_search($observer, $this->observers);
+
+		if ($key === false)
+			return;
+
+		unset($this->observers[$key]);
+	}
+
+	/**
+	 * Called to prepare the object for serialization
+	 *
+	 * @return string[] list of properties to serialize
+	 */
+	public function __sleep()
+	{
+		$properties = array('dataRead', 'object', 'commitHash', 'type', 'tagger', 'taggerEpoch', 'taggerTimezone', 'comment');
+		return array_merge($properties, parent::__sleep());
+	}
+
+	/**
+	 * Gets the cache key to use for this object
+	 *
+	 * @return string cache key
+	 */
+	public function GetCacheKey()
+	{
+		return GitPHP_Tag::CacheKey($this->project->GetProject(), $this->refName);
+	}
+
+	/**
+	 * Gets tag's creation epoch (tagger epoch, or committer epoch for light tags)
+	 *
+	 * @return string creation epoch
+	 */
+	public function GetCreationEpoch()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if ($this->LightTag())
+			return $this->GetCommit()->GetCommitterEpoch();
+		else
+			return $this->taggerEpoch;
+	}
+
+	/**
+	 * Compares two tags by age
+	 *
+	 * @param GitPHP_Tag $a first tag
+	 * @param GitPHP_Tag $b second tag
+	 * @return integer comparison result
+	 */
+	public static function CompareAge($a, $b)
+	{
+		$aObj = $a->GetObject();
+		$bObj = $b->GetObject();
+		if (($aObj instanceof GitPHP_Commit) && ($bObj instanceof GitPHP_Commit)) {
+			return GitPHP_Commit::CompareAge($aObj, $bObj);
+		}
+
+		if ($aObj instanceof GitPHP_Commit)
+			return 1;
+
+		if ($bObj instanceof GitPHP_Commit)
+			return -1;
+
+		return strcmp($a->GetName(), $b->GetName());
+	}
+
+	/**
+	 * Compares to tags by creation epoch
+	 *
+	 * @param GitPHP_Tag $a first tag
+	 * @param GitPHP_Tag $b second tag
+	 * @return integer comparison result
+	 */
+	public static function CompareCreationEpoch($a, $b)
+	{
+		$aEpoch = $a->GetCreationEpoch();
+		$bEpoch = $b->GetCreationEpoch();
+
+		if ($aEpoch == $bEpoch) {
+			return 0;
+		}
+
+		return ($aEpoch < $bEpoch ? 1 : -1);
+	}
+
+	/**
+	 * Generates a tag cache key
+	 *
+	 * @param string $proj project
+	 * @param string $tag tag name
+	 * @return string cache key
+	 */
+	public static function CacheKey($proj, $tag)
+	{
+		return 'project|' . $proj . '|tag|' . $tag;
+	}
+
+}
+

--- /dev/null
+++ b/include/git/tag/TagLoadStrategy.interface.php
@@ -1,1 +1,20 @@
+<?php
+/**
+ * Interface for tag data load strategies
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+interface GitPHP_TagLoadStrategy_Interface
+{
+	/**
+	 * Gets the data for a tag
+	 *
+	 * @param GitPHP_Tag $tag tag
+	 * @return array array of tag data
+	 */
+	public function Load($tag);
+}
 

--- /dev/null
+++ b/include/git/tag/TagLoad_Git.class.php
@@ -1,1 +1,149 @@
+<?php
+/**
+ * Tag load strategy using git exe
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_TagLoad_Git implements GitPHP_TagLoadStrategy_Interface
+{
+	/**
+	 * Executable
+	 *
+	 * @var GitPHP_GitExe
+	 */
+	protected $exe;
 
+	/**
+	 * Constructor
+	 *
+	 * @param GitPHP_GitExe $exe executable
+	 */
+	public function __construct($exe)
+	{
+		if (!$exe)
+			throw new Exception('Git exe is required');
+
+		$this->exe = $exe;
+	}
+
+	/**
+	 * Gets the data for a tag
+	 *
+	 * @param GitPHP_Tag $tag tag
+	 * @return array array of tag data
+	 */
+	public function Load($tag)
+	{
+		if (!$tag)
+			return;
+
+		$type = null;
+		$object = null;
+		$commitHash = null;
+		$tagger = null;
+		$taggerEpoch = null;
+		$taggerTimezone = null;
+		$comment = array();
+
+
+		$args = array();
+		$args[] = '-t';
+		$args[] = $tag->GetHash();
+		$ret = trim($this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args));
+		
+		if ($ret === 'commit') {
+			/* light tag */
+			$object = $tag->GetHash();
+			$commitHash = $tag->GetHash();
+			$type = 'commit';
+			return array(
+				$type,
+				$object,
+				$commitHash,
+				$tagger,
+				$taggerEpoch,
+				$taggerTimezone,
+				$comment
+			);
+		}
+
+		/* get data from tag object */
+		$args = array();
+		$args[] = 'tag';
+		$args[] = $tag->GetName();
+		$ret = $this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args);
+
+		$lines = explode("\n", $ret);
+
+		if (!isset($lines[0]))
+			return;
+
+		$objectHash = null;
+
+		$readInitialData = false;
+		foreach ($lines as $i => $line) {
+			if (!$readInitialData) {
+				if (preg_match('/^object ([0-9a-fA-F]{40})$/', $line, $regs)) {
+					$objectHash = $regs[1];
+					continue;
+				} else if (preg_match('/^type (.+)$/', $line, $regs)) {
+					$type = $regs[1];
+					continue;
+				} else if (preg_match('/^tag (.+)$/', $line, $regs)) {
+					continue;
+				} else if (preg_match('/^tagger (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+					$tagger = $regs[1];
+					$taggerEpoch = $regs[2];
+					$taggerTimezone = $regs[3];
+					continue;
+				}
+			}
+
+			$trimmed = trim($line);
+
+			if ((strlen($trimmed) > 0) || ($readInitialData === true)) {
+				$comment[] = $line;
+			}
+			$readInitialData = true;
+
+		}
+
+		switch ($type) {
+			case 'commit':
+				$object = $objectHash;
+				$commitHash = $objectHash;
+				break;
+			case 'tag':
+				$args = array();
+				$args[] = 'tag';
+				$args[] = $objectHash;
+				$ret = $this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args);
+				$lines = explode("\n", $ret);
+				foreach ($lines as $i => $line) {
+					if (preg_match('/^tag (.+)$/', $line, $regs)) {
+						$name = trim($regs[1]);
+						$object = $name;
+					}
+				}
+				break;
+			case 'blob':
+				$object = $objectHash;
+				break;
+		}
+
+		return array(
+			$type,
+			$object,
+			$commitHash,
+			$tagger,
+			$taggerEpoch,
+			$taggerTimezone,
+			$comment
+		);
+
+	}
+}
+

--- /dev/null
+++ b/include/git/tag/TagLoad_Raw.class.php
@@ -1,1 +1,134 @@
+<?php
+/**
+ * Tag load strategy using raw git objects
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_TagLoad_Raw implements GitPHP_TagLoadStrategy_Interface
+{
+	/**
+	 * Object loader
+	 *
+	 * @var GitPHP_GitObjectLoader
+	 */
+	protected $objectLoader;
 
+	/**
+	 * Constructor
+	 *
+	 * @param GitPHP_GitObjectLoader $objectLoader object loader
+	 */
+	public function __construct($objectLoader)
+	{
+		if (!$objectLoader)
+			throw new Exception('Object loader is required');
+
+		$this->objectLoader = $objectLoader;
+	}
+
+	/**
+	 * Gets the data for a tag
+	 *
+	 * @param GitPHP_Tag $tag tag
+	 * @return array array of tag data
+	 */
+	public function Load($tag)
+	{
+		if (!$tag)
+			return;
+
+		$type = null;
+		$object = null;
+		$commitHash = null;
+		$tagger = null;
+		$taggerEpoch = null;
+		$taggerTimezone = null;
+		$comment = array();
+
+		$data = $this->objectLoader->GetObject($tag->GetHash(), $packedType);
+		
+		if ($packedType == GitPHP_Pack::OBJ_COMMIT) {
+			/* light tag */
+			$object = $tag->GetHash();
+			$commitHash = $tag->GetHash();
+			$type = 'commit';
+			return array(
+				$type,
+				$object,
+				$commitHash,
+				$tagger,
+				$taggerEpoch,
+				$taggerTimezone,
+				$comment
+			);
+		}
+
+		$lines = explode("\n", $data);
+
+		if (!isset($lines[0]))
+			return;
+
+		$objectHash = null;
+
+		$readInitialData = false;
+		foreach ($lines as $i => $line) {
+			if (!$readInitialData) {
+				if (preg_match('/^object ([0-9a-fA-F]{40})$/', $line, $regs)) {
+					$objectHash = $regs[1];
+					continue;
+				} else if (preg_match('/^type (.+)$/', $line, $regs)) {
+					$type = $regs[1];
+					continue;
+				} else if (preg_match('/^tag (.+)$/', $line, $regs)) {
+					continue;
+				} else if (preg_match('/^tagger (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+					$tagger = $regs[1];
+					$taggerEpoch = $regs[2];
+					$taggerTimezone = $regs[3];
+					continue;
+				}
+			}
+
+			$trimmed = trim($line);
+
+			if ((strlen($trimmed) > 0) || ($readInitialData === true)) {
+				$comment[] = $line;
+			}
+			$readInitialData = true;
+		}
+
+		switch ($type) {
+			case 'commit':
+				$object = $objectHash;
+				$commitHash = $objectHash;
+				break;
+			case 'tag':
+				$objectData = $this->objectLoader->GetObject($objectHash);
+				$lines = explode("\n", $objectData);
+				foreach ($lines as $i => $line) {
+					if (preg_match('/^tag (.+)$/', $line, $regs)) {
+						$name = trim($regs[1]);
+						$object = $name;
+					}
+				}
+				break;
+			case 'blob':
+				$object = $objectHash;
+				break;
+		}
+
+		return array(
+			$type,
+			$object,
+			$commitHash,
+			$tagger,
+			$taggerEpoch,
+			$taggerTimezone,
+			$comment
+		);
+	}
+}
+

--- /dev/null
+++ b/include/git/tree/Tree.class.php
@@ -1,1 +1,310 @@
-
+<?php
+/**
+ * Represents a single tree
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2010 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_Tree extends GitPHP_FilesystemObject implements GitPHP_Observable_Interface, GitPHP_Cacheable_Interface
+{
+
+	/**
+	 * Tree contents
+	 *
+	 * @var array
+	 */
+	protected $contents = array();
+
+	/**
+	 * Whether contents were read
+	 *
+	 * @var boolean
+	 */
+	protected $contentsRead = false;
+
+	/**
+	 * Tree hash to path mappings
+	 *
+	 * @var array
+	 */
+	protected $treePaths = array();
+
+	/**
+	 * Blob hash to path mappings
+	 *
+	 * @var array
+	 */
+	protected $blobPaths = array();
+
+	/**
+	 * Whether hash paths have been read
+	 */
+	protected $hashPathsRead = false;
+
+	/**
+	 * Observers
+	 *
+	 * @var array
+	 */
+	protected $observers = array();
+
+	/**
+	 * Data load strategy
+	 *
+	 * @var GitPHP_TreeLoadStrategy_Interface
+	 */
+	protected $strategy;
+
+	/**
+	 * Instantiates object
+	 *
+	 * @param GitPHP_Project $project the project
+	 * @param string $hash tree hash
+	 * @param GitPHP_TreeLoadStrategy_Interface $strategy load strategy
+	 */
+	public function __construct($project, $hash, $strategy)
+	{
+		parent::__construct($project, $hash);
+
+		if (!$strategy)
+			throw new Exception('Tree load strategy is required');
+
+		$this->SetStrategy($strategy);
+	}
+
+	/**
+	 * Set the load strategy
+	 *
+	 * @param GitPHP_TreeLoadStrategy_Interface $strategy load strategy
+	 */
+	public function SetStrategy(GitPHP_TreeLoadStrategy_Interface $strategy)
+	{
+		if (!$strategy)
+			return;
+
+		$this->strategy = $strategy;
+	}
+
+	/**
+	 * Sets the object path (overrides base)
+	 *
+	 * @param string $path object path
+	 */
+	public function SetPath($path)
+	{
+		if ($this->path == $path)
+			return;
+
+		if ($this->hashPathsRead) {
+			$this->treePaths = array();
+			$this->blobPaths = array();
+			$this->hashPathsRead = false;
+		}
+
+		$this->path = $path;
+	}
+
+	/**
+	 * Gets the tree contents
+	 *
+	 * @return (GitPHP_Tree|GitPHP_Blob)[] array of objects for contents
+	 */
+	public function GetContents()
+	{
+		if (!$this->contentsRead)
+			$this->ReadContents();
+
+		$contents = array();
+		$usedTrees = array();
+		$usedBlobs = array();
+
+		for ($i = 0; $i < count($this->contents); ++$i) {
+			$data = $this->contents[$i];
+			$obj = null;
+
+			if (!isset($data['hash']) || empty($data['hash']))
+				continue;
+
+			if ($data['type'] == 'tree') {
+				$obj = $this->GetProject()->GetObjectManager()->GetTree($data['hash']);
+				if (isset($usedTrees[$data['hash']])) {
+					$obj = clone $obj;
+				} else {
+					$usedTrees[$data['hash']] = 1;
+				}
+			} else if ($data['type'] == 'blob') {
+				$obj = $this->GetProject()->GetObjectManager()->GetBlob($data['hash']);
+				if (isset($usedBlobs[$data['hash']])) {
+					$obj = clone $obj;
+				} else {
+					$usedBlobs[$data['hash']] = 1;
+				}
+
+				if (isset($data['size']) && !empty($data['size'])) {
+					$obj->SetSize($data['size']);
+				}
+			} else {
+				continue;
+			}
+
+			if (isset($data['mode']) && !empty($data['mode']))
+				$obj->SetMode($data['mode']);
+
+			if (isset($data['path']) && !empty($data['path']))
+				$obj->SetPath($data['path']);
+
+			if ($this->commitHash)
+				$obj->SetCommitHash($this->commitHash);
+
+			$contents[] = $obj;
+		}
+
+		return $contents;
+	}
+
+	/**
+	 * Reads the tree contents
+	 */
+	protected function ReadContents()
+	{
+		$this->contentsRead = true;
+
+		$this->contents = $this->strategy->Load($this);
+
+		foreach ($this->observers as $observer) {
+			$observer->ObjectChanged($this, GitPHP_Observer_Interface::CacheableDataChange);
+		}
+	}
+
+	/**
+	 * Gets tree paths mapped to hashes
+	 *
+	 * @return array
+	 */
+	public function GetTreePaths()
+	{
+		if (!$this->hashPathsRead)
+			$this->ReadHashPaths();
+
+		return $this->treePaths;
+	}
+
+	/**
+	 * Gets blob paths mapped to hashes
+	 *
+	 * @return array
+	 */
+	public function GetBlobPaths()
+	{
+		if (!$this->hashPathsRead)
+			$this->ReadHashPaths();
+
+		return $this->blobPaths;
+	}
+
+	/**
+	 * Given a filepath, get its hash
+	 *
+	 * @param string $path path
+	 * @return string hash
+	 */
+	public function PathToHash($path)
+	{
+		if (empty($path))
+			return '';
+
+		if (!$this->hashPathsRead)
+			$this->ReadHashPaths();
+
+		if (isset($this->blobPaths[$path])) {
+			return $this->blobPaths[$path];
+		}
+
+		if (isset($this->treePaths[$path])) {
+			return $this->treePaths[$path];
+		}
+
+		return '';
+	}
+
+	/**
+	 * Read hash to path mappings
+	 */
+	private function ReadHashPaths()
+	{
+		$this->hashPathsRead = true;
+
+		list($this->treePaths, $this->blobPaths) = $this->strategy->LoadHashPaths($this);
+	}
+
+	/**
+	 * Add a new observer
+	 *
+	 * @param GitPHP_Observer_Interface $observer observer
+	 */
+	public function AddObserver($observer)
+	{
+		if (!$observer)
+			return;
+
+		if (array_search($observer, $this->observers) !== false)
+			return;
+
+		$this->observers[] = $observer;
+	}
+
+	/**
+	 * Remove an observer
+	 *
+	 * @param GitPHP_Observer_Interface $observer observer
+	 */
+	public function RemoveObserver($observer)
+	{
+		if (!$observer)
+			return;
+
+		$key = array_search($observer, $this->observers);
+
+		if ($key === false)
+			return;
+
+		unset($this->observers[$key]);
+	}
+
+	/**
+	 * Called to prepare the object for serialization
+	 *
+	 * @return string[] list of properties to serialize
+	 */
+	public function __sleep()
+	{
+		$properties = array('contents', 'contentsRead');
+		return array_merge($properties, parent::__sleep());
+	}
+
+	/**
+	 * Gets the cache key to use for this object
+	 *
+	 * @return string cache key
+	 */
+	public function GetCacheKey()
+	{
+		return GitPHP_Tree::CacheKey($this->project->GetProject(), $this->hash);
+	}
+
+	/**
+	 * Generates a tree cache key
+	 *
+	 * @param string $proj project
+	 * @param string $hash hash
+	 * @return string cache key
+	 */
+	public static function CacheKey($proj, $hash)
+	{
+		return 'project|' . $proj . '|tree|' . $hash;
+	}
+
+}
+

--- /dev/null
+++ b/include/git/tree/TreeLoadStrategy.interface.php
@@ -1,1 +1,28 @@
+<?php
+/**
+ * Interface for tree data load strategies
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+interface GitPHP_TreeLoadStrategy_Interface
+{
+	/**
+	 * Gets the data for a tree
+	 *
+	 * @param GitPHP_Tree $tree tree
+	 * @return array array of tree contents
+	 */
+	public function Load($tree);
 
+	/**
+	 * Gets the hash paths for a tree
+	 *
+	 * @param GitPHP_Tree $tree tre
+	 * @return array array of treepath and hashpath arrays
+	 */
+	public function LoadHashPaths($tree);
+}
+

--- /dev/null
+++ b/include/git/tree/TreeLoad_Base.class.php
@@ -1,1 +1,73 @@
+<?php
+/**
+ * Base tree load strategy
+ *
+ * @author CHristopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+abstract class GitPHP_TreeLoad_Base implements GitPHP_TreeLoadStrategy_Interface
+{
+	/**
+	 * Executable
+	 *
+	 * @var GitPHP_GitExe
+	 */
+	protected $exe;
 
+	/**
+	 * Constructor
+	 *
+	 * @param GitPHP_GitExe $exe executable
+	 */
+	public function __construct($exe)
+	{
+		if (!$exe)
+			throw new Exception('Git exe is required');
+
+		$this->exe = $exe;
+	}
+
+	/**
+	 * Gets the hash paths for a tree
+	 *
+	 * @param GitPHP_Tree $tree tre
+	 * @return array array of treepath and hashpath arrays
+	 */
+	public function LoadHashPaths($tree)
+	{
+		if (!$tree)
+			return;
+
+		$treePaths = array();
+		$blobPaths = array();
+
+		$args = array();
+		$args[] = '--full-name';
+		$args[] = '-r';
+		$args[] = '-t';
+		$args[] = $tree->GetHash();
+
+		$lines = explode("\n", $this->exe->Execute($tree->GetProject()->GetPath(), GIT_LS_TREE, $args));
+
+		foreach ($lines as $line) {
+			if (preg_match("/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/", $line, $regs)) {
+				switch ($regs[2]) {
+					case 'tree':
+						$treePaths[trim($regs[4])] = $regs[3];
+						break;
+					case 'blob';
+						$blobPaths[trim($regs[4])] = $regs[3];
+						break;
+				}
+			}
+		}
+
+		return array(
+			$treePaths,
+			$blobPaths
+		);
+	}
+}
+

--- /dev/null
+++ b/include/git/tree/TreeLoad_Git.class.php
@@ -1,1 +1,76 @@
+<?php
+/**
+ * Tree load strategy using git exe
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_TreeLoad_Git extends GitPHP_TreeLoad_Base
+{
+	/**
+	 * Gets the data for a tree
+	 *
+	 * @param GitPHP_Tree $tree tree
+	 * @return array array of tree contents
+	 */
+	public function Load($tree)
+	{
+		if (!$tree)
+			return;
 
+		$contents = array();
+
+		$treePath = $tree->GetPath();
+
+		$args = array();
+		$args[] = '--full-name';
+		if ($this->exe->CanShowSizeInTree())
+			$args[] = '-l';
+		$args[] = '-t';
+		$args[] = $tree->GetHash();
+		
+		$lines = explode("\n", $this->exe->Execute($tree->GetProject()->GetPath(), GIT_LS_TREE, $args));
+
+		foreach ($lines as $line) {
+			if (preg_match("/^([0-9]+) (.+) ([0-9a-fA-F]{40})(\s+[0-9]+|\s+-)?\t(.+)$/", $line, $regs)) {
+				switch($regs[2]) {
+					case 'tree':
+						$data = array();
+						$data['type'] = 'tree';
+						$data['hash'] = $regs[3];
+						$data['mode'] = $regs[1];
+
+						$path = $regs[5];
+						if (!empty($treePath))
+							$path = $treePath . '/' . $path;
+						$data['path'] = $path;
+
+						$contents[] = $data;
+						break;
+					case 'blob':
+						$data = array();
+						$data['type'] = 'blob';
+						$data['hash'] = $regs[3];
+						$data['mode'] = $regs[1];
+
+						$path = $regs[5];
+						if (!empty($treePath))
+							$path = $treePath . '/' . $path;
+						$data['path'] = $path;
+
+						$size = trim($regs[4]);
+						if (!empty($size))
+							$data['size'] = $size;
+
+						$contents[] = $data;
+						break;
+				}
+			}
+		}
+
+		return $contents;
+	}
+}
+

--- /dev/null
+++ b/include/git/tree/TreeLoad_Raw.class.php
@@ -1,1 +1,91 @@
+<?php
+/**
+ * Tree load strategy using raw git objects
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2012 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_TreeLoad_Raw extends GitPHP_TreeLoad_Base
+{
+	/**
+	 * Object loader
+	 *
+	 * @var GitPHP_GitObjectLoader
+	 */
+	protected $objectLoader;
 
+	/**
+	 * Constructor
+	 *
+	 * @param GitPHP_GitObjectLoader $objectLoader object loader
+	 * @param GitPHP_GitExe $exe git exe
+	 */
+	public function __construct($objectLoader, $exe)
+	{
+		if (!$objectLoader)
+			throw new Exception('Git object loader is required');
+
+		$this->objectLoader = $objectLoader;
+
+		parent::__construct($exe);
+	}
+
+	/**
+	 * Gets the data for a tree
+	 *
+	 * @param GitPHP_Tree $tree tree
+	 * @return array array of tree contents
+	 */
+	public function Load($tree)
+	{
+		if (!$tree)
+			return;
+
+		$contents = array();
+
+		$treePath = $tree->GetPath();
+
+		$treeData = $this->objectLoader->GetObject($tree->GetHash());
+
+		$start = 0;
+		$len = strlen($treeData);
+		while ($start < $len) {
+			$pos = strpos($treeData, "\0", $start);
+
+			list($mode, $path) = explode(' ', substr($treeData, $start, $pos-$start), 2);
+			$mode = str_pad($mode, 6, '0', STR_PAD_LEFT);
+			$hash = bin2hex(substr($treeData, $pos+1, 20));
+			$start = $pos + 21;
+
+			$octmode = octdec($mode);
+
+			if ($octmode == 57344) {
+				// submodules not currently supported
+				continue;
+			}
+
+			if (!empty($treePath))
+				$path = $treePath . '/' . $path;
+
+			$data = array();
+			$data['hash'] = $hash;
+			if ($octmode & 0x4000) {
+				// tree
+				$data['type'] = 'tree';
+			} else {
+				// blob
+				$data['type'] = 'blob';
+			}
+
+			$data['mode'] = $mode;
+			$data['path'] = $path;
+
+			$contents[] = $data;
+		}
+
+		return $contents;
+	}
+}
+

comments