Merge cached project directory code into ProjectListDirectory
Merge cached project directory code into ProjectListDirectory

Since cache handling is a logical extension of the directory lister
driver, it should be in the same class and handled internally. Also
this keeps caching code out of the ProjectList factory

--- 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,7 +69,7 @@
 	 * @static
 	 * @return bool true if on 64 bit
 	 */
-	public function Is64Bit()
+	public static function Is64Bit()
 	{
 		return (strpos(php_uname('m'), '64') !== false);
 	}

--- 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();
@@ -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;
@@ -744,7 +763,7 @@
 	{
 		$this->hashPathsRead = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetProject()->GetCompat()) {
 			$this->ReadHashPathsGit();
 		} else {
 			$this->ReadHashPathsRaw($this->GetTree());

--- 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)
@@ -659,9 +667,14 @@
 		$fromBlob = $this->GetFromBlob();
 		$blob = $fromBlob->GetData(true);
 
-		$diffLines = explode("\n", $exe->Execute(GIT_DIFF,
-			array("-U0", $this->fromHash,
-				$this->toHash)));
+		$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);
 
@@ -746,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/Project.class.php
+++ b/include/git/Project.class.php
@@ -25,6 +25,8 @@
 class GitPHP_Project
 {
 
+/* internal variables {{{1*/
+
 	/**
 	 * projectRoot
 	 *
@@ -43,6 +45,8 @@
 	 */
 	protected $project;
 
+/* owner internal variables {{{2*/
+
 	/**
 	 * owner
 	 *
@@ -60,6 +64,10 @@
 	 * @access protected
 	 */
 	protected $ownerRead = false;
+
+/*}}}2*/
+
+/* description internal variables {{{2*/
 
 	/**
 	 * description
@@ -80,6 +88,8 @@
 	 */
 	protected $readDescription = false;
 
+/*}}}2*/
+
 	/**
 	 * category
 	 *
@@ -89,6 +99,8 @@
 	 */
 	protected $category = '';
 
+/* epoch internal variables {{{2*/
+
 	/**
 	 * epoch
 	 *
@@ -107,6 +119,10 @@
 	 */
 	protected $epochRead = false;
 
+/*}}}2*/
+
+/* HEAD internal variables {{{2*/
+
 	/**
 	 * head
 	 *
@@ -125,6 +141,10 @@
 	 */
 	protected $readHeadRef = false;
 
+/*}}}*/
+
+/* ref internal variables {{{2*/
+
 	/**
 	 * tags
 	 *
@@ -152,6 +172,10 @@
 	 */
 	protected $readRefs = false;
 
+/*}}}2*/
+
+/* url internal variables {{{2*/
+
 	/**
 	 * cloneUrl
 	 *
@@ -170,6 +194,10 @@
 	 */
 	protected $pushUrl = null;
 
+/*}}}2*/
+
+/* bugtracker internal variables {{{2*/
+
 	/**
 	 * bugUrl
 	 *
@@ -187,6 +215,8 @@
 	 * @access protected
 	 */
 	protected $bugPattern = null;
+
+/*}}}2*/
 
 	/**
 	 * commitCache
@@ -198,6 +228,8 @@
 	 */
 	protected $commitCache = array();
 
+/* packfile internal variables {{{2*/
+
 	/**
 	 * packs
 	 *
@@ -215,6 +247,22 @@
 	 * @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
@@ -232,6 +280,25 @@
 		$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
 	 *
@@ -268,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
 	 *
@@ -294,7 +396,7 @@
 	 */
 	protected function ReadOwner()
 	{
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetCompat()) {
 			$this->ReadOwnerGit();
 		} else {
 			$this->ReadOwnerRaw();
@@ -385,18 +487,9 @@
 		$this->owner = $owner;
 	}
 
-	/**
-	 * GetProject
-	 *
-	 * Gets the project
-	 *
-	 * @access public
-	 * @return string the project
-	 */
-	public function GetProject()
-	{
-		return $this->project;
-	}
+/*}}}2*/
+
+/* projectroot accessors {{{2*/
 
 	/**
 	 * GetProjectRoot
@@ -411,36 +504,9 @@
 		return $this->projectRoot;
 	}
 
-	/**
-	 * 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;
-	}
+/*}}}2*/
+
+/* description accessors {{{2*/
 
 	/**
 	 * GetDescription
@@ -454,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)) {
@@ -478,6 +547,8 @@
 		$this->readDescription = true;
 	}
 
+/*}}}2*/
+
 	/**
 	 * GetDaemonEnabled
 	 *
@@ -490,6 +561,8 @@
 	{
 		return file_exists($this->GetPath() . '/git-daemon-export-ok');
 	}
+
+/* category accessors {{{2*/
 
 	/**
 	 * GetCategory
@@ -517,6 +590,10 @@
 		$this->category = $category;
 	}
 
+/*}}}2*/
+
+/* clone url accessors {{{2*/
+
 	/**
 	 * GetCloneUrl
 	 *
@@ -550,6 +627,10 @@
 		$this->cloneUrl = $cUrl;
 	}
 
+/*}}}2*/
+
+/* push url accessors {{{2*/
+
 	/**
 	 * GetPushUrl
 	 *
@@ -583,6 +664,10 @@
 		$this->pushUrl = $pUrl;
 	}
 
+/*}}}2*/
+
+/* bugtracker accessors {{{2*/
+
 	/**
 	 * GetBugUrl
 	 *
@@ -641,6 +726,10 @@
 		$this->bugPattern = $bPat;
 	}
 
+/*}}}2*/
+
+/* HEAD accessors {{{2*/
+
 	/**
 	 * GetHeadCommit
 	 *
@@ -669,7 +758,7 @@
 	{
 		$this->readHeadRef = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetCompat()) {
 			$this->ReadHeadCommitGit();
 		} else {
 			$this->ReadHeadCommitRaw();
@@ -715,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
@@ -774,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
@@ -890,7 +1052,7 @@
 	{
 		$this->readRefs = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetCompat()) {
 			$this->ReadRefListGit();
 		} else {
 			$this->ReadRefListRaw();
@@ -1050,6 +1212,10 @@
 		return $files;
 	}
 
+/*}}}2*/
+
+/* tag loading methods {{{2*/
+
 	/**
 	 * GetTags
 	 *
@@ -1064,7 +1230,7 @@
 		if (!$this->readRefs)
 			$this->ReadRefList();
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetCompat()) {
 			return $this->GetTagsGit($count);
 		} else {
 			return $this->GetTagsRaw($count);
@@ -1177,6 +1343,10 @@
 		}
 	}
 
+/*}}}2*/
+
+/* head loading methods {{{2*/
+
 	/**
 	 * GetHeads
 	 *
@@ -1191,7 +1361,7 @@
 		if (!$this->readRefs)
 			$this->ReadRefList();
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetCompat()) {
 			return $this->GetHeadsGit($count);
 		} else {
 			return $this->GetHeadsRaw($count);
@@ -1280,6 +1450,10 @@
 		return $this->heads[$key];
 	}
 
+/*}}}2*/
+
+/* log methods {{{2*/
+
 	/**
 	 * GetLogHash
 	 *
@@ -1309,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);
@@ -1393,6 +1567,10 @@
 		return $log;
 	}
 
+/*}}}2*/
+
+/* blob loading methods {{{2*/
+
 	/**
 	 * GetBlob
 	 *
@@ -1414,6 +1592,10 @@
 		return new GitPHP_Blob($this, $hash);
 	}
 
+/*}}}2*/
+
+/* tree loading methods {{{2*/
+
 	/**
 	 * GetTree
 	 *
@@ -1435,264 +1617,9 @@
 		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
@@ -1766,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
@@ -74,6 +74,7 @@
 					}
 				}
 			} catch (Exception $e) {
+				GitPHP_Log::GetInstance()->Log($e->getMessage());
 			}
 		}
 	}

--- a/include/git/ProjectListArrayLegacy.class.php
+++ b/include/git/ProjectListArrayLegacy.class.php
@@ -66,6 +66,7 @@
 							$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,28 @@
 	 */
 	protected function PopulateProjects()
 	{
+		$stat = stat(GITPHP_CACHE.'ProjectList.dat');
+		if ($stat !== FALSE) {
+			$cache_life = '180';  //caching time, in seconds
+			$filemtime = max($stat['mtime'], $stat['ctime']);
+			
+			if  (time() - $filemtime >= $cache_life) {
+				GitPHP_Log::GetInstance()->Log('ProjectListDirCache: expired, reloading...');
+			} else {
+
+				$data = file_get_contents(GITPHP_CACHE.'ProjectList.dat');
+				$projects = unserialize($data);
+				if (count($projects) > 0) {
+					GitPHP_Log::GetInstance()->Log('loaded '.count($projects).' projects from cache');
+					$this->projects = $projects;
+					return;
+				}
+
+			}
+		}
+
 		$this->RecurseDir($this->projectDir);
+		$this->CacheSaveProjectList();
 	}
 
 	/**
@@ -76,12 +97,15 @@
 		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($this->projectDir, $projectPath);
@@ -90,15 +114,25 @@
 								$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);
 		}
 	}
 
+	/** Save and restore project list to prevent parsing directories
+	 */
+	private function CacheSaveProjectList()
+	{
+		$data = serialize($this->projects);
+		return (file_put_contents(GITPHP_CACHE.'ProjectList.dat',$data) > 0);
+	}
 }
 

--- a/include/git/ProjectListFile.class.php
+++ b/include/git/ProjectListFile.class.php
@@ -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,7 +330,7 @@
 	{
 		$this->dataRead = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetProject()->GetCompat()) {
 			$this->ReadDataGit();
 		} else {
 			$this->ReadDataRaw();

--- a/include/git/Tree.class.php
+++ b/include/git/Tree.class.php
@@ -114,7 +114,7 @@
 	{
 		$this->contentsRead = true;
 
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+		if ($this->GetProject()->GetCompat()) {
 			$this->ReadContentsGit();
 		} else {
 			$this->ReadContentsRaw();

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}

comments