Implement object caching for commits
Implement object caching for commits

I bent the serialization rules a little here. Git commit objects store
pointers to various other objects - other parent commits, the project,
the tree, etc. These references would be lost upon serialization.
Therefore, in the __sleep magic method to prepare for serialization, I
"reference" each object - I turn the object into a string identifier
that can be used to retrieve the object again later (for commits and
trees the hash, for the project the name, etc).
But the problem is that I can't "dereference" these objects in the
equivalent __wakeup method - if the commit points to the tree and the
tree points to the commit, then when you try to deserialize the commit
it'll try to deserialize the tree, which will then try to deserialize
the commit... which is an infinite loop.
Therefore, now these references actually remain in the object and are
just-in-time dereferenced when you want to actually access that
referenced object (using a getter). This also has the advantage of
easing the processing when loading from the cache, so that all the
object references aren't resolved at once, they're only resolved when
you need them.

--- a/include/git/Commit.class.php
+++ b/include/git/Commit.class.php
@@ -124,24 +124,6 @@
 	protected $comment = array();
 
 	/**
-	 * treeHashes
-	 *
-	 * Stores tree name to hash mappings
-	 *
-	 * @access protected
-	 */
-	protected $treeHashes = array();
-
-	/**
-	 * blobHashes
-	 *
-	 * Stores blob name to hash mappings
-	 *
-	 * @access protected
-	 */
-	protected $blobHashes = array();
-
-	/**
 	 * readTree
 	 *
 	 * Stores whether tree filenames have been read
@@ -194,6 +176,33 @@
 	 * @access public
 	 */
 	protected $containingTagRead = false;
+
+	/**
+	 * parentsReferenced
+	 *
+	 * Stores whether the parents have been referenced into pointers
+	 *
+	 * @access private
+	 */
+	private $parentsReferenced = false;
+
+	/**
+	 * treeReferenced
+	 *
+	 * Stores whether the tree has been referenced into a pointer
+	 *
+	 * @access private
+	 */
+	private $treeReferenced = false;
+
+	/**
+	 * containingTagReferenced
+	 *
+	 * Stores whether the containing tag has been referenced into a pointer
+	 *
+	 * @access private
+	 */
+	private $containingTagReferenced = false;
 
 	/**
 	 * __construct
@@ -224,6 +233,9 @@
 		if (!$this->dataRead)
 			$this->ReadData();
 
+		if ($this->parentsReferenced)
+			$this->DereferenceParents();
+
 		if (isset($this->parents[0]))
 			return $this->parents[0];
 		return null;
@@ -242,6 +254,9 @@
 		if (!$this->dataRead)
 			$this->ReadData();
 
+		if ($this->parentsReferenced)
+			$this->DereferenceParents();
+
 		return $this->parents;
 	}
 
@@ -257,6 +272,9 @@
 	{
 		if (!$this->dataRead)
 			$this->ReadData();
+
+		if ($this->treeReferenced)
+			$this->DereferenceTree();
 
 		return $this->tree;
 	}
@@ -515,7 +533,7 @@
 		$this->dataRead = true;
 
 		/* get data from git_rev_list */
-		$exe = new GitPHP_GitExe($this->project);
+		$exe = new GitPHP_GitExe($this->GetProject());
 		$args = array();
 		$args[] = '--header';
 		$args[] = '--parents';
@@ -538,7 +556,7 @@
 		$tok = strtok(' ');
 		while ($tok !== false) {
 			try {
-				$this->parents[] = new GitPHP_Commit($this->project, $tok);
+				$this->parents[] = $this->GetProject()->GetCommit($tok);
 			} catch (Exception $e) {
 			}
 			$tok = strtok(' ');
@@ -548,7 +566,7 @@
 			if (preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
 				/* Tree */
 				try {
-					$tree = $this->project->GetTree($regs[1]);
+					$tree = $this->GetProject()->GetTree($regs[1]);
 					if ($tree) {
 						$tree->SetCommit($this);
 						$this->tree = $tree;
@@ -579,6 +597,7 @@
 			}
 		}
 
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -593,7 +612,7 @@
 	{
 		$heads = array();
 
-		$projectHeads = $this->project->GetHeads();
+		$projectHeads = $this->GetProject()->GetHeads();
 
 		foreach ($projectHeads as $head) {
 			if ($head->GetCommit()->GetHash() === $this->hash) {
@@ -616,7 +635,7 @@
 	{
 		$tags = array();
 
-		$projectTags = $this->project->GetTags();
+		$projectTags = $this->GetProject()->GetTags();
 
 		foreach ($projectTags as $tag) {
 			if ($tag->GetCommit()->GetHash() === $this->hash) {
@@ -640,6 +659,9 @@
 		if (!$this->containingTagRead)
 			$this->ReadContainingTag();
 
+		if ($this->containingTagReferenced)
+			$this->DereferenceContainingTag();
+
 		return $this->containingTag;
 	}
 
@@ -654,7 +676,7 @@
 	{
 		$this->containingTagRead = true;
 
-		$exe = new GitPHP_GitExe($this->project);
+		$exe = new GitPHP_GitExe($this->GetProject());
 		$args = array();
 		$args[] = '--tags';
 		$args[] = $this->hash;
@@ -663,11 +685,13 @@
 		foreach ($revs as $revline) {
 			if (preg_match('/^([0-9a-fA-F]{40})\s+tags\/(.+)(\^[0-9]+|\~[0-9]+)$/', $revline, $regs)) {
 				if ($regs[1] == $this->hash) {
-					$this->containingTag = $this->project->GetTag($regs[2]);
+					$this->containingTag = $this->GetProject()->GetTag($regs[2]);
 					break;
 				}
 			}
 		}
+
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -680,7 +704,7 @@
 	 */
 	public function DiffToParent()
 	{
-		return new GitPHP_TreeDiff($this->project, $this->hash);
+		return new GitPHP_TreeDiff($this->GetProject(), $this->hash);
 	}
 
 	/**
@@ -722,7 +746,7 @@
 	{
 		$this->hashPathsRead = true;
 
-		$exe = new GitPHP_GitExe($this->project);
+		$exe = new GitPHP_GitExe($this->GetProject());
 
 		$args = array();
 		$args[] = '--full-name';
@@ -744,6 +768,8 @@
 				}
 			}
 		}
+
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
 	}
 	
 	/**
@@ -767,7 +793,7 @@
 
 		foreach ($this->treePaths as $path => $hash) {
 			if (preg_match('/' . $pattern . '/i', $path)) {
-				$obj = $this->project->GetTree($hash);
+				$obj = $this->GetProject()->GetTree($hash);
 				$obj->SetCommit($this);
 				$results[$path] = $obj;
 			}
@@ -775,7 +801,7 @@
 
 		foreach ($this->blobPaths as $path => $hash) {
 			if (preg_match('/' . $pattern . '/i', $path)) {
-				$obj = $this->project->GetBlob($hash);
+				$obj = $this->GetProject()->GetBlob($hash);
 				$obj->SetCommit($this);
 				$results[$path] = $obj;
 			}
@@ -800,7 +826,7 @@
 		if (empty($pattern))
 			return;
 
-		$exe = new GitPHP_GitExe($this->project);
+		$exe = new GitPHP_GitExe($this->GetProject());
 
 		$args = array();
 		$args[] = '-I';
@@ -820,7 +846,7 @@
 				if (!isset($results[$regs[1]]['object'])) {
 					$hash = $this->PathToHash($regs[1]);
 					if (!empty($hash)) {
-						$obj = $this->project->GetBlob($hash);
+						$obj = $this->GetProject()->GetBlob($hash);
 						$obj->SetCommit($this);
 						$results[$regs[1]]['object'] = $obj;
 					}
@@ -864,5 +890,174 @@
 		return array_slice($grepresults, $skip, $count, true);
 	}
 
+	/**
+	 * ReferenceParents
+	 *
+	 * Turns the list of parents into reference pointers
+	 *
+	 * @access private
+	 */
+	private function ReferenceParents()
+	{
+		if ($this->parentsReferenced)
+			return;
+
+		if ((!isset($this->parents)) || (count($this->parents) < 1))
+			return;
+
+		for ($i = 0; $i < count($this->parents); $i++) {
+			$this->parents[$i] = $this->parents[$i]->GetHash();
+		}
+
+		$this->parentsReferenced = true;
+	}
+
+	/**
+	 * DereferenceParents
+	 *
+	 * Turns the list of parent pointers back into objects
+	 *
+	 * @access private
+	 */
+	private function DereferenceParents()
+	{
+		if (!$this->parentsReferenced)
+			return;
+
+		if ((!$this->parents) || (count($this->parents) < 1))
+			return;
+
+		for ($i = 0; $i < count($this->parents); $i++) {
+			$this->parents[$i] = $this->GetProject()->GetCommit($this->parents[$i]);
+		}
+
+		$this->parentsReferenced = false;
+	}
+
+	/**
+	 * ReferenceTree
+	 *
+	 * Turns the tree into a reference pointer
+	 *
+	 * @access private
+	 */
+	private function ReferenceTree()
+	{
+		if ($this->treeReferenced)
+			return;
+
+		if (!$this->tree)
+			return;
+
+		$this->tree = $this->tree->GetHash();
+
+		$this->treeReferenced = true;
+	}
+
+	/**
+	 * DereferenceTree
+	 *
+	 * Turns the tree pointer back into an object
+	 *
+	 * @access private
+	 */
+	private function DereferenceTree()
+	{
+		if (!$this->treeReferenced)
+			return;
+
+		if (empty($this->tree))
+			return;
+
+		$this->tree = $this->GetProject()->GetTree($this->tree);
+
+		if ($this->tree)
+			$this->tree->SetCommit($this);
+
+		$this->treeReferenced = false;
+	}
+
+	/**
+	 * ReferenceContainingTag
+	 *
+	 * Turns the containing tag into a reference pointer
+	 *
+	 * @access private
+	 */
+	private function ReferenceContainingTag()
+	{
+		if ($this->containingTagReferenced)
+			return;
+
+		if (!$this->containingTag)
+			return;
+
+		$this->containingTag = $this->containingTag->GetName();
+
+		$this->containingTagReferenced = true;
+	}
+
+	/**
+	 * DereferenceContainingTag
+	 *
+	 * Turns the containing tag pointer back into an object
+	 *
+	 * @access private
+	 */
+	private function DereferenceContainingTag()
+	{
+		if (!$this->containingTagReferenced)
+			return;
+
+		if (empty($this->containingTag))
+			return;
+
+		$this->containingTag = $this->GetProject()->GetTag($this->containingTag);
+		
+		$this->containingTagReferenced = false;
+	}
+
+	/**
+	 * __sleep
+	 *
+	 * Called to prepare the object for serialization
+	 *
+	 * @access public
+	 * @return array list of properties to serialize
+	 */
+	public function __sleep()
+	{
+		if (!$this->parentsReferenced)
+			$this->ReferenceParents();
+
+		if (!$this->treeReferenced)
+			$this->ReferenceTree();
+
+		if (!$this->containingTagReferenced)
+			$this->ReferenceContainingTag();
+
+		$properties = array('dataRead', 'parents', 'tree', 'author', 'authorEpoch', 'authorTimezone', 'committer', 'committerEpoch', 'committerTimezone', 'title', 'comment', 'readTree', 'blobPaths', 'treePaths', 'hashPathsRead', 'containingTag', 'containingTagRead', 'parentsReferenced', 'treeReferenced', 'containingTagReferenced');
+		return array_merge($properties, parent::__sleep());
+	}
+
+	/**
+	 * GetCacheKey
+	 *
+	 * Gets the cache key to use for this object
+	 *
+	 * @access public
+	 * @return string cache key
+	 */
+	public function GetCacheKey()
+	{
+		$key = parent::GetCacheKey();
+		if (!empty($key))
+			$key .= '|';
+
+		$key .= 'commit|' . $this->hash;
+
+		return $key;
+	}
+
 }
 

--- a/include/git/GitObject.class.php
+++ b/include/git/GitObject.class.php
@@ -38,6 +38,15 @@
 	protected $hash;
 
 	/**
+	 * projectReferenced
+	 *
+	 * Stores whether the project has been referenced into a pointer
+	 *
+	 * @access protected
+	 */
+	protected $projectReferenced = false;
+
+	/**
 	 * __construct
 	 *
 	 * Instantiates object
@@ -64,6 +73,9 @@
 	 */
 	public function GetProject()
 	{
+		if ($this->projectReferenced)
+			$this->DereferenceProject();
+
 		return $this->project;
 	}
 
@@ -97,5 +109,74 @@
 		$this->hash = $hash;
 	}
 
+	/**
+	 * ReferenceProject
+	 *
+	 * Turns the project object into a reference pointer
+	 *
+	 * @access private
+	 */
+	private function ReferenceProject()
+	{
+		if ($this->projectReferenced)
+			return;
+
+		$this->project = $this->project->GetProject();
+
+		$this->projectReferenced = true;
+	}
+
+	/**
+	 * DereferenceProject
+	 *
+	 * Turns the project reference pointer back into an object
+	 *
+	 * @access private
+	 */
+	private function DereferenceProject()
+	{
+		if (!$this->projectReferenced)
+			return;
+
+		$this->project = GitPHP_ProjectList::GetInstance()->GetProject($this->project);
+
+		$this->projectReferenced = false;
+	}
+
+	/**
+	 * __sleep
+	 *
+	 * Called to prepare the object for serialization
+	 *
+	 * @access public
+	 * @return array list of properties to serialize
+	 */
+	public function __sleep()
+	{
+		if (!$this->projectReferenced)
+			$this->ReferenceProject();
+
+		return array('project', 'hash', 'projectReferenced');
+	}
+
+	/**
+	 * GetCacheKey
+	 *
+	 * Gets the cache key to use for this object
+	 *
+	 * @access public
+	 * @return string cache key
+	 */
+	public function GetCacheKey()
+	{
+		$projKey = 'project|';
+		if ($this->projectReferenced)
+			$projKey .= $this->project;
+		else
+			$projKey .= $this->project->GetProject();
+
+		return $projKey;
+	}
+
 }
 

--- a/include/git/Project.class.php
+++ b/include/git/Project.class.php
@@ -600,8 +600,14 @@
 			}
 		}
 
-		if (!isset($this->commitCache[$hash]))
-			$this->commitCache[$hash] = new GitPHP_Commit($this, $hash);
+		if (!isset($this->commitCache[$hash])) {
+			$cacheKey = 'project|' . $this->project . '|commit|' . $hash;
+			$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+			if ($cached)
+				$this->commitCache[$hash] = $cached;
+			else
+				$this->commitCache[$hash] = new GitPHP_Commit($this, $hash);
+		}
 
 		return $this->commitCache[$hash];
 	}

comments