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
@@ -149,6 +149,20 @@
  * 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
@@ -380,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/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;
 }

--- 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
@@ -69,10 +69,31 @@
 	 * @static
 	 * @return bool true if on 64 bit
 	 */
-	public function Is64Bit()
+	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
@@ -132,7 +132,7 @@
 	{
 		$this->dataRead = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetProject()->GetCompat()) {
 			$exe = new GitPHP_GitExe($this->GetProject());
 
 			$args = array();
@@ -144,7 +144,7 @@
 			$this->data = $this->GetProject()->GetObject($this->hash);
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -222,6 +222,26 @@
 	}
 
 	/**
+	 * 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;
+	}
+
+	/**
 	 * FileMime
 	 *
 	 * Get the file mimetype
@@ -277,7 +297,7 @@
 			}
 		}
 
-		$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, '/')) {

--- 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
@@ -525,7 +541,7 @@
 
 		$lines = null;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetProject()->GetCompat()) {
 
 			/* get data from git_rev_list */
 			$exe = new GitPHP_GitExe($this->GetProject());
@@ -559,8 +575,10 @@
 
 		}
 
+		$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]);
@@ -570,24 +588,25 @@
 					}
 				} catch (Exception $e) {
 				}
-			} else if (preg_match('/^parent ([0-9a-fA-F]{40})$/', $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 (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+			} 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 */
+				$header = false;
 				$trimmed = trim($line);
 				if (empty($this->title) && (strlen($trimmed) > 0))
 					$this->title = $trimmed;
@@ -598,7 +617,7 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -689,7 +708,7 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -744,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();
@@ -766,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);
+			}
+		}
 	}
 
 	/**

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

--- a/include/git/Project.class.php
+++ b/include/git/Project.class.php
@@ -25,6 +25,17 @@
 class GitPHP_Project
 {
 
+/* internal variables {{{1*/
+
+	/**
+	 * projectRoot
+	 *
+	 * Stores the project root internally
+	 *
+	 * @access protected
+	 */
+	protected $projectRoot;
+
 	/**
 	 * project
 	 *
@@ -34,6 +45,8 @@
 	 */
 	protected $project;
 
+/* owner internal variables {{{2*/
+
 	/**
 	 * owner
 	 *
@@ -51,6 +64,10 @@
 	 * @access protected
 	 */
 	protected $ownerRead = false;
+
+/*}}}2*/
+
+/* description internal variables {{{2*/
 
 	/**
 	 * description
@@ -71,6 +88,8 @@
 	 */
 	protected $readDescription = false;
 
+/*}}}2*/
+
 	/**
 	 * category
 	 *
@@ -80,6 +99,8 @@
 	 */
 	protected $category = '';
 
+/* epoch internal variables {{{2*/
+
 	/**
 	 * epoch
 	 *
@@ -98,6 +119,10 @@
 	 */
 	protected $epochRead = false;
 
+/*}}}2*/
+
+/* HEAD internal variables {{{2*/
+
 	/**
 	 * head
 	 *
@@ -116,6 +141,10 @@
 	 */
 	protected $readHeadRef = false;
 
+/*}}}*/
+
+/* ref internal variables {{{2*/
+
 	/**
 	 * tags
 	 *
@@ -143,6 +172,10 @@
 	 */
 	protected $readRefs = false;
 
+/*}}}2*/
+
+/* url internal variables {{{2*/
+
 	/**
 	 * cloneUrl
 	 *
@@ -161,6 +194,10 @@
 	 */
 	protected $pushUrl = null;
 
+/*}}}2*/
+
+/* bugtracker internal variables {{{2*/
+
 	/**
 	 * bugUrl
 	 *
@@ -178,6 +215,8 @@
 	 * @access protected
 	 */
 	protected $bugPattern = null;
+
+/*}}}2*/
 
 	/**
 	 * commitCache
@@ -189,6 +228,8 @@
 	 */
 	protected $commitCache = array();
 
+/* packfile internal variables {{{2*/
+
 	/**
 	 * packs
 	 *
@@ -207,19 +248,57 @@
 	 */
 	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
 	 *
@@ -230,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)) {
@@ -258,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
 	 *
@@ -284,7 +396,7 @@
 	 */
 	protected function ReadOwner()
 	{
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetCompat()) {
 			$this->ReadOwnerGit();
 		} else {
 			$this->ReadOwnerRaw();
@@ -375,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
@@ -436,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)) {
@@ -460,6 +547,8 @@
 		$this->readDescription = true;
 	}
 
+/*}}}2*/
+
 	/**
 	 * GetDaemonEnabled
 	 *
@@ -472,6 +561,8 @@
 	{
 		return file_exists($this->GetPath() . '/git-daemon-export-ok');
 	}
+
+/* category accessors {{{2*/
 
 	/**
 	 * GetCategory
@@ -499,6 +590,10 @@
 		$this->category = $category;
 	}
 
+/*}}}2*/
+
+/* clone url accessors {{{2*/
+
 	/**
 	 * GetCloneUrl
 	 *
@@ -532,6 +627,10 @@
 		$this->cloneUrl = $cUrl;
 	}
 
+/*}}}2*/
+
+/* push url accessors {{{2*/
+
 	/**
 	 * GetPushUrl
 	 *
@@ -565,6 +664,10 @@
 		$this->pushUrl = $pUrl;
 	}
 
+/*}}}2*/
+
+/* bugtracker accessors {{{2*/
+
 	/**
 	 * GetBugUrl
 	 *
@@ -623,6 +726,10 @@
 		$this->bugPattern = $bPat;
 	}
 
+/*}}}2*/
+
+/* HEAD accessors {{{2*/
+
 	/**
 	 * GetHeadCommit
 	 *
@@ -651,7 +758,7 @@
 	{
 		$this->readHeadRef = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetCompat()) {
 			$this->ReadHeadCommitGit();
 		} else {
 			$this->ReadHeadCommitRaw();
@@ -697,6 +804,157 @@
 			}
 		}
 	}
+
+/*}}}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
@@ -733,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
@@ -756,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
@@ -872,7 +1052,7 @@
 	{
 		$this->readRefs = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetCompat()) {
 			$this->ReadRefListGit();
 		} else {
 			$this->ReadRefListRaw();
@@ -1032,6 +1212,10 @@
 		return $files;
 	}
 
+/*}}}2*/
+
+/* tag loading methods {{{2*/
+
 	/**
 	 * GetTags
 	 *
@@ -1046,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';
@@ -1072,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
@@ -1112,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 {
@@ -1120,6 +1343,10 @@
 		}
 	}
 
+/*}}}2*/
+
+/* head loading methods {{{2*/
+
 	/**
 	 * GetHeads
 	 *
@@ -1134,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';
@@ -1160,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
@@ -1184,6 +1449,10 @@
 
 		return $this->heads[$key];
 	}
+
+/*}}}2*/
+
+/* log methods {{{2*/
 
 	/**
 	 * GetLogHash
@@ -1214,7 +1483,7 @@
 	 */
 	public function GetLog($hash, $count = 50, $skip = 0)
 	{
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false) || ($skip > 200)) {
+		if ($this->GetCompat() || ($skip > GitPHP_Config::GetInstance()->GetValue('largeskip', 200)) ) {
 			return $this->GetLogGit($hash, $count, $skip);
 		} else {
 			return $this->GetLogRaw($hash, $count, $skip);
@@ -1298,6 +1567,10 @@
 		return $log;
 	}
 
+/*}}}2*/
+
+/* blob loading methods {{{2*/
+
 	/**
 	 * GetBlob
 	 *
@@ -1312,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
 	 *
@@ -1333,271 +1610,16 @@
 			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);
 	}
 
-	/**
-	 * SearchCommit
-	 *
-	 * Gets a list of commits with commit messages matching the given pattern
-	 *
-	 * @access public
-	 * @param string $pattern search pattern
-	 * @param string $hash hash to start searching from
-	 * @param integer $count number of results to get
-	 * @param integer $skip number of results to skip
-	 * @return array array of matching commits
-	 */
-	public function SearchCommit($pattern, $hash = 'HEAD', $count = 50, $skip = 0)
-	{
-		if (empty($pattern))
-			return;
-
-		$args = array();
-
-		$exe = new GitPHP_GitExe($this);
-		if ($exe->CanIgnoreRegexpCase())
-			$args[] = '--regexp-ignore-case';
-		unset($exe);
-
-		$args[] = '--grep=\'' . $pattern . '\'';
-
-		$ret = $this->RevList($hash, $count, $skip, $args);
-		$len = count($ret);
-
-		for ($i = 0; $i < $len; ++$i) {
-			$ret[$i] = $this->GetCommit($ret[$i]);
-		}
-		return $ret;
-	}
-
-	/**
-	 * SearchAuthor
-	 *
-	 * Gets a list of commits with authors matching the given pattern
-	 *
-	 * @access public
-	 * @param string $pattern search pattern
-	 * @param string $hash hash to start searching from
-	 * @param integer $count number of results to get
-	 * @param integer $skip number of results to skip
-	 * @return array array of matching commits
-	 */
-	public function SearchAuthor($pattern, $hash = 'HEAD', $count = 50, $skip = 0)
-	{
-		if (empty($pattern))
-			return;
-
-		$args = array();
-
-		$exe = new GitPHP_GitExe($this);
-		if ($exe->CanIgnoreRegexpCase())
-			$args[] = '--regexp-ignore-case';
-		unset($exe);
-
-		$args[] = '--author=\'' . $pattern . '\'';
-
-		$ret = $this->RevList($hash, $count, $skip, $args);
-		$len = count($ret);
-
-		for ($i = 0; $i < $len; ++$i) {
-			$ret[$i] = $this->GetCommit($ret[$i]);
-		}
-		return $ret;
-	}
-
-	/**
-	 * SearchCommitter
-	 *
-	 * Gets a list of commits with committers matching the given pattern
-	 *
-	 * @access public
-	 * @param string $pattern search pattern
-	 * @param string $hash hash to start searching from
-	 * @param integer $count number of results to get
-	 * @param integer $skip number of results to skip
-	 * @return array array of matching commits
-	 */
-	public function SearchCommitter($pattern, $hash = 'HEAD', $count = 50, $skip = 0)
-	{
-		if (empty($pattern))
-			return;
-
-		$args = array();
-
-		$exe = new GitPHP_GitExe($this);
-		if ($exe->CanIgnoreRegexpCase())
-			$args[] = '--regexp-ignore-case';
-		unset($exe);
-
-		$args[] = '--committer=\'' . $pattern . '\'';
-
-		$ret = $this->RevList($hash, $count, $skip, $args);
-		$len = count($ret);
-
-		for ($i = 0; $i < $len; ++$i) {
-			$ret[$i] = $this->GetCommit($ret[$i]);
-		}
-		return $ret;
-	}
-
-	/**
-	 * RevList
-	 *
-	 * Common code for using rev-list command
-	 *
-	 * @access private
-	 * @param string $hash hash to list from
-	 * @param integer $count number of results to get
-	 * @param integer $skip number of results to skip
-	 * @param array $args args to give to rev-list
-	 * @return array array of hashes
-	 */
-	private function RevList($hash, $count = 50, $skip = 0, $args = array())
-	{
-		if ($count < 1)
-			return;
-
-		$exe = new GitPHP_GitExe($this);
-
-		$canSkip = true;
-		
-		if ($skip > 0)
-			$canSkip = $exe->CanSkip();
-
-		if ($canSkip) {
-			$args[] = '--max-count=' . $count;
-			if ($skip > 0) {
-				$args[] = '--skip=' . $skip;
-			}
-		} else {
-			$args[] = '--max-count=' . ($count + $skip);
-		}
-
-		$args[] = $hash;
-
-		$revlist = explode("\n", $exe->Execute(GIT_REV_LIST, $args));
-
-		if (!$revlist[count($revlist)-1]) {
-			/* the last newline creates a null entry */
-			array_splice($revlist, -1, 1);
-		}
-
-		if (($skip > 0) && (!$exe->CanSkip())) {
-			return array_slice($revlist, $skip, $count);
-		}
-
-		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;
-
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
-			$this->ReadEpochGit();
-		} else {
-			$this->ReadEpochRaw();
-		}
-	}
-
-	/**
-	 * ReadEpochGit
-	 *
-	 * Reads this project's epoch using git executable
-	 *
-	 * @access private
-	 */
-	private function ReadEpochGit()
-	{
-		$exe = new GitPHP_GitExe($this);
-
-		$args = array();
-		$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*/
+
+/* raw object loading methods {{{2*/
 
 	/**
 	 * GetObject
@@ -1671,5 +1693,257 @@
 		$this->packsRead = true;
 	}
 
+/*}}}2*/
+
+/*}}}1*/
+
+/* search methods {{{1*/
+
+	/**
+	 * SearchCommit
+	 *
+	 * Gets a list of commits with commit messages matching the given pattern
+	 *
+	 * @access public
+	 * @param string $pattern search pattern
+	 * @param string $hash hash to start searching from
+	 * @param integer $count number of results to get
+	 * @param integer $skip number of results to skip
+	 * @return array array of matching commits
+	 */
+	public function SearchCommit($pattern, $hash = 'HEAD', $count = 50, $skip = 0)
+	{
+		if (empty($pattern))
+			return;
+
+		$args = array();
+
+		$exe = new GitPHP_GitExe($this);
+		if ($exe->CanIgnoreRegexpCase())
+			$args[] = '--regexp-ignore-case';
+		unset($exe);
+
+		$args[] = '--grep=\'' . $pattern . '\'';
+
+		$ret = $this->RevList($hash, $count, $skip, $args);
+		$len = count($ret);
+
+		for ($i = 0; $i < $len; ++$i) {
+			$ret[$i] = $this->GetCommit($ret[$i]);
+		}
+		return $ret;
+	}
+
+	/**
+	 * SearchAuthor
+	 *
+	 * Gets a list of commits with authors matching the given pattern
+	 *
+	 * @access public
+	 * @param string $pattern search pattern
+	 * @param string $hash hash to start searching from
+	 * @param integer $count number of results to get
+	 * @param integer $skip number of results to skip
+	 * @return array array of matching commits
+	 */
+	public function SearchAuthor($pattern, $hash = 'HEAD', $count = 50, $skip = 0)
+	{
+		if (empty($pattern))
+			return;
+
+		$args = array();
+
+		$exe = new GitPHP_GitExe($this);
+		if ($exe->CanIgnoreRegexpCase())
+			$args[] = '--regexp-ignore-case';
+		unset($exe);
+
+		$args[] = '--author=\'' . $pattern . '\'';
+
+		$ret = $this->RevList($hash, $count, $skip, $args);
+		$len = count($ret);
+
+		for ($i = 0; $i < $len; ++$i) {
+			$ret[$i] = $this->GetCommit($ret[$i]);
+		}
+		return $ret;
+	}
+
+	/**
+	 * SearchCommitter
+	 *
+	 * Gets a list of commits with committers matching the given pattern
+	 *
+	 * @access public
+	 * @param string $pattern search pattern
+	 * @param string $hash hash to start searching from
+	 * @param integer $count number of results to get
+	 * @param integer $skip number of results to skip
+	 * @return array array of matching commits
+	 */
+	public function SearchCommitter($pattern, $hash = 'HEAD', $count = 50, $skip = 0)
+	{
+		if (empty($pattern))
+			return;
+
+		$args = array();
+
+		$exe = new GitPHP_GitExe($this);
+		if ($exe->CanIgnoreRegexpCase())
+			$args[] = '--regexp-ignore-case';
+		unset($exe);
+
+		$args[] = '--committer=\'' . $pattern . '\'';
+
+		$ret = $this->RevList($hash, $count, $skip, $args);
+		$len = count($ret);
+
+		for ($i = 0; $i < $len; ++$i) {
+			$ret[$i] = $this->GetCommit($ret[$i]);
+		}
+		return $ret;
+	}
+
+/*}}}1*/
+
+/* private utilities {{{1*/
+
+	/**
+	 * RevList
+	 *
+	 * Common code for using rev-list command
+	 *
+	 * @access private
+	 * @param string $hash hash to list from
+	 * @param integer $count number of results to get
+	 * @param integer $skip number of results to skip
+	 * @param array $args args to give to rev-list
+	 * @return array array of hashes
+	 */
+	private function RevList($hash, $count = 50, $skip = 0, $args = array())
+	{
+		if ($count < 1)
+			return;
+
+		$exe = new GitPHP_GitExe($this);
+
+		$canSkip = true;
+		
+		if ($skip > 0)
+			$canSkip = $exe->CanSkip();
+
+		if ($canSkip) {
+			$args[] = '--max-count=' . $count;
+			if ($skip > 0) {
+				$args[] = '--skip=' . $skip;
+			}
+		} else {
+			$args[] = '--max-count=' . ($count + $skip);
+		}
+
+		$args[] = $hash;
+
+		$revlist = explode("\n", $exe->Execute(GIT_REV_LIST, $args));
+
+		if (!$revlist[count($revlist)-1]) {
+			/* the last newline creates a null entry */
+			array_splice($revlist, -1, 1);
+		}
+
+		if (($skip > 0) && (!$exe->CanSkip())) {
+			return array_slice($revlist, $skip, $count);
+		}
+
+		return $revlist;
+	}
+
+/*}}}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,13 +330,13 @@
 	{
 		$this->dataRead = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetProject()->GetCompat()) {
 			$this->ReadDataGit();
 		} else {
 			$this->ReadDataRaw();
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -351,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;
 		}
 
@@ -442,7 +450,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;
 		}
 
@@ -530,7 +538,7 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -674,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
@@ -703,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/Tree.class.php
+++ b/include/git/Tree.class.php
@@ -114,13 +114,13 @@
 	{
 		$this->contentsRead = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetProject()->GetCompat()) {
 			$this->ReadContentsGit();
 		} else {
 			$this->ReadContentsRaw();
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**

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.3\n"
+"Project-Id-Version: GitPHP 0.2.4\n"
 "Report-Msgid-Bugs-To: xiphux@gmail.com\n"
-"POT-Creation-Date: 2011-06-18 21:03-0500\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"
@@ -300,7 +300,7 @@
 
 # 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 ""
 
@@ -502,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:221
+#: 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:225
+#: 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:229
+#: 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:235
+#: include/git/Project.class.php:264
 #, php-format
 msgid "%1$s is outside of the projectroot"
 msgstr ""
@@ -587,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 ""
@@ -773,7 +773,7 @@
 # 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"
@@ -787,3 +787,11 @@
 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/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