Cache packfile index offsets
Cache packfile index offsets

--- a/config/gitphp.conf.defaults.php
+++ b/config/gitphp.conf.defaults.php
@@ -138,6 +138,17 @@
 /*********************************************************
  * Features
  */
+
+/*
+ * compat
+ * Set this to true to turn on compatibility mode.  This will cause
+ * GitPHP to rely more on the git executable for loading data,
+ * which will bypass some of the limitations of PHP at the expense
+ * of performance.
+ * Turn this on if you are experiencing issues viewing data for
+ * your projects.
+ */
+$gitphp_conf['compat'] = false;
 
 /*
  * compressformat

--- a/config/gitphp.conf.php.example
+++ b/config/gitphp.conf.php.example
@@ -36,4 +36,15 @@
  */
 //$gitphp_conf['objectcache'] = true;
 
+/*
+ * compat
+ * Set this to true to turn on compatibility mode.  This will cause
+ * GitPHP to rely more on the git executable for loading data,
+ * which will bypass some of the limitations of PHP at the expense
+ * of performance.
+ * Turn this on if you are experiencing issues viewing data for
+ * your projects.
+ */
+$gitphp_conf['compat'] = false;
 
+

--- a/include/Util.class.php
+++ b/include/Util.class.php
@@ -25,21 +25,75 @@
 	 * @access public
 	 * @static
 	 * @param string $path path to add slash to
-	 * @param $backslash true to also check for backslash (windows paths)
+	 * @param $filesystem true if this is a filesystem path (to also check for backslash for windows paths)
 	 * @return string $path with a trailing slash
 	 */
-	public static function AddSlash($path, $backslash = true)
+	public static function AddSlash($path, $filesystem = true)
 	{
 		if (empty($path))
 			return $path;
 
 		$end = substr($path, -1);
 
-		if (!(( ($end == '/') || ($end == ':')) || ($backslash && (strtoupper(substr(PHP_OS, 0, 3))) && ($end == '\\'))))
-			$path .= '/';
+		if (!(( ($end == '/') || ($end == ':')) || ($filesystem && GitPHP_Util::IsWindows() && ($end == '\\')))) {
+			if (GitPHP_Util::IsWindows() && $filesystem) {
+				$path .= '\\';
+			} else {
+				$path .= '/';
+			}
+		}
 
 		return $path;
 	}
 
+	/**
+	 * IsWindows
+	 *
+	 * Tests if this is running on windows
+	 *
+	 * @access public
+	 * @static
+	 * @return bool true if on windows
+	 */
+	public static function IsWindows()
+	{
+		return (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
+	}
+
+	/**
+	 * Is64Bit
+	 *
+	 * Tests if this is a 64 bit machine
+	 *
+	 * @access public
+	 * @static
+	 * @return bool true if on 64 bit
+	 */
+	public function Is64Bit()
+	{
+		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);
 	}
 
 	/**
@@ -259,7 +270,7 @@
 
 		$magicdb = GitPHP_Config::GetInstance()->GetValue('magicdb', null);
 		if (empty($magicdb)) {
-			if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+			if (GitPHP_Util::IsWindows()) {
 				$magicdb = 'C:\\wamp\\php\\extras\\magic';
 			} else {
 				$magicdb = '/usr/share/misc/magic';
@@ -290,7 +301,7 @@
 	 */
 	private function FileMime_File()
 	{
-		if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+		if (GitPHP_Util::IsWindows()) {
 			return '';
 		}
 

--- a/include/git/Commit.class.php
+++ b/include/git/Commit.class.php
@@ -523,34 +523,40 @@
 	{
 		$this->dataRead = true;
 
-		/* get data from git_rev_list */
-		$exe = new GitPHP_GitExe($this->GetProject());
-		$args = array();
-		$args[] = '--header';
-		$args[] = '--parents';
-		$args[] = '--max-count=1';
-		$args[] = $this->hash;
-		$ret = $exe->Execute(GIT_REV_LIST, $args);
-		unset($exe);
-
-		$lines = explode("\n", $ret);
-
-		if (!isset($lines[0]))
-			return;
-
-		/* In case we returned something unexpected */
-		$tok = strtok($lines[0], ' ');
-		if ($tok != $this->hash)
-			return;
-
-		/* Read all parents */
-		$tok = strtok(' ');
-		while ($tok !== false) {
-			try {
-				$this->parents[] = $this->GetProject()->GetCommit($tok);
-			} catch (Exception $e) {
-			}
-			$tok = strtok(' ');
+		$lines = null;
+
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+
+			/* get data from git_rev_list */
+			$exe = new GitPHP_GitExe($this->GetProject());
+			$args = array();
+			$args[] = '--header';
+			$args[] = '--parents';
+			$args[] = '--max-count=1';
+			$args[] = $this->hash;
+			$ret = $exe->Execute(GIT_REV_LIST, $args);
+			unset($exe);
+
+			$lines = explode("\n", $ret);
+
+			if (!isset($lines[0]))
+				return;
+
+			/* In case we returned something unexpected */
+			$tok = strtok($lines[0], ' ');
+			if ($tok != $this->hash)
+				return;
+
+			array_shift($lines);
+
+		} else {
+			
+			$data = $this->GetProject()->GetObject($this->hash);
+			if (empty($data))
+				return;
+
+			$lines = explode("\n", $data);
+
 		}
 
 		foreach ($lines as $i => $line) {
@@ -562,6 +568,12 @@
 						$tree->SetCommit($this);
 						$this->tree = $tree;
 					}
+				} catch (Exception $e) {
+				}
+			} else if (preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
+				/* Parent */
+				try {
+					$this->parents[] = $this->GetProject()->GetCommit($regs[1]);
 				} catch (Exception $e) {
 				}
 			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
@@ -576,14 +588,12 @@
 				$this->committerTimezone = $regs[3];
 			} else {
 				/* commit comment */
-				if (!(preg_match('/^[0-9a-fA-F]{40}/', $line) || preg_match('/^parent [0-9a-fA-F]{40}/', $line))) {
-					$trimmed = trim($line);
-					if (empty($this->title) && (strlen($trimmed) > 0))
-						$this->title = $trimmed;
-					if (!empty($this->title)) {
-						if ((strlen($trimmed) > 0) || ($i < (count($lines)-1)))
-							$this->comment[] = $trimmed;
-					}
+				$trimmed = trim($line);
+				if (empty($this->title) && (strlen($trimmed) > 0))
+					$this->title = $trimmed;
+				if (!empty($this->title)) {
+					if ((strlen($trimmed) > 0) || ($i < (count($lines)-1)))
+						$this->comment[] = $trimmed;
 				}
 			}
 		}
@@ -734,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();
@@ -756,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);
+			}
+		}
 	}
 
 	/**
@@ -1004,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/DiffExe.class.php
+++ b/include/git/DiffExe.class.php
@@ -239,11 +239,10 @@
 	 */
 	public static function DefaultBinary()
 	{
-		if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+		if (GitPHP_Util::IsWindows()) {
 			// windows
 
-			$arch = php_uname('m');
-			if (strpos($arch, '64') !== false) {
+			if (GitPHP_Util::Is64Bit()) {
 				// match x86_64 and x64 (64 bit)
 				// C:\Program Files (x86)\Git\bin\diff.exe
 				return 'C:\\Progra~2\\Git\\bin\\diff.exe';

--- 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/GitExe.class.php
+++ b/include/git/GitExe.class.php
@@ -250,11 +250,10 @@
 	 */
 	public static function DefaultBinary()
 	{
-		if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+		if (GitPHP_Util::IsWindows()) {
 			// windows
 
-			$arch = php_uname('m');
-			if (strpos($arch, '64') !== false) {
+			if (GitPHP_Util::Is64Bit()) {
 				// match x86_64 and x64 (64 bit)
 				// C:\Program Files (x86)\Git\bin\git.exe
 				return 'C:\\Progra~2\\Git\\bin\\git.exe';

--- 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);
 	}
 
 }

--- /dev/null
+++ b/include/git/Pack.class.php
@@ -1,1 +1,570 @@
-
+<?php
+/**
+ * GitPHP Pack
+ *
+ * Extracts data from a pack
+ * Based on code from Glip by Patrik Fimml
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2011 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
+
+/**
+ * Pack class
+ *
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_Pack
+{
+
+	const OBJ_COMMIT = 1;
+	const OBJ_TREE = 2;
+	const OBJ_BLOB = 3;
+	const OBJ_TAG = 4;
+	const OBJ_OFS_DELTA = 6;
+	const OBJ_REF_DELTA = 7;
+
+	/**
+	 * project
+	 *
+	 * Stores the project internally
+	 *
+	 * @access protected
+	 */
+	protected $project;
+
+	/**
+	 * hash
+	 *
+	 * Stores the hash of the pack
+	 *
+	 * @access protected
+	 */
+	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
+	 *
+	 * @access public
+	 * @param mixed $project the project
+	 * @param string $hash pack hash
+	 * @return mixed pack object
+	 * @throws Exception exception on invalid hash
+	 */
+	public function __construct($project, $hash)
+	{
+		if (!(preg_match('/[0-9A-Fa-f]{40}/', $hash))) {
+			throw new Exception(sprintf(__('Invalid hash %1$s'), $hash));
+		}
+		$this->hash = $hash;
+		$this->project = $project;
+
+		if (!file_exists($project->GetPath() . '/objects/pack/pack-' . $hash . '.idx')) {
+			throw new Exception('Pack index does not exist');
+		}
+		if (!file_exists($project->GetPath() . '/objects/pack/pack-' . $hash . '.pack')) {
+			throw new Exception('Pack file does not exist');
+		}
+	}
+
+	/**
+	 * GetHash
+	 *
+	 * Gets the hash
+	 *
+	 * @access public
+	 * @return string object hash
+	 */
+	public function GetHash()
+	{
+		return $this->hash;
+	}
+
+	/**
+	 * ContainsObject
+	 *
+	 * Checks if an object exists in the pack
+	 *
+	 * @access public
+	 * @param string $hash object hash
+	 * @return boolean true if object is in pack
+	 */
+	public function ContainsObject($hash)
+	{
+		if (!preg_match('/[0-9a-fA-F]{40}/', $hash)) {
+			return false;
+		}
+
+		return $this->FindPackedObject($hash) !== false;
+	}
+
+	/**
+	 * FindPackedObject
+	 *
+	 * Searches for an object's offset in the index
+	 *
+	 * @return int offset
+	 * @param string $hash hash
+	 * @access private
+	 */
+	private function FindPackedObject($hash)
+	{
+		if (!preg_match('/[0-9a-fA-F]{40}/', $hash)) {
+			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($indexFile, 'rb');
+		flock($index, LOCK_SH);
+
+		$magic = fread($index, 4);
+		if ($magic == "\xFFtOc") {
+			$version = GitPHP_Pack::fuint32($index);
+			if ($version == 2) {
+				$offset = $this->SearchIndexV2($index, $hash);
+			}
+		} else {
+			$offset = $this->SearchIndexV1($index, $hash);
+		}
+		flock($index, LOCK_UN);
+		fclose($index);
+		$this->offsetCache[$hash] = $offset;
+		return $offset;
+	}
+
+	/**
+	 * SearchIndexV1
+	 *
+	 * Seraches a version 1 index for a hash
+	 *
+	 * @access private
+	 * @param resource $index file pointer to index
+	 * @param string $hash hash to find
+	 * @return int pack offset if found
+	 */
+	private function SearchIndexV1($index, $hash)
+	{
+		/*
+		 * index v1 struture:
+		 * fanout table - 256*4 bytes
+		 * offset/sha table - 24*count bytes (4 byte offset + 20 byte sha for each index)
+		 */
+
+		$binaryHash = pack('H40', $hash);
+
+		/*
+		 * get the start/end indices to search
+		 * from the fanout table
+		 */
+		list($low, $high) = $this->ReadFanout($index, $binaryHash, 0);
+
+		if ($low == $high) {
+			return false;
+		}
+
+		/*
+		 * binary serach for the index of the hash in the sha/offset listing
+		 * between cur and after from the fanout
+		 */
+		while ($low <= $high) {
+			$mid = ($low + $high) >> 1;
+			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);
+			
+			if ($cmp < 0) {
+				$high = $mid - 1;
+			} else if ($cmp > 0) {
+				$low = $mid + 1;
+			} else {
+				return $off;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * SearchIndexV2
+	 *
+	 * Seraches a version 2 index for a hash
+	 *
+	 * @access private
+	 * @param resource $index file pointer to index
+	 * @param string $hash hash to find
+	 * @return int pack offset if found
+	 */
+	private function SearchIndexV2($index, $hash)
+	{
+		/*
+		 * index v2 structure:
+		 * magic and version - 2*4 bytes
+		 * fanout table - 256*4 bytes
+		 * sha listing - 20*count bytes
+		 * crc checksums - 4*count bytes
+		 * offsets - 4*count bytes
+		 */
+		$binaryHash = pack('H40', $hash);
+
+		/*
+		 * get the start/end indices to search
+		 * from the fanout table
+		 */
+		list($low, $high) = $this->ReadFanout($index, $binaryHash, 8);
+		if ($low == $high) {
+			return false;
+		}
+
+		/*
+		 * get the object count from fanout[255]
+		 */
+		fseek($index, 8 + 4*255);
+		$objectCount = GitPHP_Pack::fuint32($index);
+
+		/*
+		 * binary search for the index of the hash in the sha listing
+		 * between cur and after from the fanout
+		 */
+		$objIndex = false;
+		while ($low <= $high) {
+			$mid = ($low + $high) >> 1;
+			fseek($index, 8 + 4*256 + 20*$mid);
+
+			$binName = fread($index, 20);
+			$name = bin2hex($binName);
+
+			$cmp = strcmp($hash, $name);
+
+			if ($cmp < 0) {
+				$high = $mid - 1;
+			} else if ($cmp > 0) {
+				$low = $mid + 1;
+			} else {
+				$objIndex = $mid;
+				break;
+			}
+		}
+		if ($objIndex === false) {
+			return false;
+		}
+
+		/*
+		 * get the offset from the same index in the offset table
+		 */
+		fseek($index, 8 + 4*256 + 24*$objectCount + 4*$objIndex);
+		$offset = GitPHP_Pack::fuint32($index);
+		if ($offset & 0x80000000) {
+			throw new Exception('64-bit offsets not implemented');
+		}
+		return $offset;
+	}
+
+	/**
+	 * ReadFanout
+	 *
+	 * Finds the start/end index a hash will be located between,
+	 * acconding to the fanout table
+	 *
+	 * @access private 
+	 * @param resource $index index file pointer
+	 * @param string $binaryHash binary encoded hash to find
+	 * @param int $offset offset in the index file where the fanout table is located
+	 * @return array Range where object can be located
+	 */
+	private function ReadFanout($index, $binaryHash, $offset)
+	{
+		/*
+		 * fanout table has 255 4-byte integers
+		 * indexed by the first byte of the object name.
+		 * the value at that index is the index at which objects
+		 * starting with that byte can be found
+		 * (first level fan-out)
+		 */
+		if ($binaryHash{0} == "\x00") {
+			$low = 0;
+			fseek($index, $offset);
+			$high = GitPHP_Pack::fuint32($index);
+		} else {
+			fseek($index, $offset + (ord($binaryHash{0}) - 1) * 4);
+			$low = GitPHP_Pack::fuint32($index);
+			$high = GitPHP_Pack::fuint32($index);
+		}
+		return array($low, $high);
+	}
+
+	/**
+	 * GetObject
+	 *
+	 * Extracts an object from the pack
+	 *
+	 * @access public
+	 * @param string $hash hash of object to extract
+	 * @param int $type output parameter, returns the type of the object
+	 * @return string object content, or false if not found
+	 */
+	public function GetObject($hash, &$type = 0)
+	{
+		$offset = $this->FindPackedObject($hash);
+		if ($offset === false) {
+			return false;
+		}
+
+		$pack = fopen($this->project->GetPath() . '/objects/pack/pack-' . $this->hash . '.pack', 'rb');
+		flock($pack, LOCK_SH);
+
+		$magic = fread($pack, 4);
+		$version = GitPHP_Pack::fuint32($pack);
+		if ($magic != 'PACK' || $version != 2) {
+			flock($pack, LOCK_UN);
+			fclose($pack);
+			throw new Exception('Unsupported pack format');
+		}
+
+		list($type, $data) = $this->UnpackObject($pack, $offset);
+
+		flock($pack, LOCK_UN);
+		fclose($pack);
+		return $data;
+	}
+
+	/**
+	 * UnpackObject
+	 *
+	 * Extracts an object at an offset
+	 *
+	 * @access private
+	 * @param resource $pack pack file pointer
+	 * @param int $offset object offset
+	 * @return array object type and data
+	 */
+	private function UnpackObject($pack, $offset)
+	{
+		fseek($pack, $offset);
+
+		/*
+		 * object header:
+		 * first byte is the type (high 3 bits) and low byte of size (lower 4 bits)
+		 * subsequent bytes each have 7 next higher bits of the size (little endian)
+		 * most significant bit is either 1 or 0 to indicate whether the next byte
+		 * should be read as part of the size.  1 means continue reading the size,
+		 * 0 means the data is starting
+		 */
+		$c = ord(fgetc($pack));
+		$type = ($c >> 4) & 0x07;
+		$size = $c & 0x0F;
+		for ($i = 4; $c & 0x80; $i += 7) {
+			$c = ord(fgetc($pack));
+			$size |= (($c & 0x7f) << $i);
+		}
+
+		if ($type == GitPHP_Pack::OBJ_COMMIT || $type == GitPHP_Pack::OBJ_TREE || $type == GitPHP_Pack::OBJ_BLOB || $type == GitPHP_Pack::OBJ_TAG) {
+			/*
+			 * regular gzipped object data
+			 */
+			return array($type, gzuncompress(fread($pack, $size+512), $size));
+		} else if ($type == GitPHP_Pack::OBJ_OFS_DELTA) {
+			/*
+			 * delta of an object at offset
+			 */
+			$buf = fread($pack, $size+512+20);
+
+			/*
+			 * read the base object offset
+			 * each subsequent byte's 7 least significant bits
+			 * are part of the offset in decreasing significance per byte
+			 * (opposite of other places)
+			 * most significant bit is a flag indicating whether to read the
+			 * next byte as part of the offset
+			 */
+			$pos = 0;
+			$off = -1;
+			do {
+				$off++;
+				$c = ord($buf{$pos++});
+				$off = ($off << 7) + ($c & 0x7f);
+			} while ($c & 0x80);
+
+			/*
+			 * next read the compressed delta data
+			 */
+			$delta = gzuncompress(substr($buf, $pos), $size);
+			unset($buf);
+
+			$baseOffset = $offset - $off;
+			if ($baseOffset > 0) {
+				/*
+				 * read base object at offset and apply delta to it
+				 */
+				list($type, $base) = $this->UnpackObject($pack, $baseOffset);
+				$data = GitPHP_Pack::ApplyDelta($delta, $base);
+				return array($type, $data);
+			}
+		} else if ($type == GitPHP_Pack::OBJ_REF_DELTA) {
+			/*
+			 * delta of object with hash
+			 */
+
+			/*
+			 * first the base object's hash
+			 * load that object
+			 */
+			$hash = fread($pack, 20);
+			$hash = bin2hex($hash);
+			$base = $this->project->GetObject($hash, $type);
+
+			/*
+			 * then the gzipped delta data
+			 */
+			$delta = gzuncompress(fread($pack, $size + 512), $size);
+
+			$data = GitPHP_Pack::ApplyDelta($delta, $base);
+
+			return array($type, $data);
+		}
+
+		return false;
+	}
+
+	/**
+	 * ApplyDelta
+	 *
+	 * Applies a binary delta to a base object
+	 *
+	 * @static
+	 * @access private
+	 * @param string $delta delta string
+	 * @param string $base base object data
+	 * @return string patched content
+	 */
+	private static function ApplyDelta($delta, $base)
+	{
+		/*
+		 * algorithm from patch-delta.c
+		 */
+		$pos = 0;
+		$baseSize = GitPHP_Pack::ParseVarInt($delta, $pos);
+		$resultSize = GitPHP_Pack::ParseVarInt($delta, $pos);
+
+		$data = '';
+		$deltalen = strlen($delta);
+		while ($pos < $deltalen) {
+			$opcode = ord($delta{$pos++});
+			if ($opcode & 0x80) {
+				$off = 0;
+				if ($opcode & 0x01) $off = ord($delta{$pos++});
+				if ($opcode & 0x02) $off |= ord($delta{$pos++}) <<  8;
+				if ($opcode & 0x04) $off |= ord($delta{$pos++}) << 16;
+				if ($opcode & 0x08) $off |= ord($delta{$pos++}) << 24;
+				$len = 0;
+				if ($opcode & 0x10) $len = ord($delta{$pos++});
+				if ($opcode & 0x20) $len |= ord($delta{$pos++}) <<  8;
+				if ($opcode & 0x40) $len |= ord($delta{$pos++}) << 16;
+				if ($len == 0) $len = 0x10000;
+				$data .= substr($base, $off, $len);
+			} else if ($opcode > 0) {
+				$data .= substr($delta, $pos, $opcode);
+				$pos += $opcode;
+			}
+		}
+		return $data;
+	}
+
+	/**
+	 * ParseVarInt
+	 *
+	 * Reads a git-style packed variable length integer
+	 * sequence of bytes, where each byte's 7 less significant bits
+	 * are pieces of the int in increasing significance for each byte (little endian)
+	 * the most significant bit of each byte is a flag whether to continue
+	 * reading bytes or not
+	 *
+	 * @access private
+	 * @static
+	 * @param string $str packed data string
+	 * @param int $pos position in string to read from
+	 * @return int parsed integer
+	 */
+	private static function ParseVarInt($str, &$pos=0)
+	{
+		$ret = 0;
+		$byte = 0x80;
+		for ($shift = 0; $byte & 0x80; $shift += 7) {
+			$byte = ord($str{$pos++});
+			$ret |= (($byte & 0x7F) << $shift);
+		}
+		return $ret;
+	}
+
+	/**
+	 * uint32
+	 *
+	 * Unpacks a packed 32 bit integer
+	 *
+	 * @static
+	 * @access private
+	 * @return int integer
+	 * @param string $str binary data
+	 */
+	private static function uint32($str)
+	{
+		$a = unpack('Nx', substr($str, 0, 4));
+		return $a['x'];
+	}
+
+	/**
+	 * fuint32
+	 *
+	 * Reads and unpacks the next 32 bit integer
+	 *
+	 * @static
+	 * @access private
+	 * @return int integer
+	 * @param resource $handle file handle
+	 */
+	private static function fuint32($handle)
+	{
+		return GitPHP_Pack::uint32(fread($handle, 4));
+	}
+}
+

--- a/include/git/Project.class.php
+++ b/include/git/Project.class.php
@@ -14,6 +14,7 @@
 require_once(GITPHP_GITOBJECTDIR . 'Commit.class.php');
 require_once(GITPHP_GITOBJECTDIR . 'Head.class.php');
 require_once(GITPHP_GITOBJECTDIR . 'Tag.class.php');
+require_once(GITPHP_GITOBJECTDIR . 'Pack.class.php');
 
 /**
  * Project class
@@ -25,6 +26,15 @@
 {
 
 	/**
+	 * projectRoot
+	 *
+	 * Stores the project root internally
+	 *
+	 * @access protected
+	 */
+	protected $projectRoot;
+
+	/**
 	 * project
 	 *
 	 * Stores the project internally
@@ -43,13 +53,13 @@
 	protected $owner = "";
 
 	/**
-	 * readOwner
+	 * ownerRead
 	 *
 	 * Stores whether the file owner has been read
 	 *
 	 * @access protected
 	 */
-	protected $readOwner = false;
+	protected $ownerRead = false;
 
 	/**
 	 * description
@@ -189,15 +199,36 @@
 	protected $commitCache = array();
 
 	/**
+	 * packs
+	 *
+	 * Stores the list of packs
+	 *
+	 * @access protected
+	 */
+	protected $packs = array();
+
+	/**
+	 * packsRead
+	 *
+	 * Stores whether packs have been read
+	 *
+	 * @access protected
+	 */
+	protected $packsRead = false;
+
+	/**
 	 * __construct
 	 *
 	 * 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);
 	}
 
@@ -211,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)) {
@@ -249,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)
@@ -302,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
@@ -311,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);
 	}
 
 	/**
@@ -332,9 +439,7 @@
 	 */
 	public function GetPath()
 	{
-		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
-
-		return $projectRoot . $this->project;
+		return $this->projectRoot . $this->project;
 	}
 
 	/**
@@ -564,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();
+			}
+		}
 	}
 
 	/**
@@ -741,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';
@@ -779,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
@@ -792,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';
@@ -818,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
@@ -830,6 +1141,9 @@
 	{
 		if (empty($tag))
 			return null;
+
+		if (!$this->readRefs)
+			$this->ReadRefList();
 
 		$key = 'refs/tags/' . $tag;
 
@@ -877,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';
@@ -903,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
@@ -916,6 +1268,9 @@
 		if (empty($head))
 			return null;
 
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
 		$key = 'refs/heads/' . $head;
 
 		if (!isset($this->heads[$key])) {
@@ -930,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);
 	}
@@ -954,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;
 	}
 
@@ -1204,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();
@@ -1221,5 +1668,103 @@
 		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;
+		}
+	}
+
+	/**
+	 * GetObject
+	 *
+	 * Gets the raw content of an object
+	 *
+	 * @access public
+	 * @param string $hash object hash
+	 * @return string object data
+	 */
+	public function GetObject($hash, &$type = 0)
+	{
+		if (!preg_match('/^[0-9A-Fa-f]{40}$/', $hash)) {
+			return false;
+		}
+
+		// first check if it's unpacked
+		$path = $this->GetPath() . '/objects/' . substr($hash, 0, 2) . '/' . substr($hash, 2);
+		if (file_exists($path)) {
+			list($header, $data) = explode("\0", gzuncompress(file_get_contents($path)), 2);
+			sscanf($header, "%s %d", $typestr, $size);
+			switch ($typestr) {
+				case 'commit':
+					$type = GitPHP_Pack::OBJ_COMMIT;
+					break;
+				case 'tree':
+					$type = GitPHP_Pack::OBJ_TREE;
+					break;
+				case 'blob':
+					$type = GitPHP_Pack::OBJ_BLOB;
+					break;
+				case 'tag':
+					$type = GitPHP_Pack::OBJ_TAG;
+					break;
+			}
+			return $data;
+		}
+
+		if (!$this->packsRead) {
+			$this->ReadPacks();
+		}
+
+		// then try packs
+		foreach ($this->packs as $pack) {
+			$data = $pack->GetObject($hash, $type);
+			if ($data !== false) {
+				return $data;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * ReadPacks
+	 *
+	 * Read the list of packs in the repository
+	 *
+	 * @access private
+	 */
+	private function ReadPacks()
+	{
+		$dh = opendir($this->GetPath() . '/objects/pack');
+		if ($dh !== false) {
+			while (($file = readdir($dh)) !== false) {
+				if (preg_match('/^pack-([0-9A-Fa-f]{40})\.idx$/', $file, $regs)) {
+					$this->packs[] = new GitPHP_Pack($this, $regs[1]);
+				}
+			}
+		}
+		$this->packsRead = true;
+	}
+
 }
 

--- 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/TmpDir.class.php
+++ b/include/git/TmpDir.class.php
@@ -107,7 +107,7 @@
 
 		if (empty($tmpdir)) {
 			// ultimate default - should never get this far
-			if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+			if (GitPHP_Util::IsWindows()) {
 				$tmpdir = 'C:\\Windows\\Temp';
 			} else {
 				$tmpdir = '/tmp';

--- 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