Use the shared smarty cache instead of handling it manually
Use the shared smarty cache instead of handling it manually

This cuts down the code and allows us to use memcache if configured.
Since the list of projects is background-level data (and not HTML
pages), it uses the object cache. But since it's not immutable like git
objects, the lifetime uses the regular cache lifetime setting, which is
generally set shorter than the object cache lifetime (that could even be
set to -1).

--- a/config/gitphp.conf.defaults.php
+++ b/config/gitphp.conf.defaults.php
@@ -138,6 +138,31 @@
 /*********************************************************
  * 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;
+
+/**
+ * largeskip
+ * When GitPHP is reading through the history for pages of the shortlog/log
+ * beyond the first, it needs to read from the tip but skip a number of commits
+ * for the previous pages.  The more commits it needs to skip, the longer it takes.
+ * Calling the git executable is faster when skipping a large number of commits,
+ * ie reading a log page significantly beyond the first.  This determines
+ * the threshold at which GitPHP will fall back to using the git exe for the log.
+ * Currently each log page shows 100 commits, so this would be calculated at
+ * page number * 100.  So for example at the default of 200, pages 0-2 would be
+ * loaded natively and pages 3+ would fall back on the git exe.
+ */
+$gitphp_conf['largeskip'] = 200;
 
 /*
  * compressformat
@@ -369,11 +394,18 @@
 
 /*
  * debug
- * Turns on extra warning messages and benchmarking.
+ * Turns on extra warning messages
  * Not recommended for production systems, as it will give
- * way more benchmarking info than you care about, and
+ * way more info about what's happening than you care about, and
  * will screw up non-html output (rss, opml, snapshots, etc)
  */
 $gitphp_conf['debug'] = false;
 
-
+/*
+ * benchmark
+ * Turns on extra timestamp and memory benchmarking info
+ * when debug mode is turned on.  Generates lots of output.
+ */
+$gitphp_conf['benchmark'] = false;
+
+

--- 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/config/projects.conf.php.example
+++ b/config/projects.conf.php.example
@@ -15,7 +15,7 @@
  * git_projects
  * List of projects
  *
- * There are three ways to list projects:
+ * There are several ways to list projects:
  *
  * 1. Array of projects
  */
@@ -37,7 +37,16 @@
 //$git_projects = '/git/projectlist.txt';
 
 /*
- * 3. Leave commented to read all projects in the project root
+ * 3. Path to scm-manager repositories.xml file
+ * Points to the repository config file used by the scm-manager
+ * program.  In order for this to work, the projectroot for GitPHP
+ * must be the same as the git repository directory configured
+ * in scm-manager
+ */
+//$git_projects = '~/.scm/config/repositories.xml';
+
+/*
+ * 4. Leave commented to read all projects in the project root
  */
 
 
@@ -78,6 +87,12 @@
  *	     setting in the config for this project.  This can also be
  *	     an empty string to override the global bug url to say that
  *	     only this project has no bug url.
+ *
+ * 'compat': whether this project runs in compatibility mode.  (true/false)
+ *	     This overrides the compat setting in the config for this project.
+ *	     Compatibility mode relies more on the git executable for loading
+ *	     data, at the expense of performance.  Use if you are having
+ *	     trouble loading data for this project.
  */
 //$git_projects_settings['php/gitphp.git'] = array(
 //	'category' => 'PHP',
@@ -86,10 +101,12 @@
 //	'cloneurl' => 'http://git.xiphux.com/php/gitphp.git',
 //	'pushurl' => '',
 //	'bugpattern' => '/#([0-9]+)/',
-//	'bugurl' => 'http://mantis.xiphux.com/view.php?id=${1}'
+//	'bugurl' => 'http://mantis.xiphux.com/view.php?id=${1}',
+//	'compat' => false
 //);
 //$git_projects_settings['gentoo.git'] = array(
-//	'description' => 'Gentoo portage overlay'
+//	'description' => 'Gentoo portage overlay',
+//	'compat' => true
 //);
 //$git_projects_settings['core/fbx.git'] = array(
 //	'description' => 'FBX music player',

--- a/css/gitphpskin.css
+++ b/css/gitphpskin.css
@@ -325,6 +325,14 @@
 	color: #880000; 
 }
 
+span.commit_title {
+	font-weight: bold;
+}
+
+span.merge_title {
+	color: #777777;
+}
+
 span.newfile {
 	color: #008000;
 }
@@ -414,6 +422,8 @@
 	float: left;
 	width: 19%;
 	word-wrap: break-word;
+	background-color: #ffffff;
+	border-bottom: 1px solid #edece6;
 }
 
 div.commitDiffSBS div.SBSTOC a

--- a/include/Log.class.php
+++ b/include/Log.class.php
@@ -36,6 +36,15 @@
 	protected $enabled = false;
 
 	/**
+	 * benchmark
+	 *
+	 * Stores whether benchmarking is enabled
+	 *
+	 * @access protected
+	 */
+	protected $benchmark = false;
+
+	/**
 	 * startTime
 	 *
 	 * Stores the starting instant
@@ -94,6 +103,7 @@
 		$this->startMem = memory_get_usage();
 
 		$this->enabled = GitPHP_Config::GetInstance()->GetValue('debug', false);
+		$this->benchmark = GitPHP_Config::GetInstance()->GetValue('benchmark', false);
 	}
 
 	/**
@@ -136,8 +146,12 @@
 			return;
 
 		$entry = array();
-		$entry['time'] = microtime(true);
-		$entry['mem'] = memory_get_usage();
+		
+		if ($this->benchmark) {
+			$entry['time'] = microtime(true);
+			$entry['mem'] = memory_get_usage();
+		}
+
 		$entry['msg'] = $message;
 		$this->entries[] = $entry;
 	}
@@ -166,6 +180,32 @@
 	public function SetEnabled($enable)
 	{
 		$this->enabled = $enable;
+	}
+
+	/**
+	 * GetBenchmark
+	 *
+	 * Gets whether benchmarking is enabled
+	 *
+	 * @access public
+	 * @return boolean true if benchmarking is enabled
+	 */
+	public function GetBenchmark()
+	{
+		return $this->benchmark;
+	}
+
+	/**
+	 * SetBenchmark
+	 *
+	 * Sets whether benchmarking is enabled
+	 *
+	 * @access public
+	 * @param boolean $bench true if benchmarking is enabled
+	 */
+	public function SetBenchmark($bench)
+	{
+		$this->benchmark = $bench;
 	}
 
 	/**
@@ -181,21 +221,31 @@
 		$data = array();
 	
 		if ($this->enabled) {
-			$endTime = microtime(true);
-			$endMem = memory_get_usage();
-
-			$lastTime = $this->startTime;
-			$lastMem = $this->startMem;
-
-			$data[] = '[' . $this->startTime . '] [' . $this->startMem . ' bytes] Start';
+
+			if ($this->benchmark) {
+				$endTime = microtime(true);
+				$endMem = memory_get_usage();
+
+				$lastTime = $this->startTime;
+				$lastMem = $this->startMem;
+
+				$data[] = 'DEBUG: [' . $this->startTime . '] [' . $this->startMem . ' bytes] Start';
+
+			}
 
 			foreach ($this->entries as $entry) {
-				$data[] = '[' . $entry['time'] . '] [' . ($entry['time'] - $this->startTime) . ' sec since start] [' . ($entry['time'] - $lastTime) . ' sec since last] [' . $entry['mem'] . ' bytes] [' . ($entry['mem'] - $this->startMem) . ' bytes since start] [' . ($entry['mem'] - $lastMem) . ' bytes since last] ' . $entry['msg'];
-				$lastTime = $entry['time'];
-				$lastMem = $entry['mem'];
+				if ($this->benchmark) {
+					$data[] = 'DEBUG: [' . $entry['time'] . '] [' . ($entry['time'] - $this->startTime) . ' sec since start] [' . ($entry['time'] - $lastTime) . ' sec since last] [' . $entry['mem'] . ' bytes] [' . ($entry['mem'] - $this->startMem) . ' bytes since start] [' . ($entry['mem'] - $lastMem) . ' bytes since last] ' . $entry['msg'];
+					$lastTime = $entry['time'];
+					$lastMem = $entry['mem'];
+				} else {
+					$data[] = 'DEBUG: ' . $entry['msg'];
+				}
 			}
 
-			$data[] = '[' . $endTime . '] [' . ($endTime - $this->startTime) . ' sec since start] [' . ($endTime - $lastTime) . ' sec since last] [' . $endMem . ' bytes] [' . ($endMem - $this->startMem) . ' bytes since start] [' . ($endMem - $lastMem) . ' bytes since last] End';
+			if ($this->benchmark) {
+				$data[] = '[' . $endTime . '] [' . ($endTime - $this->startTime) . ' sec since start] [' . ($endTime - $lastTime) . ' sec since last] [' . $endMem . ' bytes] [' . ($endMem - $this->startMem) . ' bytes since start] [' . ($endMem - $lastMem) . ' bytes since last] End';
+			}
 		}
 
 		return $data;

--- 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 static 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/cache/Cache.class.php
+++ b/include/cache/Cache.class.php
@@ -26,30 +26,34 @@
 	const Template = 'data.tpl';
 
 	/**
-	 * instance
-	 *
-	 * Stores the singleton instance
+	 * objectCacheInstance
+	 *
+	 * Stores the singleton instance of the object cache
 	 *
 	 * @access protected
 	 * @static
 	 */
-	protected static $instance;
-
-	/**
-	 * GetInstance
-	 *
-	 * Return the singleton instance
+	protected static $objectCacheInstance;
+
+	/**
+	 * GetObjectCacheInstance
+	 *
+	 * Return the singleton instance of the object cache
 	 *
 	 * @access public
 	 * @static
 	 * @return mixed instance of cache class
 	 */
-	public static function GetInstance()
-	{
-		if (!self::$instance) {
-			self::$instance = new GitPHP_Cache();
+	public static function GetObjectCacheInstance()
+	{
+		if (!self::$objectCacheInstance) {
+			self::$objectCacheInstance = new GitPHP_Cache();
+			if (GitPHP_Config::GetInstance()->GetValue('objectcache', false)) {
+				self::$objectCacheInstance->SetEnabled(true);
+				self::$objectCacheInstance->SetLifetime(GitPHP_Config::GetInstance()->GetValue('objectcachelifetime', 86400));
+			}
 		}
-		return self::$instance;
+		return self::$objectCacheInstance;
 	}
 
 	/**
@@ -80,8 +84,6 @@
 	 */
 	public function __construct()
 	{
-		if (GitPHP_Config::GetInstance()->GetValue('objectcache', false))
-			$this->SetEnabled(true);
 	}
 
 	/**
@@ -119,6 +121,38 @@
 	}
 
 	/**
+	 * GetLifetime
+	 *
+	 * Gets the cache lifetime
+	 *
+	 * @access public
+	 * @return int cache lifetime in seconds
+	 */
+	public function GetLifetime()
+	{
+		if (!$this->enabled)
+			return false;
+
+		return $this->tpl->cache_lifetime;
+	}
+
+	/**
+	 * SetLifetime
+	 *
+	 * Sets the cache lifetime
+	 *
+	 * @access public
+	 * @param int $lifetime cache lifetime in seconds
+	 */
+	public function SetLifetime($lifetime)
+	{
+		if (!$this->enabled)
+			return;
+
+		$this->tpl->cache_lifetime = $lifetime;
+	}
+
+	/**
 	 * Get
 	 *
 	 * Get an item from the cache
@@ -151,14 +185,21 @@
 	 * @access public
 	 * @param string $key cache key
 	 * @param mixed $value value
-	 */
-	public function Set($key = null, $value = null)
+	 * @param int $lifetime override the lifetime for this data
+	 */
+	public function Set($key = null, $value = null, $lifetime = null)
 	{
 		if (empty($key) || empty($value))
 			return;
 
 		if (!$this->enabled)
 			return;
+
+		$oldLifetime = null;
+		if ($lifetime !== null) {
+			$oldLifetime = $this->tpl->cache_lifetime;
+			$this->tpl->cache_lifetime = $lifetime;
+		}
 
 		$this->Delete($key);
 		$this->tpl->clear_all_assign();
@@ -167,6 +208,10 @@
 		// Force it into smarty's cache
 		$tmp = $this->tpl->fetch(GitPHP_Cache::Template, $key);
 		unset($tmp);
+
+		if ($lifetime !== null) {
+			$this->tpl->cache_lifetime = $oldLifetime;
+		}
 	}
 
 	/**
@@ -235,11 +280,11 @@
 		if ($this->tpl)
 			return;
 
+		require_once(GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('smarty_prefix', 'lib/smarty/libs/')) . 'Smarty.class.php');
 		$this->tpl = new Smarty;
+		$this->tpl->plugins_dir[] = GITPHP_INCLUDEDIR . 'smartyplugins';
 
 		$this->tpl->caching = 2;
-
-		$this->tpl->cache_lifetime = GitPHP_Config::GetInstance()->GetValue('objectcachelifetime', 86400);
 
 		$servers = GitPHP_Config::GetInstance()->GetValue('memcache', null);
 		if (isset($servers) && is_array($servers) && (count($servers) > 0)) {

--- 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,15 +132,19 @@
 	{
 		$this->dataRead = true;
 
-		$exe = new GitPHP_GitExe($this->GetProject());
-
-		$args = array();
-		$args[] = 'blob';
-		$args[] = $this->hash;
-
-		$this->data = $exe->Execute(GIT_CAT_FILE, $args);
-
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		if ($this->GetProject()->GetCompat()) {
+			$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::GetObjectCacheInstance()->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);
 	}
 
 	/**
@@ -208,6 +219,26 @@
 	public function SetSize($size)
 	{
 		$this->size = $size;
+	}
+
+	/**
+	 * IsBinary
+	 *
+	 * Tests if this blob is a binary file
+	 *
+	 * @access public
+	 * @return boolean true if binary file
+	 */
+	public function IsBinary()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		$data = $this->data;
+		if (strlen($this->data) > 8000)
+			$data = substr($data, 0, 8000);
+
+		return strpos($data, chr(0)) !== false;
 	}
 
 	/**
@@ -259,14 +290,14 @@
 
 		$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';
 			}
 		}
 
-		$finfo = finfo_open(FILEINFO_MIME, $magicdb);
+		$finfo = @finfo_open(FILEINFO_MIME, $magicdb);
 		if ($finfo) {
 			$mime = finfo_buffer($finfo, $this->data, FILEINFO_MIME);
 			if ($mime && strpos($mime, '/')) {
@@ -290,7 +321,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
@@ -513,6 +513,22 @@
 	}
 
 	/**
+	 * IsMergeCommit
+	 *
+	 * Returns whether this is a merge commit
+	 *
+	 * @access pubilc
+	 * @return boolean true if merge commit
+	 */
+	public function IsMergeCommit()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return count($this->parents) > 1;
+	}
+
+	/**
 	 * ReadData
 	 *
 	 * Read the data for the commit
@@ -523,38 +539,46 @@
 	{
 		$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 ($this->GetProject()->GetCompat()) {
+
+			/* 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);
+
+		}
+
+		$header = true;
 
 		foreach ($lines as $i => $line) {
-			if (preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
+			if ($header && preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
 				/* Tree */
 				try {
 					$tree = $this->GetProject()->GetTree($regs[1]);
@@ -564,31 +588,36 @@
 					}
 				} catch (Exception $e) {
 				}
-			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+			} else if ($header && preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
+				/* Parent */
+				try {
+					$this->parents[] = $this->GetProject()->GetCommit($regs[1]);
+				} catch (Exception $e) {
+				}
+			} else if ($header && preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
 				/* author data */
 				$this->author = $regs[1];
 				$this->authorEpoch = $regs[2];
 				$this->authorTimezone = $regs[3];
-			} else if (preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+			} else if ($header && preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
 				/* committer data */
 				$this->committer = $regs[1];
 				$this->committerEpoch = $regs[2];
 				$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;
-					}
+				$header = false;
+				$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;
 				}
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -679,7 +708,7 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -734,6 +763,24 @@
 	{
 		$this->hashPathsRead = true;
 
+		if ($this->GetProject()->GetCompat()) {
+			$this->ReadHashPathsGit();
+		} else {
+			$this->ReadHashPathsRaw($this->GetTree());
+		}
+
+		GitPHP_Cache::GetObjectCacheInstance()->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 +803,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 +1078,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
@@ -54,13 +54,11 @@
 	 */
 	public function __construct()
 	{
-		$this->binary = GitPHP_Config::GetInstance()->GetValue('diffbin');
-		if (empty($this->binary)) {
-			if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
-				$this->binary = 'C:\\Progra~1\\Git\\bin\\diff.exe';
-			} else {
-				$this->binary = 'diff';
-			}
+		$binary = GitPHP_Config::GetInstance()->GetValue('diffbin');
+		if (empty($binary)) {
+			$this->binary = GitPHP_DiffExe::DefaultBinary();
+		} else {
+			$this->binary = $binary;
 		}
 
 	}
@@ -241,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';
@@ -256,7 +253,7 @@
 			}
 		} else {
 			// *nix, just use PATH
-			$this->binary = 'diff';
+			return 'diff';
 		}
 	}
 }

--- a/include/git/FileDiff.class.php
+++ b/include/git/FileDiff.class.php
@@ -573,58 +573,66 @@
 			return;
 		}
 
-		$tmpdir = GitPHP_TmpDir::GetInstance();
-
-		$pid = 0;
-		if (function_exists('posix_getpid'))
-			$pid = posix_getpid();
-		else
-			$pid = rand();
-
-		$fromTmpFile = null;
-		$toTmpFile = null;
-
-		$fromName = null;
-		$toName = null;
-
-		if ((empty($this->status)) || ($this->status == 'D') || ($this->status == 'M')) {
-			$fromBlob = $this->GetFromBlob();
-			$fromTmpFile = 'gitphp_' . $pid . '_from';
-			$tmpdir->AddFile($fromTmpFile, $fromBlob->GetData());
-
-			$fromName = 'a/';
-			if (!empty($file)) {
-				$fromName .= $file;
-			} else if (!empty($this->fromFile)) {
-				$fromName .= $this->fromFile;
-			} else {
-				$fromName .= $this->fromHash;
-			}
-		}
-
-		if ((empty($this->status)) || ($this->status == 'A') || ($this->status == 'M')) {
-			$toBlob = $this->GetToBlob();
-			$toTmpFile = 'gitphp_' . $pid . '_to';
-			$tmpdir->AddFile($toTmpFile, $toBlob->GetData());
-
-			$toName = 'b/';
-			if (!empty($file)) {
-				$toName .= $file;
-			} else if (!empty($this->toFile)) {
-				$toName .= $this->toFile;
-			} else {
-				$toName .= $this->toHash;
-			}
-		}
-
-		$this->diffData = GitPHP_DiffExe::Diff((empty($fromTmpFile) ? null : ($tmpdir->GetDir() . $fromTmpFile)), $fromName, (empty($toTmpFile) ? null : ($tmpdir->GetDir() . $toTmpFile)), $toName);
-
-		if (!empty($fromTmpFile)) {
-			$tmpdir->RemoveFile($fromTmpFile);
-		}
-
-		if (!empty($toTmpFile)) {
-			$tmpdir->RemoveFile($toTmpFile);
+		if (function_exists('xdiff_string_diff')) {
+
+			$this->diffData = $this->GetXDiff(3, true, $file);
+
+		} else {
+
+			$tmpdir = GitPHP_TmpDir::GetInstance();
+
+			$pid = 0;
+			if (function_exists('posix_getpid'))
+				$pid = posix_getpid();
+			else
+				$pid = rand();
+
+			$fromTmpFile = null;
+			$toTmpFile = null;
+
+			$fromName = null;
+			$toName = null;
+
+			if ((empty($this->status)) || ($this->status == 'D') || ($this->status == 'M')) {
+				$fromBlob = $this->GetFromBlob();
+				$fromTmpFile = 'gitphp_' . $pid . '_from';
+				$tmpdir->AddFile($fromTmpFile, $fromBlob->GetData());
+
+				$fromName = 'a/';
+				if (!empty($file)) {
+					$fromName .= $file;
+				} else if (!empty($this->fromFile)) {
+					$fromName .= $this->fromFile;
+				} else {
+					$fromName .= $this->fromHash;
+				}
+			}
+
+			if ((empty($this->status)) || ($this->status == 'A') || ($this->status == 'M')) {
+				$toBlob = $this->GetToBlob();
+				$toTmpFile = 'gitphp_' . $pid . '_to';
+				$tmpdir->AddFile($toTmpFile, $toBlob->GetData());
+
+				$toName = 'b/';
+				if (!empty($file)) {
+					$toName .= $file;
+				} else if (!empty($this->toFile)) {
+					$toName .= $this->toFile;
+				} else {
+					$toName .= $this->toHash;
+				}
+			}
+
+			$this->diffData = GitPHP_DiffExe::Diff((empty($fromTmpFile) ? null : escapeshellarg($tmpdir->GetDir() . $fromTmpFile)), $fromName, (empty($toTmpFile) ? null : escapeshellarg($tmpdir->GetDir() . $toTmpFile)), $toName);
+
+			if (!empty($fromTmpFile)) {
+				$tmpdir->RemoveFile($fromTmpFile);
+			}
+
+			if (!empty($toTmpFile)) {
+				$tmpdir->RemoveFile($toTmpFile);
+			}
+
 		}
 
 		if ($explode)
@@ -656,13 +664,17 @@
 
 		$exe = new GitPHP_GitExe($this->project);
 
-		$rawBlob = $exe->Execute(GIT_CAT_FILE,
-			array("blob", $this->fromHash));
-		$blob  = explode("\n", $rawBlob);
-
-		$diffLines = explode("\n", $exe->Execute(GIT_DIFF,
-			array("-U0", $this->fromHash,
-				$this->toHash)));
+		$fromBlob = $this->GetFromBlob();
+		$blob = $fromBlob->GetData(true);
+
+		$diffLines = '';
+		if (function_exists('xdiff_string_diff')) {
+			$diffLines = explode("\n", $this->GetXDiff(0, false));
+		} else {
+			$diffLines = explode("\n", $exe->Execute(GIT_DIFF,
+				array("-U0", $this->fromHash,
+					$this->toHash)));
+		}
 
 		unset($exe);
 
@@ -747,6 +759,65 @@
 	}
 
 	/**
+	 * GetXDiff
+	 *
+	 * Get diff using xdiff
+	 *
+	 * @access private
+	 * @param int $context number of context lines
+	 * @param boolean $header true to include standard diff header
+	 * @param string $file override the file name
+	 * @return string diff content
+	 */
+	private function GetXDiff($context = 3, $header = true, $file = null)
+	{
+		if (!function_exists('xdiff_string_diff'))
+			return '';
+
+		$fromData = '';
+		$toData = '';
+		$isBinary = false;
+		$fromName = '/dev/null';
+		$toName = '/dev/null';
+		if (empty($this->status) || ($this->status == 'M') || ($this->status == 'D')) {
+			$fromBlob = $this->GetFromBlob();
+			$isBinary = $isBinary || $fromBlob->IsBinary();
+			$fromData = $fromBlob->GetData(false);
+			$fromName = 'a/';
+			if (!empty($file)) {
+				$fromName .= $file;
+			} else if (!empty($this->fromFile)) {
+				$fromName .= $this->fromFile;
+			} else {
+				$fromName .= $this->fromHash;
+			}
+		}
+		if (empty($this->status) || ($this->status == 'M') || ($this->status == 'A')) {
+			$toBlob = $this->GetToBlob();
+			$isBinary = $isBinary || $toBlob->IsBinary();
+			$toData = $toBlob->GetData(false);
+			$toName = 'b/';
+			if (!empty($file)) {
+				$toName .= $file;
+			} else if (!empty($this->toFile)) {
+				$toName .= $this->toFile;
+			} else {
+				$toName .= $this->toHash;
+			}
+		}
+		$output = '';
+		if ($isBinary) {
+			$output = sprintf(__('Binary files %1$s and %2$s differ'), $fromName, $toName) . "\n";
+		} else {
+			if ($header) {
+				$output = '--- ' . $fromName . "\n" . '+++ ' . $toName . "\n";
+			}
+			$output .= xdiff_string_diff($fromData, $toData, $context);
+		}
+		return $output;
+	}
+
+	/**
 	 * GetCommit
 	 *
 	 * Gets the commit for this filediff

--- 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
@@ -24,6 +25,17 @@
 class GitPHP_Project
 {
 
+/* internal variables {{{1*/
+
+	/**
+	 * projectRoot
+	 *
+	 * Stores the project root internally
+	 *
+	 * @access protected
+	 */
+	protected $projectRoot;
+
 	/**
 	 * project
 	 *
@@ -33,6 +45,8 @@
 	 */
 	protected $project;
 
+/* owner internal variables {{{2*/
+
 	/**
 	 * owner
 	 *
@@ -43,13 +57,17 @@
 	protected $owner = "";
 
 	/**
-	 * readOwner
+	 * ownerRead
 	 *
 	 * Stores whether the file owner has been read
 	 *
 	 * @access protected
 	 */
-	protected $readOwner = false;
+	protected $ownerRead = false;
+
+/*}}}2*/
+
+/* description internal variables {{{2*/
 
 	/**
 	 * description
@@ -70,6 +88,8 @@
 	 */
 	protected $readDescription = false;
 
+/*}}}2*/
+
 	/**
 	 * category
 	 *
@@ -79,6 +99,8 @@
 	 */
 	protected $category = '';
 
+/* epoch internal variables {{{2*/
+
 	/**
 	 * epoch
 	 *
@@ -97,6 +119,10 @@
 	 */
 	protected $epochRead = false;
 
+/*}}}2*/
+
+/* HEAD internal variables {{{2*/
+
 	/**
 	 * head
 	 *
@@ -115,6 +141,10 @@
 	 */
 	protected $readHeadRef = false;
 
+/*}}}*/
+
+/* ref internal variables {{{2*/
+
 	/**
 	 * tags
 	 *
@@ -142,6 +172,10 @@
 	 */
 	protected $readRefs = false;
 
+/*}}}2*/
+
+/* url internal variables {{{2*/
+
 	/**
 	 * cloneUrl
 	 *
@@ -160,6 +194,10 @@
 	 */
 	protected $pushUrl = null;
 
+/*}}}2*/
+
+/* bugtracker internal variables {{{2*/
+
 	/**
 	 * bugUrl
 	 *
@@ -177,6 +215,8 @@
 	 * @access protected
 	 */
 	protected $bugPattern = null;
+
+/*}}}2*/
 
 	/**
 	 * commitCache
@@ -188,19 +228,77 @@
 	 */
 	protected $commitCache = array();
 
+/* packfile internal variables {{{2*/
+
+	/**
+	 * packs
+	 *
+	 * Stores the list of packs
+	 *
+	 * @access protected
+	 */
+	protected $packs = array();
+
+	/**
+	 * packsRead
+	 *
+	 * Stores whether packs have been read
+	 *
+	 * @access protected
+	 */
+	protected $packsRead = false;
+
+/*}}}2*/
+
+	/**
+	 * compat
+	 *
+	 * Stores whether this project is running
+	 * in compatibility mode
+	 *
+	 * @access protected
+	 */
+	protected $compat = null;
+
+/*}}}1*/
+
+/* class methods {{{1*/
+
 	/**
 	 * __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);
 	}
 
+/*}}}1*/
+
+/* accessors {{{1*/
+
+/* project accessors {{{2*/
+
+	/**
+	 * GetProject
+	 *
+	 * Gets the project
+	 *
+	 * @access public
+	 * @return string the project
+	 */
+	public function GetProject()
+	{
+		return $this->project;
+	}
+
 	/**
 	 * SetProject
 	 *
@@ -211,10 +309,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)) {
@@ -239,6 +335,41 @@
 
 	}
 
+/*}}}2*/
+
+	/**
+	 * GetSlug
+	 *
+	 * Gets the project as a filename/url friendly slug
+	 *
+	 * @access public
+	 * @return string the slug
+	 */
+	public function GetSlug()
+	{
+		$project = $this->project;
+
+		if (substr($project, -4) == '.git')
+			$project = substr($project, 0, -4);
+		
+		return GitPHP_Util::MakeSlug($project);
+	}
+
+	/**
+	 * GetPath
+	 *
+	 * Gets the full project path
+	 *
+	 * @access public
+	 * @return string project path
+	 */
+	public function GetPath()
+	{
+		return $this->projectRoot . $this->project;
+	}
+
+/* owner accessors {{{2 */
+
 	/**
 	 * GetOwner
 	 *
@@ -249,33 +380,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 ($this->GetCompat()) {
+			$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)
@@ -288,54 +487,26 @@
 		$this->owner = $owner;
 	}
 
-	/**
-	 * GetProject
-	 *
-	 * Gets the project
-	 *
-	 * @access public
-	 * @return string the project
-	 */
-	public function GetProject()
-	{
-		return $this->project;
-	}
-
-	/**
-	 * GetSlug
-	 *
-	 * Gets the project as a filename/url friendly slug
-	 *
-	 * @access public
-	 * @return string the slug
-	 */
-	public function GetSlug()
-	{
-		$from = array(
-			'/',
-			'.git'
-		);
-		$to = array(
-			'-',
-			''
-		);
-		return str_replace($from, $to, $this->project);
-	}
-
-	/**
-	 * GetPath
-	 *
-	 * Gets the full project path
-	 *
-	 * @access public
-	 * @return string project path
-	 */
-	public function GetPath()
-	{
-		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
-
-		return $projectRoot . $this->project;
-	}
+/*}}}2*/
+
+/* projectroot accessors {{{2*/
+
+	/**
+	 * GetProjectRoot
+	 *
+	 * Gets the project root
+	 *
+	 * @access public
+	 * @return string the project root
+	 */
+	public function GetProjectRoot()
+	{
+		return $this->projectRoot;
+	}
+
+/*}}}2*/
+
+/* description accessors {{{2*/
 
 	/**
 	 * GetDescription
@@ -349,7 +520,10 @@
 	public function GetDescription($trim = 0)
 	{
 		if (!$this->readDescription) {
-			$this->description = file_get_contents($this->GetPath() . '/description');
+			if (file_exists($this->GetPath() . '/description')) {
+				$this->description = file_get_contents($this->GetPath() . '/description');
+			}
+			$this->readDescription = true;
 		}
 		
 		if (($trim > 0) && (strlen($this->description) > $trim)) {
@@ -373,6 +547,8 @@
 		$this->readDescription = true;
 	}
 
+/*}}}2*/
+
 	/**
 	 * GetDaemonEnabled
 	 *
@@ -385,6 +561,8 @@
 	{
 		return file_exists($this->GetPath() . '/git-daemon-export-ok');
 	}
+
+/* category accessors {{{2*/
 
 	/**
 	 * GetCategory
@@ -412,6 +590,10 @@
 		$this->category = $category;
 	}
 
+/*}}}2*/
+
+/* clone url accessors {{{2*/
+
 	/**
 	 * GetCloneUrl
 	 *
@@ -445,6 +627,10 @@
 		$this->cloneUrl = $cUrl;
 	}
 
+/*}}}2*/
+
+/* push url accessors {{{2*/
+
 	/**
 	 * GetPushUrl
 	 *
@@ -478,6 +664,10 @@
 		$this->pushUrl = $pUrl;
 	}
 
+/*}}}2*/
+
+/* bugtracker accessors {{{2*/
+
 	/**
 	 * GetBugUrl
 	 *
@@ -536,6 +726,10 @@
 		$this->bugPattern = $bPat;
 	}
 
+/*}}}2*/
+
+/* HEAD accessors {{{2*/
+
 	/**
 	 * GetHeadCommit
 	 *
@@ -564,12 +758,203 @@
 	{
 		$this->readHeadRef = true;
 
+		if ($this->GetCompat()) {
+			$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();
+			}
+		}
+	}
+
+/*}}}2*/
+
+/* epoch accessors {{{2*/
+
+	/**
+	 * GetEpoch
+	 *
+	 * Gets this project's epoch
+	 * (time of last change)
+	 *
+	 * @access public
+	 * @return integer timestamp
+	 */
+	public function GetEpoch()
+	{
+		if (!$this->epochRead)
+			$this->ReadEpoch();
+
+		return $this->epoch;
+	}
+
+	/**
+	 * GetAge
+	 *
+	 * Gets this project's age
+	 * (time since most recent change)
+	 *
+	 * @access public
+	 * @return integer age
+	 */
+	public function GetAge()
+	{
+		if (!$this->epochRead)
+			$this->ReadEpoch();
+
+		return time() - $this->epoch;
+	}
+
+	/**
+	 * ReadEpoch
+	 *
+	 * Reads this project's epoch
+	 * (timestamp of most recent change)
+	 *
+	 * @access private
+	 */
+	private function ReadEpoch()
+	{
+		$this->epochRead = true;
+
+		if ($this->GetCompat()) {
+			$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();
+		$args[] = '--format="%(committer)"';
+		$args[] = '--sort=-committerdate';
+		$args[] = '--count=1';
+		$args[] = 'refs/heads';
+
+		$epochstr = trim($exe->Execute(GIT_FOR_EACH_REF, $args));
+
+		if (preg_match('/ (\d+) [-+][01]\d\d\d$/', $epochstr, $regs)) {
+			$this->epoch = $regs[1];
+		}
+
+		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;
+		}
+	}
+
+/*}}}2*/
+
+/* compatibility accessors {{{2*/
+
+	/**
+	 * GetCompat
+	 *
+	 * Gets whether this project is running in compatibility mode
+	 *
+	 * @access public
+	 * @return boolean true if compatibilty mode
+	 */
+	public function GetCompat()
+	{
+		if ($this->compat !== null) {
+			return $this->compat;
+		}
+
+		return GitPHP_Config::GetInstance()->GetValue('compat', false);
+	}
+
+	/**
+	 * SetCompat
+	 *
+	 * Sets whether this project is running in compatibility mode
+	 *
+	 * @access public
+	 * @param boolean true if compatibility mode
+	 */
+	public function SetCompat($compat)
+	{
+		$this->compat = $compat;
+	}
+
+/*}}}2*/
+
+/*}}}1*/
+
+/* data loading methods {{{1*/
+
+/* commit loading methods {{{2*/
 
 	/**
 	 * GetCommit
@@ -606,7 +991,7 @@
 
 			if (!isset($this->commitCache[$hash])) {
 				$cacheKey = 'project|' . $this->project . '|commit|' . $hash;
-				$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+				$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($cacheKey);
 				if ($cached)
 					$this->commitCache[$hash] = $cached;
 				else
@@ -629,87 +1014,9 @@
 		return null;
 	}
 
-	/**
-	 * CompareProject
-	 *
-	 * Compares two projects by project name
-	 *
-	 * @access public
-	 * @static
-	 * @param mixed $a first project
-	 * @param mixed $b second project
-	 * @return integer comparison result
-	 */
-	public static function CompareProject($a, $b)
-	{
-		$catCmp = strcmp($a->GetCategory(), $b->GetCategory());
-		if ($catCmp !== 0)
-			return $catCmp;
-
-		return strcmp($a->GetProject(), $b->GetProject());
-	}
-
-	/**
-	 * CompareDescription
-	 *
-	 * Compares two projects by description
-	 *
-	 * @access public
-	 * @static
-	 * @param mixed $a first project
-	 * @param mixed $b second project
-	 * @return integer comparison result
-	 */
-	public static function CompareDescription($a, $b)
-	{
-		$catCmp = strcmp($a->GetCategory(), $b->GetCategory());
-		if ($catCmp !== 0)
-			return $catCmp;
-
-		return strcmp($a->GetDescription(), $b->GetDescription());
-	}
-
-	/**
-	 * CompareOwner
-	 *
-	 * Compares two projects by owner
-	 *
-	 * @access public
-	 * @static
-	 * @param mixed $a first project
-	 * @param mixed $b second project
-	 * @return integer comparison result
-	 */
-	public static function CompareOwner($a, $b)
-	{
-		$catCmp = strcmp($a->GetCategory(), $b->GetCategory());
-		if ($catCmp !== 0)
-			return $catCmp;
-
-		return strcmp($a->GetOwner(), $b->GetOwner());
-	}
-
-	/**
-	 * CompareAge
-	 *
-	 * Compares two projects by age
-	 *
-	 * @access public
-	 * @static
-	 * @param mixed $a first project
-	 * @param mixed $b second project
-	 * @return integer comparison result
-	 */
-	public static function CompareAge($a, $b)
-	{
-		$catCmp = strcmp($a->GetCategory(), $b->GetCategory());
-		if ($catCmp !== 0)
-			return $catCmp;
-
-		if ($a->GetAge() === $b->GetAge())
-			return 0;
-		return ($a->GetAge() < $b->GetAge() ? -1 : 1);
-	}
+/*}}}2*/
+
+/* ref loading methods {{{2*/
 
 	/**
 	 * GetRefs
@@ -741,10 +1048,26 @@
 	 *
 	 * @access protected
 	 */
-	public function ReadRefList()
+	protected function ReadRefList()
 	{
 		$this->readRefs = true;
 
+		if ($this->GetCompat()) {
+			$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 +1102,121 @@
 	}
 
 	/**
+	 * 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;
+	}
+
+/*}}}2*/
+
+/* tag loading methods {{{2*/
+
+	/**
 	 * GetTags
 	 *
 	 * Gets list of tags for this project by age descending
@@ -792,6 +1230,24 @@
 		if (!$this->readRefs)
 			$this->ReadRefList();
 
+		if ($this->GetCompat()) {
+			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 +1274,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
@@ -831,6 +1308,9 @@
 		if (empty($tag))
 			return null;
 
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
 		$key = 'refs/tags/' . $tag;
 
 		if (!isset($this->tags[$key])) {
@@ -855,7 +1335,7 @@
 			return;
 
 		$cacheKey = 'project|' . $this->project . '|tag|' . $tag;
-		$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+		$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($cacheKey);
 		if ($cached) {
 			return $cached;
 		} else {
@@ -863,6 +1343,10 @@
 		}
 	}
 
+/*}}}2*/
+
+/* head loading methods {{{2*/
+
 	/**
 	 * GetHeads
 	 *
@@ -877,6 +1361,24 @@
 		if (!$this->readRefs)
 			$this->ReadRefList();
 
+		if ($this->GetCompat()) {
+			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 +1405,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 +1438,9 @@
 		if (empty($head))
 			return null;
 
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
 		$key = 'refs/heads/' . $head;
 
 		if (!isset($this->heads[$key])) {
@@ -925,18 +1450,22 @@
 		return $this->heads[$key];
 	}
 
+/*}}}2*/
+
+/* log methods {{{2*/
+
 	/**
 	 * GetLogHash
 	 *
 	 * 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,6 +1483,26 @@
 	 */
 	public function GetLog($hash, $count = 50, $skip = 0)
 	{
+		if ($this->GetCompat() || ($skip > GitPHP_Config::GetInstance()->GetValue('largeskip', 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) {
@@ -963,6 +1512,66 @@
 	}
 
 	/**
+	 * 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;
+	}
+
+/*}}}2*/
+
+/* blob loading methods {{{2*/
+
+	/**
 	 * GetBlob
 	 *
 	 * Gets a blob from this project
@@ -976,13 +1585,17 @@
 			return null;
 
 		$cacheKey = 'project|' . $this->project . '|blob|' . $hash;
-		$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+		$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($cacheKey);
 		if ($cached)
 			return $cached;
 
 		return new GitPHP_Blob($this, $hash);
 	}
 
+/*}}}2*/
+
+/* tree loading methods {{{2*/
+
 	/**
 	 * GetTree
 	 *
@@ -997,12 +1610,94 @@
 			return null;
 
 		$cacheKey = 'project|' . $this->project . '|tree|' . $hash;
-		$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+		$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($cacheKey);
 		if ($cached)
 			return $cached;
 
 		return new GitPHP_Tree($this, $hash);
 	}
+
+/*}}}2*/
+
+/* raw object loading methods {{{2*/
+
+	/**
+	 * 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;
+	}
+
+/*}}}2*/
+
+/*}}}1*/
+
+/* search methods {{{1*/
 
 	/**
 	 * SearchCommit
@@ -1109,6 +1804,10 @@
 		return $ret;
 	}
 
+/*}}}1*/
+
+/* private utilities {{{1*/
+
 	/**
 	 * RevList
 	 *
@@ -1158,68 +1857,93 @@
 		return $revlist;
 	}
 
-	/**
-	 * GetEpoch
-	 *
-	 * Gets this project's epoch
-	 * (time of last change)
-	 *
-	 * @access public
-	 * @return integer timestamp
-	 */
-	public function GetEpoch()
-	{
-		if (!$this->epochRead)
-			$this->ReadEpoch();
-
-		return $this->epoch;
-	}
-
-	/**
-	 * GetAge
-	 *
-	 * Gets this project's age
-	 * (time since most recent change)
-	 *
-	 * @access public
-	 * @return integer age
-	 */
-	public function GetAge()
-	{
-		if (!$this->epochRead)
-			$this->ReadEpoch();
-
-		return time() - $this->epoch;
-	}
-
-	/**
-	 * ReadEpoch
-	 *
-	 * Reads this project's epoch
-	 * (timestamp of most recent change)
-	 *
-	 * @access private
-	 */
-	private function ReadEpoch()
-	{
-		$this->epochRead = true;
-
-		$exe = new GitPHP_GitExe($this);
-
-		$args = array();
-		$args[] = '--format="%(committer)"';
-		$args[] = '--sort=-committerdate';
-		$args[] = '--count=1';
-		$args[] = 'refs/heads';
-
-		$epochstr = trim($exe->Execute(GIT_FOR_EACH_REF, $args));
-
-		if (preg_match('/ (\d+) [-+][01]\d\d\d$/', $epochstr, $regs)) {
-			$this->epoch = $regs[1];
-		}
-
-		unset($exe);
-	}
+/*}}}1*/
+
+/* static utilities {{{1*/
+
+	/**
+	 * CompareProject
+	 *
+	 * Compares two projects by project name
+	 *
+	 * @access public
+	 * @static
+	 * @param mixed $a first project
+	 * @param mixed $b second project
+	 * @return integer comparison result
+	 */
+	public static function CompareProject($a, $b)
+	{
+		$catCmp = strcmp($a->GetCategory(), $b->GetCategory());
+		if ($catCmp !== 0)
+			return $catCmp;
+
+		return strcmp($a->GetProject(), $b->GetProject());
+	}
+
+	/**
+	 * CompareDescription
+	 *
+	 * Compares two projects by description
+	 *
+	 * @access public
+	 * @static
+	 * @param mixed $a first project
+	 * @param mixed $b second project
+	 * @return integer comparison result
+	 */
+	public static function CompareDescription($a, $b)
+	{
+		$catCmp = strcmp($a->GetCategory(), $b->GetCategory());
+		if ($catCmp !== 0)
+			return $catCmp;
+
+		return strcmp($a->GetDescription(), $b->GetDescription());
+	}
+
+	/**
+	 * CompareOwner
+	 *
+	 * Compares two projects by owner
+	 *
+	 * @access public
+	 * @static
+	 * @param mixed $a first project
+	 * @param mixed $b second project
+	 * @return integer comparison result
+	 */
+	public static function CompareOwner($a, $b)
+	{
+		$catCmp = strcmp($a->GetCategory(), $b->GetCategory());
+		if ($catCmp !== 0)
+			return $catCmp;
+
+		return strcmp($a->GetOwner(), $b->GetOwner());
+	}
+
+	/**
+	 * CompareAge
+	 *
+	 * Compares two projects by age
+	 *
+	 * @access public
+	 * @static
+	 * @param mixed $a first project
+	 * @param mixed $b second project
+	 * @return integer comparison result
+	 */
+	public static function CompareAge($a, $b)
+	{
+		$catCmp = strcmp($a->GetCategory(), $b->GetCategory());
+		if ($catCmp !== 0)
+			return $catCmp;
+
+		if ($a->GetAge() === $b->GetAge())
+			return 0;
+		return ($a->GetAge() < $b->GetAge() ? -1 : 1);
+	}
+
+/*}}}1*/
 
 }
 

--- a/include/git/ProjectList.class.php
+++ b/include/git/ProjectList.class.php
@@ -14,6 +14,7 @@
 require_once(GITPHP_GITOBJECTDIR . 'ProjectListFile.class.php');
 require_once(GITPHP_GITOBJECTDIR . 'ProjectListArray.class.php');
 require_once(GITPHP_GITOBJECTDIR . 'ProjectListArrayLegacy.class.php');
+require_once(GITPHP_GITOBJECTDIR . 'ProjectListScmManager.class.php');
 
 /**
  * ProjectList class
@@ -65,10 +66,15 @@
 		if (self::$instance)
 			return;
 
+
 		if (!empty($file) && is_file($file) && include($file)) {
 			if (isset($git_projects)) {
 				if (is_string($git_projects)) {
-					self::$instance = new GitPHP_ProjectListFile($git_projects);
+					if (function_exists('simplexml_load_file') && GitPHP_ProjectListScmManager::IsSCMManager($git_projects)) {
+						self::$instance = new GitPHP_ProjectListScmManager($git_projects);
+					} else {
+						self::$instance = new GitPHP_ProjectListFile($git_projects);
+					}
 				} else if (is_array($git_projects)) {
 					if ($legacy) {
 						self::$instance = new GitPHP_ProjectListArrayLegacy($git_projects);
@@ -79,8 +85,10 @@
 			}
 		}
 
-		if (!self::$instance)
+		if (!self::$instance) {
+
 			self::$instance = new GitPHP_ProjectListDirectory(GitPHP_Config::GetInstance()->GetValue('projectroot'));
+		}
 
 		if (isset($git_projects_settings) && !$legacy)
 			self::$instance->ApplySettings($git_projects_settings);

--- a/include/git/ProjectListArray.class.php
+++ b/include/git/ProjectListArray.class.php
@@ -52,26 +52,29 @@
 	 */
 	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);
 					}
 				}
 			} catch (Exception $e) {
+				GitPHP_Log::GetInstance()->Log($e->getMessage());
 			}
 		}
 	}

--- a/include/git/ProjectListArrayLegacy.class.php
+++ b/include/git/ProjectListArrayLegacy.class.php
@@ -55,15 +55,18 @@
 	 */
 	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;
 					} catch (Exception $e) {
+						GitPHP_Log::GetInstance()->Log($e->getMessage());
 					}
 				}
 			}

--- a/include/git/ProjectListBase.class.php
+++ b/include/git/ProjectListBase.class.php
@@ -295,6 +295,9 @@
 		if (isset($projData['bugurl']) && is_string($projData['bugurl'])) {
 			$projectObj->SetBugUrl($projData['bugurl']);
 		}
+		if (isset($projData['compat'])) {
+			$projectObj->SetCompat($projData['compat']);
+		}
 	}
 
 	/**

--- a/include/git/ProjectListDirectory.class.php
+++ b/include/git/ProjectListDirectory.class.php
@@ -61,7 +61,17 @@
 	 */
 	protected function PopulateProjects()
 	{
+		$key = 'projectdir|' . $this->projectDir . '|projectlist|directory';
+		$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($key);
+		if ($cached && (count($cached) > 0)) {
+			GitPHP_Log::GetInstance()->Log('Loaded ' . count($cached) . ' projects from cache');
+			$this->projects = $cached;
+			return;
+		}
+
 		$this->RecurseDir($this->projectDir);
+
+		GitPHP_Cache::GetObjectCacheInstance()->Set($key, $this->projects, GitPHP_Config::GetInstance()->GetValue('cachelifetime', 3600));
 	}
 
 	/**
@@ -76,24 +86,30 @@
 		if (!is_dir($dir))
 			return;
 
+		GitPHP_Log::GetInstance()->Log(sprintf('Searching directory %1$s', $dir));
+
 		if ($dh = opendir($dir)) {
 			$trimlen = strlen($this->projectDir) + 1;
 			while (($file = readdir($dh)) !== false) {
 				$fullPath = $dir . '/' . $file;
 				if ((strpos($file, '.') !== 0) && is_dir($fullPath)) {
 					if (is_file($fullPath . '/HEAD')) {
+						GitPHP_Log::GetInstance()->Log(sprintf('Found project %1$s', $fullPath));
 						$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;
 							}
 						} catch (Exception $e) {
+							GitPHP_Log::GetInstance()->Log($e->getMessage());
 						}
 					} else {
 						$this->RecurseDir($fullPath);
 					}
+				} else {
+					GitPHP_Log::GetInstance()->Log(sprintf('Skipping %1$s', $fullPath));
 				}
 			}
 			closedir($dh);

--- 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)) {
@@ -72,7 +72,10 @@
 						}
 						$this->projects[$regs[1]] = $projObj;
 					} catch (Exception $e) {
+						GitPHP_Log::GetInstance()->Log($e->getMessage());
 					}
+				} else {
+					GitPHP_Log::GetInstance()->Log(sprintf('%1$s is not a git project', $projectRoot . $regs[1]));
 				}
 			}
 		}

--- /dev/null
+++ b/include/git/ProjectListScmManager.class.php
@@ -1,1 +1,138 @@
+<?php
+/**
+ * GitPHP ProjectListScmManager
+ *
+ * Lists all projects in an scm-manager config file
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2011 Christopher Han
+ * @package GitPHP
+ * @subpackage Git
+ */
 
+require_once(GITPHP_INCLUDEDIR . 'Config.class.php');
+require_once(GITPHP_GITOBJECTDIR . 'ProjectListBase.class.php');
+require_once(GITPHP_GITOBJECTDIR . 'Project.class.php');
+
+/**
+ * ProjectListScmManager class
+ *
+ * @package GitPHP
+ * @subpackage Git
+ */
+class GitPHP_ProjectListScmManager extends GitPHP_ProjectListBase
+{
+	/**
+	 * __construct
+	 *
+	 * constructor
+	 *
+	 * @param string $projectFile file to read
+	 * @throws Exception if parameter is not a readable file
+	 * @access public
+	 */
+	public function __construct($projectFile)
+	{
+		if (!(is_string($projectFile) && is_file($projectFile))) {
+			throw new Exception(sprintf(__('%1$s is not a file'), $projectFile));
+		}
+
+		$this->projectConfig = $projectFile;
+
+		parent::__construct();
+	}
+
+	/**
+	 * PopulateProjects
+	 *
+	 * Populates the internal list of projects
+	 *
+	 * @access protected
+	 * @throws Exception if file cannot be read
+	 */
+	protected function PopulateProjects()
+	{
+		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
+
+		$use_errors = libxml_use_internal_errors(true);
+
+		$xml = simplexml_load_file($this->projectConfig);
+
+		libxml_clear_errors();
+		libxml_use_internal_errors($use_errors);
+
+		if (!$xml) {
+			throw new Exception(sprintf('Could not load SCM manager config %1$s', $this->projectConfig));
+		}
+
+		foreach ($xml->repositories->repository as $repository) {
+
+			if ($repository->type != 'git') {
+				GitPHP_Log::GetInstance()->Log(sprintf('%1$s is not a git project', $repository->name));
+				continue;
+			}
+
+			if ($repository->public != 'true') {
+				GitPHP_Log::GetInstance()->Log(sprintf('%1$s is not public', $repository->name));
+				continue;
+			}
+
+			$projName = trim($repository->name);
+			if (empty($projName))
+				continue;
+
+			if (is_file($projectRoot . $projName . '/HEAD')) {
+				try {
+					$projObj = new GitPHP_Project($projectRoot, $projName);
+					$projOwner = trim($repository->contact);
+					if (!empty($projOwner)) {
+						$projObj->SetOwner($projOwner);
+					}
+					$projDesc = trim($repository->description);
+					if (!empty($projDesc)) {
+						$projObj->SetDescription($projDesc);
+					}
+					$this->projects[$projName] = $projObj;
+				} catch (Exception $e) {
+					GitPHP_Log::GetInstance()->Log($e->getMessage());
+				}
+			} else {
+				GitPHP_Log::GetInstance()->Log(sprintf('%1$s is not a git project', $projName));
+			}
+		}
+	}
+
+	/**
+	 * IsSCMManager
+	 *
+	 * Tests if this file is an SCM manager config file
+	 *
+	 * @access protected
+	 * @returns true if file is an SCM manager config
+	 */
+	public static function IsSCMManager($file)
+	{
+		if (empty($file))
+			return false;
+
+		if (!(is_string($file) && is_file($file)))
+			return false;
+
+		$use_errors = libxml_use_internal_errors(true);
+
+		$xml = simplexml_load_file($file);
+
+		libxml_clear_errors();
+		libxml_use_internal_errors($use_errors);
+
+		if (!$xml)
+			return false;
+
+		if ($xml->getName() !== 'repository-db')
+			return false;
+
+		return true;
+	}
+
+}
+

--- a/include/git/Tag.class.php
+++ b/include/git/Tag.class.php
@@ -170,6 +170,14 @@
 				$this->DereferenceCommit();
 		}
 
+		if (!$this->commit) {
+			if ($this->object instanceof GitPHP_Commit) {
+				$this->commit = $this->object;
+			} else if ($this->object instanceof GitPHP_Tag) {
+				$this->commit = $this->object->GetCommit();
+			}
+		}
+
 		return $this->commit;
 	}
 
@@ -322,6 +330,24 @@
 	{
 		$this->dataRead = true;
 
+		if ($this->GetProject()->GetCompat()) {
+			$this->ReadDataGit();
+		} else {
+			$this->ReadDataRaw();
+		}
+
+		GitPHP_Cache::GetObjectCacheInstance()->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';
@@ -333,7 +359,7 @@
 			$this->object = $this->GetProject()->GetCommit($this->GetHash());
 			$this->commit = $this->object;
 			$this->type = 'commit';
-			GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+			GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 			return;
 		}
 
@@ -406,8 +432,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::GetObjectCacheInstance()->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;
+		}
 	}
 
 	/**
@@ -436,7 +538,7 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -580,6 +682,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 +718,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 +730,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 ($this->GetProject()->GetCompat()) {
+			$this->ReadContentsGit();
+		} else {
+			$this->ReadContentsRaw();
+		}
+
+		GitPHP_Cache::GetObjectCacheInstance()->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/include/version.php
+++ b/include/version.php
@@ -12,7 +12,7 @@
 /**
  * Defines the version
  */
-$gitphp_version = "0.2.3";
+$gitphp_version = "0.2.4";
 
 /**
  * Defines the app string (app name and version)

file:a/index.php -> file:b/index.php
--- a/index.php
+++ b/index.php
@@ -25,6 +25,8 @@
 define('GITPHP_CONTROLLERDIR', GITPHP_INCLUDEDIR . 'controller/');
 define('GITPHP_CACHEDIR', GITPHP_INCLUDEDIR . 'cache/');
 define('GITPHP_LOCALEDIR', GITPHP_BASEDIR . 'locale/');
+
+define('GITPHP_CACHE', GITPHP_BASEDIR . 'cache/');
 
 include_once(GITPHP_INCLUDEDIR . 'version.php');
 
@@ -130,9 +132,11 @@
 	if (!$exe->Valid()) {
 		throw new GitPHP_MessageException(sprintf(__('Could not run the git executable "%1$s".  You may need to set the "%2$s" config value.'), $exe->GetBinary(), 'gitbin'), true, 500);
 	}
-	$exe = new GitPHP_DiffExe();
-	if (!$exe->Valid()) {
-		throw new GitPHP_MessageException(sprintf(__('Could not run the diff executable "%1$s".  You may need to set the "%2$s" config value.'), $exe->GetBinary(), 'diffbin'), true, 500);
+	if (!function_exists('xdiff_string_diff')) {
+		$exe = new GitPHP_DiffExe();
+		if (!$exe->Valid()) {
+			throw new GitPHP_MessageException(sprintf(__('Could not run the diff executable "%1$s".  You may need to set the "%2$s" config value.'), $exe->GetBinary(), 'diffbin'), true, 500);
+		}
 	}
 	unset($exe);
 
@@ -182,7 +186,7 @@
 if (GitPHP_Log::GetInstance()->GetEnabled()) {
 	$entries = GitPHP_Log::GetInstance()->GetEntries();
 	foreach ($entries as $logline) {
-		echo "\n" . $logline;
+		echo "<br />\n" . $logline;
 	}
 }
 

--- a/locale/gitphp.pot
+++ b/locale/gitphp.pot
@@ -6,9 +6,9 @@
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: GitPHP 0.2.1\n"
+"Project-Id-Version: GitPHP 0.2.4\n"
 "Report-Msgid-Bugs-To: xiphux@gmail.com\n"
-"POT-Creation-Date: 2010-12-02 22:09-0600\n"
+"POT-Creation-Date: 2011-07-22 23:42-0500\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -110,10 +110,14 @@
 msgstr ""
 
 # Used as a link to a side-by-side version of a diff
+#: templates/blobdiff.tpl
+#: templates/commitdiff.tpl
 msgid "side by side"
 msgstr ""
 
 # Used as a link to a unified version of a diff
+#: templates/blobdiff.tpl
+#: templates/commitdiff.tpl
 msgid "unified"
 msgstr ""
 
@@ -147,7 +151,7 @@
 #: templates/log.tpl
 #: templates/history.tpl
 #: templates/shortloglist.tpl
-#: include/controller/Controller_Commitdiff.class.php:79
+#: include/controller/Controller_Commitdiff.class.php:85
 msgid "commitdiff"
 msgstr ""
 
@@ -169,6 +173,7 @@
 # Comes before a list of files
 # %1: the number of files
 #: templates/commit.tpl
+#: templates/commitdiff.tpl
 msgid "%1 file changed:"
 msgid_plural "%1 files changed:"
 msgstr[0] ""
@@ -288,14 +293,14 @@
 
 # Link back to the list of projects
 #: templates/header.tpl
-#: include/controller/ControllerBase.class.php:250
+#: include/controller/ControllerBase.class.php:257
 #: include/controller/Controller_ProjectList.class.php:94
 msgid "projects"
 msgstr ""
 
 # Used as a search type, to search the contents of files in the project
 #: templates/header.tpl
-#: include/git/Blob.class.php:174
+#: include/git/Blob.class.php:178
 msgid "file"
 msgstr ""
 
@@ -428,8 +433,8 @@
 #: include/controller/Controller_Tag.class.php:34
 #: include/controller/Controller_Tags.class.php:34
 #: include/controller/Controller_Project.class.php:33
-#: include/controller/Controller_Commitdiff.class.php:34
-#: include/controller/Controller_Blobdiff.class.php:34
+#: include/controller/Controller_Commitdiff.class.php:36
+#: include/controller/Controller_Blobdiff.class.php:36
 #: include/controller/Controller_History.class.php:34
 #: include/controller/Controller_Heads.class.php:34
 #: include/controller/Controller_Search.class.php:47
@@ -443,7 +448,7 @@
 msgstr ""
 
 # Used as link to and title for a diff of a single file
-#: include/controller/Controller_Blobdiff.class.php:79
+#: include/controller/Controller_Blobdiff.class.php:81
 msgid "blobdiff"
 msgstr ""
 
@@ -497,45 +502,45 @@
 msgstr ""
 
 # A type of filesystem object stored in a project
-#: include/git/Blob.class.php:162
+#: include/git/Blob.class.php:166
 msgid "directory"
 msgstr ""
 
 # A type of filesystem object stored in a project
-#: include/git/Blob.class.php:168
+#: include/git/Blob.class.php:172
 msgid "symlink"
 msgstr ""
 
 # Used when an object is stored in a project but git doesn't know what type it is
-#: include/git/Blob.class.php:181
+#: include/git/Blob.class.php:185
 msgid "unknown"
 msgstr ""
 
 # Error message when user specifies a path for a project root or project, but the path given isn't a directory
 # %1$s: the path the user specified
 #: include/git/ProjectListDirectory.class.php:47
-#: include/git/Project.class.php:230
+#: include/git/Project.class.php:250
 #, php-format
 msgid "%1$s is not a directory"
 msgstr ""
 
 # Error message when a path specified in the config is not a git repository
 # %1$s: the specified path
-#: include/git/Project.class.php:234
+#: include/git/Project.class.php:254
 #, php-format
 msgid "%1$s is not a git repository"
 msgstr ""
 
 # Error message when a path specified is using '..' to break out of the project root (a hack attempt)
 # %1$s: The specified path
-#: include/git/Project.class.php:238
+#: include/git/Project.class.php:258
 #, php-format
 msgid "%1$s is attempting directory traversal"
 msgstr ""
 
 # Error message when a path specified is outside of the project root
 # %1$s: The specified path
-#: include/git/Project.class.php:244
+#: include/git/Project.class.php:264
 #, php-format
 msgid "%1$s is outside of the projectroot"
 msgstr ""
@@ -582,7 +587,7 @@
 
 # Error message when a hash specified in a URL isn't a valid git hash
 # %1$s: the hash entered
-#: include/git/GitObject.class.php:107
+#: include/git/Pack.class.php:80 include/git/GitObject.class.php:107
 #, php-format
 msgid "Invalid hash %1$s"
 msgstr ""
@@ -768,10 +773,25 @@
 # Error message displayed when the diff executable isn't found or doesn't work
 # %1$s: the diff executable the system is trying to run
 # %2$s: the config value the user needs to set to specify the correct path
-#: index.php:135
+#: index.php:136
 #, php-format
 msgid ""
 "Could not run the diff executable \"%1$s\".  You may need to set the \"%2$s"
 "\" config value."
 msgstr ""
 
+# Link displayed in commitdiff view, when the user has filtered
+# the display to a single file using the list of changed files.
+# This will go back to showing all files in the commitdiff
+#: templates/commitdiff.tpl
+msgid "(show all)"
+msgstr ""
+
+# Message displayed when diffing two binary files.
+# %1$s: the filename of the first file
+# %2$s: the filename of the second file
+#: include/git/FileDiff.class.php:810
+#, php-format
+msgid "Binary files %1$s and %2$s differ"
+msgstr ""
+

--- a/templates/commitdiff.tpl
+++ b/templates/commitdiff.tpl
@@ -38,7 +38,7 @@
      <div class="SBSTOC">
        <ul>
        <li class="listcount">
-       {t count=$treediff->Count() 1=$treediff->Count() plural="%1 files changed:"}%1 file changed:{/t} <a href="#" class="showAll">(show all)</a></li>
+       {t count=$treediff->Count() 1=$treediff->Count() plural="%1 files changed:"}%1 file changed:{/t} <a href="#" class="showAll">{t}(show all){/t}</a></li>
        {foreach from=$treediff item=filediff}
        <li>
        <a href="#{$filediff->GetFromHash()}_{$filediff->GetToHash()}" class="SBSTOCItem">

--- a/templates/log.tpl
+++ b/templates/log.tpl
@@ -44,7 +44,8 @@
    </div>
    <div class="title_text">
      <div class="log_link">
-       <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}">{t}commit{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commitdiff&amp;h={$rev->GetHash()}">{t}commitdiff{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$rev->GetHash()}&amp;hb={$rev->GetHash()}">{t}tree{/t}</a>
+       {assign var=revtree value=$rev->GetTree()}
+       <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}">{t}commit{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commitdiff&amp;h={$rev->GetHash()}">{t}commitdiff{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$revtree->GetHash()}&amp;hb={$rev->GetHash()}">{t}tree{/t}</a>
        <br />
        {if $mark}
          {if $mark->GetHash() == $rev->GetHash()}

--- a/templates/shortloglist.tpl
+++ b/templates/shortloglist.tpl
@@ -15,11 +15,14 @@
        <td title="{if $rev->GetAge() > 60*60*24*7*2}{$rev->GetAge()|agestring}{else}{$rev->GetCommitterEpoch()|date_format:"%Y-%m-%d"}{/if}"><em>{if $rev->GetAge() > 60*60*24*7*2}{$rev->GetCommitterEpoch()|date_format:"%Y-%m-%d"}{else}{$rev->GetAge()|agestring}{/if}</em></td>
        <td><em>{$rev->GetAuthorName()}</em></td>
        <td>
-         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}" class="list commitTip" {if strlen($rev->GetTitle()) > 50}title="{$rev->GetTitle()|htmlspecialchars}"{/if}><strong>{$rev->GetTitle(50)|escape}</strong></a>
+         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}" class="list commitTip" {if strlen($rev->GetTitle()) > 50}title="{$rev->GetTitle()|htmlspecialchars}"{/if}>
+         {if $rev->IsMergeCommit()}<span class="merge_title">{else}<span class="commit_title">{/if}{$rev->GetTitle(50)|escape}</span>
+         </a>
 	 {include file='refbadges.tpl' commit=$rev}
        </td>
        <td class="link">
-         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}">{t}commit{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commitdiff&amp;h={$rev->GetHash()}">{t}commitdiff{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$rev->GetHash()}&amp;hb={$rev->GetHash()}">{t}tree{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=snapshot&amp;h={$rev->GetHash()}" class="snapshotTip">{t}snapshot{/t}</a>
+         {assign var=revtree value=$rev->GetTree()}
+         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}">{t}commit{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commitdiff&amp;h={$rev->GetHash()}">{t}commitdiff{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$revtree->GetHash()}&amp;hb={$rev->GetHash()}">{t}tree{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=snapshot&amp;h={$rev->GetHash()}" class="snapshotTip">{t}snapshot{/t}</a>
 	 {if $source == 'shortlog'}
 	  | 
 	  {if $mark}

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