Cache packfile index offsets
Cache packfile index offsets

--- a/include/Util.class.php
+++ b/include/Util.class.php
@@ -74,5 +74,26 @@
 		return (strpos(php_uname('m'), '64') !== false);
 	}
 
+	/**
+	 * MakeSlug
+	 *
+	 * Turn a string into a filename-friendly slug
+	 *
+	 * @access public
+	 * @param string $str string to slugify
+	 * @static
+	 * @return string slug
+	 */
+	public static function MakeSlug($str)
+	{
+		$from = array(
+			'/'
+		);
+		$to = array(
+			'-'
+		);
+		return str_replace($from, $to, $str);
+	}
+
 }
 

--- a/include/git/Archive.class.php
+++ b/include/git/Archive.class.php
@@ -226,6 +226,10 @@
 
 		$fname = $this->GetProject()->GetSlug();
 
+		if (!empty($this->path)) {
+			$fname .= '-' . GitPHP_Util::MakeSlug($this->path);
+		}
+
 		$fname .= '.' . $this->GetExtension();
 
 		return $fname;
@@ -284,7 +288,12 @@
 			return $this->prefix;
 		}
 
-		return $this->GetProject()->GetSlug() . '/';
+		$pfx = $this->GetProject()->GetSlug() . '/';
+
+		if (!empty($this->path))
+			$pfx .= $this->path . '/';
+
+		return $pfx;
 	}
 
 	/**
@@ -378,9 +387,6 @@
 		$args[] = '--prefix=' . $this->GetPrefix();
 		$args[] = $this->gitObject->GetHash();
 
-		if (!empty($this->path))
-			$args[] = $this->path;
-
 		$data = $exe->Execute(GIT_ARCHIVE, $args);
 		unset($exe);
 

--- a/include/git/Blob.class.php
+++ b/include/git/Blob.class.php
@@ -47,7 +47,7 @@
 	 *
 	 * @access protected
 	 */
-	protected $size;
+	protected $size = null;
 
 	/**
 	 * history
@@ -132,13 +132,17 @@
 	{
 		$this->dataRead = true;
 
-		$exe = new GitPHP_GitExe($this->GetProject());
-
-		$args = array();
-		$args[] = 'blob';
-		$args[] = $this->hash;
-
-		$this->data = $exe->Execute(GIT_CAT_FILE, $args);
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$exe = new GitPHP_GitExe($this->GetProject());
+
+			$args = array();
+			$args[] = 'blob';
+			$args[] = $this->hash;
+
+			$this->data = $exe->Execute(GIT_CAT_FILE, $args);
+		} else {
+			$this->data = $this->GetProject()->GetObject($this->hash);
+		}
 
 		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
 	}
@@ -194,7 +198,14 @@
 	 */
 	public function GetSize()
 	{
-		return $this->size;
+		if ($this->size !== null) {
+			return $this->size;
+		}
+
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return strlen($this->data);
 	}
 
 	/**

--- a/include/git/Commit.class.php
+++ b/include/git/Commit.class.php
@@ -744,6 +744,24 @@
 	{
 		$this->hashPathsRead = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadHashPathsGit();
+		} else {
+			$this->ReadHashPathsRaw($this->GetTree());
+		}
+
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadHashPathsGit
+	 *
+	 * Reads hash to path mappings using git exe
+	 *
+	 * @access private
+	 */
+	private function ReadHashPathsGit()
+	{
 		$exe = new GitPHP_GitExe($this->GetProject());
 
 		$args = array();
@@ -766,8 +784,35 @@
 				}
 			}
 		}
-
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadHashPathsRaw
+	 *
+	 * Reads hash to path mappings using raw objects
+	 *
+	 * @access private
+	 */
+	private function ReadHashPathsRaw($tree)
+	{
+		if (!$tree) {
+			return;
+		}
+
+		$contents = $tree->GetContents();
+
+		foreach ($contents as $obj) {
+			if ($obj instanceof GitPHP_Blob) {
+				$hash = $obj->GetHash();
+				$path = $obj->GetPath();
+				$this->blobPaths[trim($path)] = $hash;
+			} else if ($obj instanceof GitPHP_Tree) {
+				$hash = $obj->GetHash();
+				$path = $obj->GetPath();
+				$this->treePaths[trim($path)] = $hash;
+				$this->ReadHashPathsRaw($obj);
+			}
+		}
 	}
 
 	/**
@@ -1014,5 +1059,23 @@
 		return $key;
 	}
 
+	/**
+	 * CompareAge
+	 *
+	 * Compares two commits by age
+	 *
+	 * @access public
+	 * @static
+	 * @param mixed $a first commit
+	 * @param mixed $b second commit
+	 * @return integer comparison result
+	 */
+	public static function CompareAge($a, $b)
+	{
+		if ($a->GetAge() === $b->GetAge())
+			return 0;
+		return ($a->GetAge() < $b->GetAge() ? -1 : 1);
+	}
+
 }
 

--- a/include/git/FileDiff.class.php
+++ b/include/git/FileDiff.class.php
@@ -656,9 +656,8 @@
 
 		$exe = new GitPHP_GitExe($this->project);
 
-		$rawBlob = $exe->Execute(GIT_CAT_FILE,
-			array("blob", $this->fromHash));
-		$blob  = explode("\n", $rawBlob);
+		$fromBlob = $this->GetFromBlob();
+		$blob = $fromBlob->GetData(true);
 
 		$diffLines = explode("\n", $exe->Execute(GIT_DIFF,
 			array("-U0", $this->fromHash,

--- a/include/git/Head.class.php
+++ b/include/git/Head.class.php
@@ -79,9 +79,7 @@
 	{
 		$aObj = $a->GetCommit();
 		$bObj = $b->GetCommit();
-		if ($aObj->GetAge() === $bObj->GetAge())
-			return 0;
-		return ($aObj->GetAge() < $bObj->GetAge() ? -1 : 1);
+		return GitPHP_Commit::CompareAge($aObj, $bObj);
 	}
 
 }

--- a/include/git/Pack.class.php
+++ b/include/git/Pack.class.php
@@ -46,6 +46,24 @@
 	protected $hash;
 
 	/**
+	 * offsetCache
+	 *
+	 * Caches object offsets
+	 *
+	 * @access protected
+	 */
+	protected $offsetCache = array();
+
+	/**
+	 * indexModified
+	 *
+	 * Stores the index file last modified time
+	 *
+	 * @access protected
+	 */
+	protected $indexModified = 0;
+
+	/**
 	 * __construct
 	 *
 	 * Instantiates object
@@ -118,9 +136,20 @@
 			return false;
 		}
 
+		$indexFile = $this->project->GetPath() . '/objects/pack/pack-' . $this->hash . '.idx';
+		$mTime = filemtime($indexFile);
+		if ($mTime > $this->indexModified) {
+			$this->offsetCache = array();
+			$this->indexModified = $mTime;
+		}
+
+		if (isset($this->offsetCache[$hash])) {
+			return $this->offsetCache[$hash];
+		}
+
 		$offset = false;
 
-		$index = fopen($this->project->GetPath() . '/objects/pack/pack-' . $this->hash . '.idx', 'rb');
+		$index = fopen($indexFile, 'rb');
 		flock($index, LOCK_SH);
 
 		$magic = fread($index, 4);
@@ -134,6 +163,7 @@
 		}
 		flock($index, LOCK_UN);
 		fclose($index);
+		$this->offsetCache[$hash] = $offset;
 		return $offset;
 	}
 
@@ -173,11 +203,13 @@
 		 */
 		while ($low <= $high) {
 			$mid = ($low + $high) >> 1;
-			fseek($index, 4*256 + 24*$low);
+			fseek($index, 4*256 + 24*$mid);
 
 			$off = GitPHP_Pack::fuint32($index);
 			$binName = fread($index, 20);
 			$name = bin2hex($binName);
+
+			$this->offsetCache[$name] = $off;
 
 			$cmp = strcmp($hash, $name);
 			

--- a/include/git/Project.class.php
+++ b/include/git/Project.class.php
@@ -26,6 +26,15 @@
 {
 
 	/**
+	 * projectRoot
+	 *
+	 * Stores the project root internally
+	 *
+	 * @access protected
+	 */
+	protected $projectRoot;
+
+	/**
 	 * project
 	 *
 	 * Stores the project internally
@@ -44,13 +53,13 @@
 	protected $owner = "";
 
 	/**
-	 * readOwner
+	 * ownerRead
 	 *
 	 * Stores whether the file owner has been read
 	 *
 	 * @access protected
 	 */
-	protected $readOwner = false;
+	protected $ownerRead = false;
 
 	/**
 	 * description
@@ -213,10 +222,13 @@
 	 * Class constructor
 	 *
 	 * @access public
+	 * @param string $projectRoot project root
+	 * @param string $project project
 	 * @throws Exception if project is invalid or outside of projectroot
 	 */
-	public function __construct($project)
-	{
+	public function __construct($projectRoot, $project)
+	{
+		$this->projectRoot = GitPHP_Util::AddSlash($projectRoot);
 		$this->SetProject($project);
 	}
 
@@ -230,10 +242,8 @@
 	 */
 	private function SetProject($project)
 	{
-		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
-
-		$realProjectRoot = realpath($projectRoot);
-		$path = $projectRoot . $project;
+		$realProjectRoot = realpath($this->projectRoot);
+		$path = $this->projectRoot . $project;
 		$fullPath = realpath($path);
 
 		if (!is_dir($fullPath)) {
@@ -268,33 +278,101 @@
 	 */
 	public function GetOwner()
 	{
-		if (empty($this->owner) && !$this->readOwner) {
-
-			$exe = new GitPHP_GitExe($this);
-			$args = array();
-			$args[] = 'gitweb.owner';
-			$this->owner = $exe->Execute(GIT_CONFIG, $args);
-			unset($exe);
-			
-			if (empty($this->owner) && function_exists('posix_getpwuid')) {
-				$uid = fileowner($this->GetPath());
-				if ($uid !== false) {
-					$data = posix_getpwuid($uid);
-					if (isset($data['gecos']) && !empty($data['gecos'])) {
-						$this->owner = $data['gecos'];
-					} elseif (isset($data['name']) && !empty($data['name'])) {
-						$this->owner = $data['name'];
-					}
-				}
-			}
-
-			$this->readOwner = true;
+		if (empty($this->owner) && !$this->ownerRead) {
+			$this->ReadOwner();
 		}
 	
 		return $this->owner;
 	}
 
 	/**
+	 * ReadOwner
+	 *
+	 * Reads the project owner
+	 *
+	 * @access protected
+	 */
+	protected function ReadOwner()
+	{
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadOwnerGit();
+		} else {
+			$this->ReadOwnerRaw();
+		}
+
+		if (empty($this->owner) && function_exists('posix_getpwuid')) {
+			$uid = fileowner($this->GetPath());
+			if ($uid !== false) {
+				$data = posix_getpwuid($uid);
+				if (isset($data['gecos']) && !empty($data['gecos'])) {
+					$this->owner = $data['gecos'];
+				} elseif (isset($data['name']) && !empty($data['name'])) {
+					$this->owner = $data['name'];
+				}
+			}
+		}
+
+		$this->ownerRead = true;
+	}
+
+	/**
+	 * ReadOwnerGit
+	 *
+	 * Reads the project owner using the git executable
+	 *
+	 * @access private
+	 */
+	private function ReadOwnerGit()
+	{
+		$exe = new GitPHP_GitExe($this);
+		$args = array();
+		$args[] = 'gitweb.owner';
+		$this->owner = $exe->Execute(GIT_CONFIG, $args);
+		unset($exe);
+	}
+
+	/**
+	 * ReadOwnerRaw
+	 *
+	 * Reads the project owner using the raw config file
+	 *
+	 * @access private
+	 */
+	private function ReadOwnerRaw()
+	{
+		// not worth writing a full config parser right now
+
+		if (!file_exists($this->GetPath() . '/config'))
+			return;
+
+		$configData = explode("\n", file_get_contents($this->GetPath() . '/config'));
+
+		$gitwebSection = false;
+		foreach ($configData as $configLine) {
+			$trimmed = trim($configLine);
+			if (empty($trimmed)) {
+				continue;
+			}
+
+			if (preg_match('/^\[(.+)\]$/', $trimmed, $regs)) {
+				// section header
+				$gitwebSection = ($regs[1] == 'gitweb');
+			} else if ($gitwebSection) {
+				$eq = strpos($trimmed, '=');
+				if ($eq === false) {
+					continue;
+				}
+
+				$key = trim(substr($trimmed, 0, $eq));
+				if ($key == 'owner') {
+					$this->owner = trim(substr($trimmed, $eq+1));
+					break;
+				}
+			}
+		}
+	}
+
+	/**
 	 * SetOwner
 	 *
 	 * Sets the project's owner (from an external source)
@@ -321,6 +399,19 @@
 	}
 
 	/**
+	 * GetProjectRoot
+	 *
+	 * Gets the project root
+	 *
+	 * @access public
+	 * @return string the project root
+	 */
+	public function GetProjectRoot()
+	{
+		return $this->projectRoot;
+	}
+
+	/**
 	 * GetSlug
 	 *
 	 * Gets the project as a filename/url friendly slug
@@ -330,15 +421,12 @@
 	 */
 	public function GetSlug()
 	{
-		$from = array(
-			'/',
-			'.git'
-		);
-		$to = array(
-			'-',
-			''
-		);
-		return str_replace($from, $to, $this->project);
+		$project = $this->project;
+
+		if (substr($project, -4) == '.git')
+			$project = substr($project, 0, -4);
+		
+		return GitPHP_Util::MakeSlug($project);
 	}
 
 	/**
@@ -351,9 +439,7 @@
 	 */
 	public function GetPath()
 	{
-		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
-
-		return $projectRoot . $this->project;
+		return $this->projectRoot . $this->project;
 	}
 
 	/**
@@ -583,11 +669,51 @@
 	{
 		$this->readHeadRef = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadHeadCommitGit();
+		} else {
+			$this->ReadHeadCommitRaw();
+		}
+	}
+
+	/**
+	 * ReadHeadCommitGit
+	 *
+	 * Read head commit using git executable
+	 *
+	 * @access private
+	 */
+	private function ReadHeadCommitGit()
+	{
 		$exe = new GitPHP_GitExe($this);
 		$args = array();
 		$args[] = '--verify';
 		$args[] = 'HEAD';
 		$this->head = trim($exe->Execute(GIT_REV_PARSE, $args));
+	}
+
+	/**
+	 * ReadHeadCommitRaw
+	 *
+	 * Read head commit using raw git head pointer
+	 *
+	 * @access private
+	 */
+	private function ReadHeadCommitRaw()
+	{
+		$head = trim(file_get_contents($this->GetPath() . '/HEAD'));
+		if (preg_match('/^([0-9A-Fa-f]{40})$/', $head, $regs)) {
+			/* Detached HEAD */
+			$this->head = $regs[1];
+		} else if (preg_match('/^ref: (.+)$/', $head, $regs)) {
+			/* standard pointer to head */
+			if (!$this->readRefs)
+				$this->ReadRefList();
+
+			if (isset($this->heads[$regs[1]])) {
+				$this->head = $this->heads[$regs[1]]->GetHash();
+			}
+		}
 	}
 
 	/**
@@ -760,10 +886,26 @@
 	 *
 	 * @access protected
 	 */
-	public function ReadRefList()
+	protected function ReadRefList()
 	{
 		$this->readRefs = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadRefListGit();
+		} else {
+			$this->ReadRefListRaw();
+		}
+	}
+
+	/**
+	 * ReadRefListGit
+	 *
+	 * Reads the list of refs for this project using the git executable
+	 *
+	 * @access private
+	 */
+	private function ReadRefListGit()
+	{
 		$exe = new GitPHP_GitExe($this);
 		$args = array();
 		$args[] = '--heads';
@@ -798,6 +940,117 @@
 	}
 
 	/**
+	 * ReadRefListRaw
+	 *
+	 * Reads the list of refs for this project using the raw git files
+	 *
+	 * @access private
+	 */
+	private function ReadRefListRaw()
+	{
+		$pathlen = strlen($this->GetPath()) + 1;
+
+		// read loose heads
+		$heads = $this->ListDir($this->GetPath() . '/refs/heads');
+		for ($i = 0; $i < count($heads); $i++) {
+			$key = trim(substr($heads[$i], $pathlen), "/\\");
+
+			if (isset($this->heads[$key])) {
+				continue;
+			}
+
+			$hash = trim(file_get_contents($heads[$i]));
+			if (preg_match('/^[0-9A-Fa-f]{40}$/', $hash)) {
+				$head = substr($key, strlen('refs/heads/'));
+				$this->heads[$key] = new GitPHP_Head($this, $head, $hash);
+			}
+		}
+
+		// read loose tags
+		$tags = $this->ListDir($this->GetPath() . '/refs/tags');
+		for ($i = 0; $i < count($tags); $i++) {
+			$key = trim(substr($tags[$i], $pathlen), "/\\");
+
+			if (isset($this->tags[$key])) {
+				continue;
+			}
+
+			$hash = trim(file_get_contents($tags[$i]));
+			if (preg_match('/^[0-9A-Fa-f]{40}$/', $hash)) {
+				$tag = substr($key, strlen('refs/tags/'));
+				$this->tags[$key] = $this->LoadTag($tag, $hash);
+			}
+		}
+
+		// check packed refs
+		if (file_exists($this->GetPath() . '/packed-refs')) {
+			$packedRefs = explode("\n", file_get_contents($this->GetPath() . '/packed-refs'));
+
+			$lastRef = null;
+			foreach ($packedRefs as $ref) {
+
+				if (preg_match('/^\^([0-9A-Fa-f]{40})$/', $ref, $regs)) {
+					// dereference of previous ref
+					if (($lastRef != null) && ($lastRef instanceof GitPHP_Tag)) {
+						$derefCommit = $this->GetCommit($regs[1]);
+						if ($derefCommit) {
+							$lastRef->SetCommit($derefCommit);
+						}
+					}
+				}
+
+				$lastRef = null;
+
+				if (preg_match('/^([0-9A-Fa-f]{40}) refs\/(tags|heads)\/(.+)$/', $ref, $regs)) {
+					// standard tag/head
+					$key = 'refs/' . $regs[2] . '/' . $regs[3];
+					if ($regs[2] == 'tags') {
+						if (!isset($this->tags[$key])) {
+							$lastRef = $this->LoadTag($regs[3], $regs[1]);
+							$this->tags[$key] = $lastRef;
+						}
+					} else if ($regs[2] == 'heads') {
+						if (!isset($this->heads[$key])) {
+							$this->heads[$key] = new GitPHP_Head($this, $regs[3], $regs[1]);
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * ListDir
+	 *
+	 * Recurses into a directory and lists files inside
+	 *
+	 * @access private
+	 * @param string $dir directory
+	 * @return array array of filenames
+	 */
+	private function ListDir($dir)
+	{
+		$files = array();
+		if ($dh = opendir($dir)) {
+			while (($file = readdir($dh)) !== false) {
+				if (($file == '.') || ($file == '..')) {
+					continue;
+				}
+				$fullFile = $dir . '/' . $file;
+				if (is_dir($fullFile)) {
+					$subFiles = $this->ListDir($fullFile);
+					if (count($subFiles) > 0) {
+						$files = array_merge($files, $subFiles);
+					}
+				} else {
+					$files[] = $fullFile;
+				}
+			}
+		}
+		return $files;
+	}
+
+	/**
 	 * GetTags
 	 *
 	 * Gets list of tags for this project by age descending
@@ -811,6 +1064,24 @@
 		if (!$this->readRefs)
 			$this->ReadRefList();
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			return $this->GetTagsGit($count);
+		} else {
+			return $this->GetTagsRaw($count);
+		}
+	}
+
+	/**
+	 * GetTagsGit
+	 *
+	 * Gets list of tags for this project by age descending using git executable
+	 *
+	 * @access private
+	 * @param integer $count number of tags to load
+	 * @return array array of tags
+	 */
+	private function GetTagsGit($count = 0)
+	{
 		$exe = new GitPHP_GitExe($this);
 		$args = array();
 		$args[] = '--sort=-creatordate';
@@ -837,6 +1108,27 @@
 	}
 
 	/**
+	 * GetTagsRaw
+	 *
+	 * Gets list of tags for this project by age descending using raw git objects
+	 *
+	 * @access private
+	 * @param integer $count number of tags to load
+	 * @return array array of tags
+	 */
+	private function GetTagsRaw($count = 0)
+	{
+		$tags = $this->tags;
+		usort($tags, array('GitPHP_Tag', 'CompareCreationEpoch'));
+
+		if (($count > 0) && (count($tags) > $count)) {
+			$tags = array_slice($tags, 0, $count);
+		}
+
+		return $tags;
+	}
+
+	/**
 	 * GetTag
 	 *
 	 * Gets a single tag
@@ -849,6 +1141,9 @@
 	{
 		if (empty($tag))
 			return null;
+
+		if (!$this->readRefs)
+			$this->ReadRefList();
 
 		$key = 'refs/tags/' . $tag;
 
@@ -896,6 +1191,24 @@
 		if (!$this->readRefs)
 			$this->ReadRefList();
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			return $this->GetHeadsGit($count);
+		} else {
+			return $this->GetHeadsRaw($count);
+		}
+	}
+
+	/**
+	 * GetHeadsGit
+	 *
+	 * Gets the list of sorted heads using the git executable
+	 *
+	 * @access private
+	 * @param integer $count number of tags to load
+	 * @return array array of heads
+	 */
+	private function GetHeadsGit($count = 0)
+	{
 		$exe = new GitPHP_GitExe($this);
 		$args = array();
 		$args[] = '--sort=-committerdate';
@@ -922,6 +1235,26 @@
 	}
 
 	/**
+	 * GetHeadsRaw
+	 *
+	 * Gets the list of sorted heads using raw git objects
+	 *
+	 * @access private
+	 * @param integer $count number of tags to load
+	 * @return array array of heads
+	 */
+	private function GetHeadsRaw($count = 0)
+	{
+		$heads = $this->heads;
+		usort($heads, array('GitPHP_Head', 'CompareAge'));
+
+		if (($count > 0) && (count($heads) > $count)) {
+			$heads = array_slice($heads, 0, $count);
+		}
+		return $heads;
+	}
+
+	/**
 	 * GetHead
 	 *
 	 * Gets a single head
@@ -935,6 +1268,9 @@
 		if (empty($head))
 			return null;
 
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
 		$key = 'refs/heads/' . $head;
 
 		if (!isset($this->heads[$key])) {
@@ -949,13 +1285,13 @@
 	 *
 	 * Gets log entries as an array of hashes
 	 *
-	 * @access public
+	 * @access private
 	 * @param string $hash hash to start the log at
 	 * @param integer $count number of entries to get
 	 * @param integer $skip number of entries to skip
 	 * @return array array of hashes
 	 */
-	public function GetLogHash($hash, $count = 50, $skip = 0)
+	private function GetLogHash($hash, $count = 50, $skip = 0)
 	{
 		return $this->RevList($hash, $count, $skip);
 	}
@@ -973,11 +1309,87 @@
 	 */
 	public function GetLog($hash, $count = 50, $skip = 0)
 	{
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false) || ($skip > 200)) {
+			return $this->GetLogGit($hash, $count, $skip);
+		} else {
+			return $this->GetLogRaw($hash, $count, $skip);
+		}
+	}
+
+	/**
+	 * GetLogGit
+	 *
+	 * Gets log entries using git exe
+	 *
+	 * @access private
+	 * @param string $hash hash to start the log at
+	 * @param integer $count number of entries to get
+	 * @param integer $skip number of entries to skip
+	 * @return array array of commit objects
+	 */
+	private function GetLogGit($hash, $count = 50, $skip = 0)
+	{
 		$log = $this->GetLogHash($hash, $count, $skip);
 		$len = count($log);
 		for ($i = 0; $i < $len; ++$i) {
 			$log[$i] = $this->GetCommit($log[$i]);
 		}
+		return $log;
+	}
+
+	/**
+	 * GetLogRaw
+	 *
+	 * Gets log entries using raw git objects
+	 * Based on history walking code from glip
+	 *
+	 * @access private
+	 */
+	private function GetLogRaw($hash, $count = 50, $skip = 0)
+	{
+		$total = $count + $skip;
+
+		$inc = array();
+		$num = 0;
+		$queue = array($this->GetCommit($hash));
+		while (($commit = array_shift($queue)) !== null) {
+			$parents = $commit->GetParents();
+			foreach ($parents as $parent) {
+				if (!isset($inc[$parent->GetHash()])) {
+					$inc[$parent->GetHash()] = 1;
+					$queue[] = $parent;
+					$num++;
+				} else {
+					$inc[$parent->GetHash()]++;
+				}
+			}
+			if ($num >= $total)
+				break;
+		}
+
+		$queue = array($this->GetCommit($hash));
+		$log = array();
+		$num = 0;
+		while (($commit = array_pop($queue)) !== null) {
+			array_push($log, $commit);
+			$num++;
+			if ($num == $total) {
+				break;
+			}
+			$parents = $commit->GetParents();
+			foreach ($parents as $parent) {
+				if (isset($inc[$parent->GetHash()])) {
+					if (--$inc[$parent->GetHash()] == 0) {
+						$queue[] = $parent;
+					}
+				}
+			}
+		}
+
+		if ($skip > 0) {
+			$log = array_slice($log, $skip, $count);
+		}
+		usort($log, array('GitPHP_Commit', 'CompareAge'));
 		return $log;
 	}
 
@@ -1223,6 +1635,22 @@
 	{
 		$this->epochRead = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadEpochGit();
+		} else {
+			$this->ReadEpochRaw();
+		}
+	}
+
+	/**
+	 * ReadEpochGit
+	 *
+	 * Reads this project's epoch using git executable
+	 *
+	 * @access private
+	 */
+	private function ReadEpochGit()
+	{
 		$exe = new GitPHP_GitExe($this);
 
 		$args = array();
@@ -1238,6 +1666,32 @@
 		}
 
 		unset($exe);
+	}
+
+	/**
+	 * ReadEpochRaw
+	 *
+	 * Reads this project's epoch using raw objects
+	 *
+	 * @access private
+	 */
+	private function ReadEpochRaw()
+	{
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
+		$epoch = 0;
+		foreach ($this->heads as $head) {
+			$commit = $head->GetCommit();
+			if ($commit) {
+				if ($commit->GetCommitterEpoch() > $epoch) {
+					$epoch = $commit->GetCommitterEpoch();
+				}
+			}
+		}
+		if ($epoch > 0) {
+			$this->epoch = $epoch;
+		}
 	}
 
 	/**

--- a/include/git/ProjectListArray.class.php
+++ b/include/git/ProjectListArray.class.php
@@ -52,21 +52,23 @@
 	 */
 	protected function PopulateProjects()
 	{
+		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
+
 		foreach ($this->projectConfig as $proj => $projData) {
 			try {
 				if (is_string($projData)) {
 					// Just flat array of project paths
-					$projObj = new GitPHP_Project($projData);
+					$projObj = new GitPHP_Project($projectRoot, $projData);
 					$this->projects[$projData] = $projObj;
 				} else if (is_array($projData)) {
 					if (is_string($proj) && !empty($proj)) {
 						// Project key pointing to data array
-						$projObj = new GitPHP_Project($proj);
+						$projObj = new GitPHP_Project($projectRoot, $proj);
 						$this->projects[$proj] = $projObj;
 						$this->ApplyProjectSettings($proj, $projData);
 					} else if (isset($projData['project'])) {
 						// List of data arrays with projects inside
-						$projObj = new GitPHP_Project($projData['project']);
+						$projObj = new GitPHP_Project($projectRoot, $projData['project']);
 						$this->projects[$projData['project']] = $projObj;
 						$this->ApplyProjectSettings(null, $projData);
 					}

--- a/include/git/ProjectListArrayLegacy.class.php
+++ b/include/git/ProjectListArrayLegacy.class.php
@@ -55,11 +55,13 @@
 	 */
 	protected function PopulateProjects()
 	{
+		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
+
 		foreach ($this->projectConfig as $cat => $plist) {
 			if (is_array($plist)) {
 				foreach ($plist as $pname => $ppath) {
 					try {
-						$projObj = new GitPHP_Project($ppath);
+						$projObj = new GitPHP_Project($projectRoot, $ppath);
 						if ($cat != GITPHP_NO_CATEGORY)
 							$projObj->SetCategory($cat);
 						$this->projects[$ppath] = $projObj;

--- a/include/git/ProjectListDirectory.class.php
+++ b/include/git/ProjectListDirectory.class.php
@@ -84,7 +84,7 @@
 					if (is_file($fullPath . '/HEAD')) {
 						$projectPath = substr($fullPath, $trimlen);
 						try {
-							$proj = new GitPHP_Project($projectPath);
+							$proj = new GitPHP_Project($this->projectDir, $projectPath);
 							$proj->SetCategory(trim(substr($dir, strlen($this->projectDir)), '/'));
 							if ((!GitPHP_Config::GetInstance()->GetValue('exportedonly', false)) || $proj->GetDaemonEnabled()) {
 								$this->projects[$projectPath] = $proj;

--- a/include/git/ProjectListFile.class.php
+++ b/include/git/ProjectListFile.class.php
@@ -63,7 +63,7 @@
 			if (preg_match('/^([^\s]+)(\s.+)?$/', $line, $regs)) {
 				if (is_file($projectRoot . $regs[1] . '/HEAD')) {
 					try {
-						$projObj = new GitPHP_Project($regs[1]);
+						$projObj = new GitPHP_Project($projectRoot, $regs[1]);
 						if (isset($regs[2]) && !empty($regs[2])) {
 							$projOwner = trim($regs[2]);
 							if (!empty($projOwner)) {

--- a/include/git/Tag.class.php
+++ b/include/git/Tag.class.php
@@ -322,6 +322,24 @@
 	{
 		$this->dataRead = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadDataGit();
+		} else {
+			$this->ReadDataRaw();
+		}
+
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadDataGit
+	 *
+	 * Reads the tag data using the git executable
+	 *
+	 * @access private
+	 */
+	private function ReadDataGit()
+	{
 		$exe = new GitPHP_GitExe($this->GetProject());
 		$args = array();
 		$args[] = '-t';
@@ -406,8 +424,84 @@
 				}
 				break;
 		}
-
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadDataRaw
+	 *
+	 * Reads the tag data using the raw git object
+	 *
+	 * @access private
+	 */
+	private function ReadDataRaw()
+	{
+		$data = $this->GetProject()->GetObject($this->GetHash(), $type);
+		
+		if ($type == GitPHP_Pack::OBJ_COMMIT) {
+			/* light tag */
+			$this->object = $this->GetProject()->GetCommit($this->GetHash());
+			$this->commit = $this->object;
+			$this->type = 'commit';
+			GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+			return;
+		}
+
+		$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)) {
+					$this->type = $regs[1];
+					continue;
+				} else if (preg_match('/^tag (.+)$/', $line, $regs)) {
+					continue;
+				} else if (preg_match('/^tagger (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+					$this->tagger = $regs[1];
+					$this->taggerEpoch = $regs[2];
+					$this->taggerTimezone = $regs[3];
+					continue;
+				}
+			}
+
+			$trimmed = trim($line);
+
+			if ((strlen($trimmed) > 0) || ($readInitialData === true)) {
+				$this->comment[] = $line;
+			}
+			$readInitialData = true;
+		}
+
+		switch ($this->type) {
+			case 'commit':
+				try {
+					$this->object = $this->GetProject()->GetCommit($objectHash);
+					$this->commit = $this->object;
+				} catch (Exception $e) {
+				}
+				break;
+			case 'tag':
+				$objectData = $this->GetProject()->GetObject($objectHash);
+				$lines = explode("\n", $objectData);
+				foreach ($lines as $i => $line) {
+					if (preg_match('/^tag (.+)$/', $line, $regs)) {
+						$name = trim($regs[1]);
+						$this->object = $this->GetProject()->GetTag($name);
+						if ($this->object) {
+							$this->object->SetHash($objectHash);
+						}
+					}
+				}
+				break;
+		}
 	}
 
 	/**
@@ -580,6 +674,25 @@
 		return $key;
 	}
 
+	/**
+	 * GetCreationEpoch
+	 *
+	 * Gets tag's creation epoch
+	 * (tagger epoch, or committer epoch for light tags)
+	 *
+	 * @access public
+	 * @return string creation epoch
+	 */
+	public function GetCreationEpoch()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		if ($this->LightTag())
+			return $this->GetCommit()->GetCommitterEpoch();
+		else
+			return $this->taggerEpoch;
+	}
 
 	/**
 	 * CompareAge
@@ -597,9 +710,7 @@
 		$aObj = $a->GetObject();
 		$bObj = $b->GetObject();
 		if (($aObj instanceof GitPHP_Commit) && ($bObj instanceof GitPHP_Commit)) {
-			if ($aObj->GetAge() === $bObj->GetAge())
-				return 0;
-			return ($aObj->GetAge() < $bObj->GetAge() ? -1 : 1);
+			return GitPHP_Commit::CompareAge($aObj, $bObj);
 		}
 
 		if ($aObj instanceof GitPHP_Commit)
@@ -611,5 +722,28 @@
 		return strcmp($a->GetName(), $b->GetName());
 	}
 
+	/**
+	 * CompareCreationEpoch
+	 *
+	 * Compares to tags by creation epoch
+	 *
+	 * @access public
+	 * @static
+	 * @param mixed $a first tag
+	 * @param mixed $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);
+	}
+
 }
 

--- a/include/git/Tree.class.php
+++ b/include/git/Tree.class.php
@@ -114,6 +114,24 @@
 	{
 		$this->contentsRead = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadContentsGit();
+		} else {
+			$this->ReadContentsRaw();
+		}
+
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadContentsGit
+	 *
+	 * Reads the tree contents using the git executable
+	 *
+	 * @access private
+	 */
+	private function ReadContentsGit()
+	{
 		$exe = new GitPHP_GitExe($this->GetProject());
 
 		$args = array();
@@ -157,7 +175,58 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadContentsRaw
+	 *
+	 * Reads the tree contents using the raw git object
+	 *
+	 * @access private
+	 */
+	private function ReadContentsRaw()
+	{
+		$treeData = $this->GetProject()->GetObject($this->hash);
+
+		$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($this->path))
+				$path = $this->path . '/' . $path;
+
+			$obj = null;
+			if ($octmode & 0x4000) {
+				// tree
+				$obj = $this->GetProject()->GetTree($hash);
+			} else {
+				// blob
+				$obj = $this->GetProject()->GetBlob($hash);
+			}
+
+			if (!$obj) {
+				continue;
+			}
+
+			$obj->SetMode($mode);
+			$obj->SetPath($path);
+			if ($this->commit)
+				$obj->SetCommit($this->commit);
+			$this->contents[] = $obj;
+		}
 	}
 
 	/**

--- a/templates/treelist.tpl
+++ b/templates/treelist.tpl
@@ -34,7 +34,7 @@
       <td class="link">
         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$treeitem->GetHash()}&amp;hb={$commit->GetHash()}&amp;f={$treeitem->GetPath()}">{t}tree{/t}</a>
 	 | 
-	<a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=snapshot&amp;h={$commit->GetHash()}&amp;f={$treeitem->GetPath()}" class="snapshotTip">{t}snapshot{/t}</a>
+	<a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=snapshot&amp;h={$treeitem->GetHash()}&amp;f={$treeitem->GetPath()}" class="snapshotTip">{t}snapshot{/t}</a>
       </td>
     {/if}
   </tr>

comments