Support scm-manager repository configs
Support scm-manager repository configs

Requires xml support. Only displays repositories marked as 'public'.

--- a/config/gitphp.conf.defaults.php
+++ b/config/gitphp.conf.defaults.php
@@ -138,6 +138,31 @@
 /*********************************************************
  * Features
  */
+
+/*
+ * compat
+ * Set this to true to turn on compatibility mode.  This will cause
+ * GitPHP to rely more on the git executable for loading data,
+ * which will bypass some of the limitations of PHP at the expense
+ * of performance.
+ * Turn this on if you are experiencing issues viewing data for
+ * your projects.
+ */
+$gitphp_conf['compat'] = false;
+
+/**
+ * largeskip
+ * When GitPHP is reading through the history for pages of the shortlog/log
+ * beyond the first, it needs to read from the tip but skip a number of commits
+ * for the previous pages.  The more commits it needs to skip, the longer it takes.
+ * Calling the git executable is faster when skipping a large number of commits,
+ * ie reading a log page significantly beyond the first.  This determines
+ * the threshold at which GitPHP will fall back to using the git exe for the log.
+ * Currently each log page shows 100 commits, so this would be calculated at
+ * page number * 100.  So for example at the default of 200, pages 0-2 would be
+ * loaded natively and pages 3+ would fall back on the git exe.
+ */
+$gitphp_conf['largeskip'] = 200;
 
 /*
  * compressformat

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

--- a/config/projects.conf.php.example
+++ b/config/projects.conf.php.example
@@ -15,7 +15,7 @@
  * git_projects
  * List of projects
  *
- * There are three ways to list projects:
+ * There are several ways to list projects:
  *
  * 1. Array of projects
  */
@@ -37,7 +37,16 @@
 //$git_projects = '/git/projectlist.txt';
 
 /*
- * 3. Leave commented to read all projects in the project root
+ * 3. Path to scm-manager repositories.xml file
+ * Points to the repository config file used by the scm-manager
+ * program.  In order for this to work, the projectroot for GitPHP
+ * must be the same as the git repository directory configured
+ * in scm-manager
+ */
+//$git_projects = '~/.scm/config/repositories.xml';
+
+/*
+ * 4. Leave commented to read all projects in the project root
  */
 
 

--- a/css/gitphp.css
+++ b/css/gitphp.css
@@ -131,6 +131,14 @@
 	width: 50%;
 }
 
+/*
+ * side-by-side commitdiff
+ */
+div.commitDiffSBS div.SBSTOC .showAll
+{
+	display: none;
+}
+
 
 /*
  * Geshi styles

--- 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;
 }
@@ -400,6 +408,57 @@
 	border-right: 1px solid #d9d8d1;
 }
 
+/*
+ * side-by-side commitdiff
+ */
+div.commitDiffSBS
+{
+	width: 100%;
+	border-top: 2px solid #edece6;
+}
+
+div.commitDiffSBS div.SBSTOC
+{
+	float: left;
+	width: 19%;
+	word-wrap: break-word;
+	background-color: #ffffff;
+	border-bottom: 1px solid #edece6;
+}
+
+div.commitDiffSBS div.SBSTOC a
+{
+	text-decoration: none;
+}
+
+div.commitDiffSBS div.SBSTOC ul
+{
+	margin-left: 8px;
+	padding-left: 8px;
+}
+
+div.commitDiffSBS div.SBSTOC .listcount
+{
+	list-style-type: none;
+}
+
+div.commitDiffSBS div.SBSTOC .activeItem
+{
+	background-color: #edece6;
+}
+
+div.commitDiffSBS .SBSContent
+{
+	float: right;
+	width: 80%;
+	border-left: 1px solid #edece6;
+}
+
+div.commitDiffSBS .SBSFooter
+{
+	clear: both;
+}
+
 
 /*
  * Blob/blame display

--- a/include/Util.class.php
+++ b/include/Util.class.php
@@ -25,21 +25,75 @@
 	 * @access public
 	 * @static
 	 * @param string $path path to add slash to
-	 * @param $backslash true to also check for backslash (windows paths)
+	 * @param $filesystem true if this is a filesystem path (to also check for backslash for windows paths)
 	 * @return string $path with a trailing slash
 	 */
-	public static function AddSlash($path, $backslash = true)
+	public static function AddSlash($path, $filesystem = true)
 	{
 		if (empty($path))
 			return $path;
 
 		$end = substr($path, -1);
 
-		if (!(( ($end == '/') || ($end == ':')) || ($backslash && (strtoupper(substr(PHP_OS, 0, 3))) && ($end == '\\'))))
-			$path .= '/';
+		if (!(( ($end == '/') || ($end == ':')) || ($filesystem && GitPHP_Util::IsWindows() && ($end == '\\')))) {
+			if (GitPHP_Util::IsWindows() && $filesystem) {
+				$path .= '\\';
+			} else {
+				$path .= '/';
+			}
+		}
 
 		return $path;
 	}
 
+	/**
+	 * IsWindows
+	 *
+	 * Tests if this is running on windows
+	 *
+	 * @access public
+	 * @static
+	 * @return bool true if on windows
+	 */
+	public static function IsWindows()
+	{
+		return (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
+	}
+
+	/**
+	 * Is64Bit
+	 *
+	 * Tests if this is a 64 bit machine
+	 *
+	 * @access public
+	 * @static
+	 * @return bool true if on 64 bit
+	 */
+	public static function Is64Bit()
+	{
+		return (strpos(php_uname('m'), '64') !== false);
+	}
+
+	/**
+	 * MakeSlug
+	 *
+	 * Turn a string into a filename-friendly slug
+	 *
+	 * @access public
+	 * @param string $str string to slugify
+	 * @static
+	 * @return string slug
+	 */
+	public static function MakeSlug($str)
+	{
+		$from = array(
+			'/'
+		);
+		$to = array(
+			'-'
+		);
+		return str_replace($from, $to, $str);
+	}
+
 }
 

--- a/include/controller/Controller_Blobdiff.class.php
+++ b/include/controller/Controller_Blobdiff.class.php
@@ -10,21 +10,7 @@
  * @subpackage Controller
  */
 
-/**
- * Constants for blobdiff modes
- */
-define('GITPHP_BLOBDIFF_UNIFIED', 1);
-define('GITPHP_BLOBDIFF_SIDEBYSIDE', 2);
-
-/**
- * Constant of the blobdiff mode cookie in the user's browser
- */
-define('GITPHP_BLOBDIFF_MODE_COOKIE', 'GitPHPBlobdiffMode');
-
-/**
- * Blobdiff mode cookie lifetime
- */
-define('GITPHP_BLOBDIFF_MODE_COOKIE_LIFETIME', 60*60*24*365);		// 1 year
+require_once(GITPHP_CONTROLLERDIR . 'Controller_DiffBase.class.php');
 
 /**
  * Blobdiff controller class
@@ -32,7 +18,7 @@
  * @package GitPHP
  * @subpackage Controller
  */
-class GitPHP_Controller_Blobdiff extends GitPHP_ControllerBase
+class GitPHP_Controller_Blobdiff extends GitPHP_Controller_DiffBase
 {
 
 	/**
@@ -106,6 +92,8 @@
 	 */
 	protected function ReadQuery()
 	{
+		parent::ReadQuery();
+
 		if (isset($_GET['f']))
 			$this->params['file'] = $_GET['f'];
 		if (isset($_GET['h']))
@@ -114,55 +102,6 @@
 			$this->params['hashbase'] = $_GET['hb'];
 		if (isset($_GET['hp']))
 			$this->params['hashparent'] = $_GET['hp'];
-
-		if (!isset($this->params['plain']) || $this->params['plain'] != true) {
-
-			$mode = GITPHP_BLOBDIFF_UNIFIED;	// default
-
-			/*
-			 * Check cookie
-			 */
-			if (!empty($_COOKIE[GITPHP_BLOBDIFF_MODE_COOKIE])) {
-				$mode = $_COOKIE[GITPHP_BLOBDIFF_MODE_COOKIE];
-			} else {
-				/*
-				 * Create cookie to prevent browser delay
-				 */
-				setcookie(GITPHP_BLOBDIFF_MODE_COOKIE, $mode, time()+GITPHP_BLOBDIFF_MODE_COOKIE_LIFETIME);
-			}
-
-			if (isset($_GET['o'])) {
-				/*
-				 * User is choosing a new mode
-				 */
-				if ($_GET['o'] == 'sidebyside') {
-					$mode = GITPHP_BLOBDIFF_SIDEBYSIDE;
-					setcookie(GITPHP_BLOBDIFF_MODE_COOKIE, GITPHP_BLOBDIFF_SIDEBYSIDE, time()+GITPHP_BLOBDIFF_MODE_COOKIE_LIFETIME);
-				} else if ($_GET['o'] == 'unified') {
-					$mode = GITPHP_BLOBDIFF_UNIFIED;
-					setcookie(GITPHP_BLOBDIFF_MODE_COOKIE, GITPHP_BLOBDIFF_UNIFIED, time()+GITPHP_BLOBDIFF_MODE_COOKIE_LIFETIME);
-				}
-			}
-
-			if ($mode == GITPHP_BLOBDIFF_SIDEBYSIDE) {
-				$this->params['sidebyside'] = true;
-			}
-
-		}
-	}
-
-	/**
-	 * LoadHeaders
-	 *
-	 * Loads headers for this template
-	 *
-	 * @access protected
-	 */
-	protected function LoadHeaders()
-	{
-		if (isset($this->params['plain']) && ($this->params['plain'] === true)) {
-			$this->headers[] = 'Content-type: text/plain; charset=UTF-8';
-		}
 	}
 
 	/**

--- a/include/controller/Controller_Commitdiff.class.php
+++ b/include/controller/Controller_Commitdiff.class.php
@@ -10,21 +10,7 @@
  * @subpackage Controller
  */
 
-/**
- * Constants for blobdiff modes
- */
-define('GITPHP_BLOBDIFF_UNIFIED', 1);
-define('GITPHP_BLOBDIFF_SIDEBYSIDE', 2);
-
-/**
- * Constant of the blobdiff mode cookie in the user's browser
- */
-define('GITPHP_BLOBDIFF_MODE_COOKIE', 'GitPHPBlobdiffMode');
-
-/**
- * Blobdiff mode cookie lifetime
- */
-define('GITPHP_BLOBDIFF_MODE_COOKIE_LIFETIME', 60*60*24*365);           // 1 year
+require_once(GITPHP_CONTROLLERDIR . 'Controller_DiffBase.class.php');
 
 /**
  * Commitdiff controller class
@@ -32,7 +18,7 @@
  * @package GitPHP
  * @subpackage Controller
  */
-class GitPHP_Controller_Commitdiff extends GitPHP_ControllerBase
+class GitPHP_Controller_Commitdiff extends GitPHP_Controller_DiffBase
 {
 
 	/**
@@ -110,45 +96,12 @@
 	 */
 	protected function ReadQuery()
 	{
+		parent::ReadQuery();
+
 		if (isset($_GET['h']))
 			$this->params['hash'] = $_GET['h'];
 		if (isset($_GET['hp']))
 			$this->params['hashparent'] = $_GET['hp'];
-
-		if (!isset($this->params['plain']) || $this->params['plain'] != true) {
-
-			$mode = GITPHP_BLOBDIFF_UNIFIED;        // default
-
-
-			/*
-			 * Check cookie
-			 */
-			if (!empty($_COOKIE[GITPHP_BLOBDIFF_MODE_COOKIE])) {
-				$mode = $_COOKIE[GITPHP_BLOBDIFF_MODE_COOKIE];
-			} else {
-				/*
-				 * Create cookie to prevent browser delay
-				 */
-				setcookie(GITPHP_BLOBDIFF_MODE_COOKIE, $mode, time()+GITPHP_BLOBDIFF_MODE_COOKIE_LIFETIME);
-			}
-
-			if (isset($_GET['o'])) {
-				/*
-				 * User is choosing a new mode
-				 */
-				if ($_GET['o'] == 'sidebyside') {
-					$mode = GITPHP_BLOBDIFF_SIDEBYSIDE;
-					setcookie(GITPHP_BLOBDIFF_MODE_COOKIE, GITPHP_BLOBDIFF_SIDEBYSIDE, time()+GITPHP_BLOBDIFF_MODE_COOKIE_LIFETIME);
-				} else if ($_GET['o'] == 'unified') {
-					$mode = GITPHP_BLOBDIFF_UNIFIED;
-					setcookie(GITPHP_BLOBDIFF_MODE_COOKIE, GITPHP_BLOBDIFF_UNIFIED, time()+GITPHP_BLOBDIFF_MODE_COOKIE_LIFETIME);
-				}
-			}
-
-			if ($mode == GITPHP_BLOBDIFF_SIDEBYSIDE) {
-				$this->params['sidebyside'] = true;
-			}
-		}
 	}
 
 	/**
@@ -160,10 +113,9 @@
 	 */
 	protected function LoadHeaders()
 	{
+		parent::LoadHeaders();
+
 		if (isset($this->params['plain']) && ($this->params['plain'] === true)) {
-			GitPHP_Log::GetInstance()->SetEnabled(false);
-
-			$this->headers[] = 'Content-type: text/plain; charset=UTF-8';
 			$this->headers[] = 'Content-disposition: inline; filename="git-' . $this->params['hash'] . '.patch"';
 		}
 	}
@@ -186,6 +138,7 @@
 
 		if (isset($this->params['sidebyside']) && ($this->params['sidebyside'] === true)) {
 			$this->tpl->assign('sidebyside', true);
+			$this->tpl->assign('extrascripts', array('commitdiff'));
 		}
 
 		$treediff = new GitPHP_TreeDiff($this->project, $this->params['hash'], (isset($this->params['hashparent']) ? $this->params['hashparent'] : ''));

--- /dev/null
+++ b/include/controller/Controller_DiffBase.class.php
@@ -1,1 +1,113 @@
+<?php
+/**
+ * GitPHP Controller DiffBase
+ *
+ * Base controller for diff-type views
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2011 Christopher Han
+ * @package GitPHP
+ * @subpackage Controller
+ */
 
+
+/**
+ * Constants for diff modes
+ */
+define('GITPHP_DIFF_UNIFIED', 1);
+define('GITPHP_DIFF_SIDEBYSIDE', 2);
+
+/**
+ * Constant of the diff mode cookie in the user's browser
+ */
+define('GITPHP_DIFF_MODE_COOKIE', 'GitPHPDiffMode');
+
+/**
+ * Diff mode cookie lifetime
+ */
+define('GITPHP_DIFF_MODE_COOKIE_LIFETIME', 60*60*24*365);           // 1 year
+
+/**
+ * DiffBase controller class
+ *
+ * @package GitPHP
+ * @subpackage Controller
+ */
+abstract class GitPHP_Controller_DiffBase extends GitPHP_ControllerBase
+{
+	
+	/**
+	 * ReadQuery
+	 *
+	 * Read query into parameters
+	 *
+	 * @access protected
+	 */
+	protected function ReadQuery()
+	{
+		if (!isset($this->params['plain']) || $this->params['plain'] != true) {
+
+			if ($this->DiffMode(isset($_GET['o']) ? $_GET['o'] : '') == GITPHP_DIFF_SIDEBYSIDE) {
+				$this->params['sidebyside'] = true;
+			}
+
+		}
+	}
+
+	/**
+	 * DiffMode
+	 *
+	 * Determines the diff mode to use
+	 *
+	 * @param string $overrideMode mode overridden by the user
+	 * @access protected
+	 */
+	protected function DiffMode($overrideMode = '')
+	{
+		$mode = GITPHP_DIFF_UNIFIED;	// default
+
+		/*
+		 * Check cookie
+		 */
+		if (!empty($_COOKIE[GITPHP_DIFF_MODE_COOKIE])) {
+			$mode = $_COOKIE[GITPHP_DIFF_MODE_COOKIE];
+		} else {
+			/*
+			 * Create cookie to prevent browser delay
+			 */
+			setcookie(GITPHP_DIFF_MODE_COOKIE, $mode, time()+GITPHP_DIFF_MODE_COOKIE_LIFETIME);
+		}
+
+		if (!empty($overrideMode)) {
+			/*
+			 * User is choosing a new mode
+			 */
+			if ($overrideMode == 'sidebyside') {
+				$mode = GITPHP_DIFF_SIDEBYSIDE;
+				setcookie(GITPHP_DIFF_MODE_COOKIE, GITPHP_DIFF_SIDEBYSIDE, time()+GITPHP_DIFF_MODE_COOKIE_LIFETIME);
+			} else if ($overrideMode == 'unified') {
+				$mode = GITPHP_DIFF_UNIFIED;
+				setcookie(GITPHP_DIFF_MODE_COOKIE, GITPHP_DIFF_UNIFIED, time()+GITPHP_DIFF_MODE_COOKIE_LIFETIME);
+			}
+		}
+
+		return $mode;
+	}
+
+	/**
+	 * LoadHeaders
+	 *
+	 * Loads headers for this template
+	 *
+	 * @access protected
+	 */
+	protected function LoadHeaders()
+	{
+		if (isset($this->params['plain']) && ($this->params['plain'] === true)) {
+			GitPHP_Log::GetInstance()->SetEnabled(false);
+			$this->headers[] = 'Content-type: text/plain; charset=UTF-8';
+		}
+	}
+
+}
+

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

--- a/include/git/Blob.class.php
+++ b/include/git/Blob.class.php
@@ -47,7 +47,7 @@
 	 *
 	 * @access protected
 	 */
-	protected $size;
+	protected $size = null;
 
 	/**
 	 * history
@@ -132,13 +132,17 @@
 	{
 		$this->dataRead = true;
 
-		$exe = new GitPHP_GitExe($this->GetProject());
-
-		$args = array();
-		$args[] = 'blob';
-		$args[] = $this->hash;
-
-		$this->data = $exe->Execute(GIT_CAT_FILE, $args);
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$exe = new GitPHP_GitExe($this->GetProject());
+
+			$args = array();
+			$args[] = 'blob';
+			$args[] = $this->hash;
+
+			$this->data = $exe->Execute(GIT_CAT_FILE, $args);
+		} else {
+			$this->data = $this->GetProject()->GetObject($this->hash);
+		}
 
 		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
 	}
@@ -194,7 +198,14 @@
 	 */
 	public function GetSize()
 	{
-		return $this->size;
+		if ($this->size !== null) {
+			return $this->size;
+		}
+
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return strlen($this->data);
 	}
 
 	/**
@@ -208,6 +219,26 @@
 	public function SetSize($size)
 	{
 		$this->size = $size;
+	}
+
+	/**
+	 * IsBinary
+	 *
+	 * Tests if this blob is a binary file
+	 *
+	 * @access public
+	 * @return boolean true if binary file
+	 */
+	public function IsBinary()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		$data = $this->data;
+		if (strlen($this->data) > 8000)
+			$data = substr($data, 0, 8000);
+
+		return strpos($data, chr(0)) !== false;
 	}
 
 	/**
@@ -259,14 +290,14 @@
 
 		$magicdb = GitPHP_Config::GetInstance()->GetValue('magicdb', null);
 		if (empty($magicdb)) {
-			if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+			if (GitPHP_Util::IsWindows()) {
 				$magicdb = 'C:\\wamp\\php\\extras\\magic';
 			} else {
 				$magicdb = '/usr/share/misc/magic';
 			}
 		}
 
-		$finfo = finfo_open(FILEINFO_MIME, $magicdb);
+		$finfo = @finfo_open(FILEINFO_MIME, $magicdb);
 		if ($finfo) {
 			$mime = finfo_buffer($finfo, $this->data, FILEINFO_MIME);
 			if ($mime && strpos($mime, '/')) {
@@ -290,7 +321,7 @@
 	 */
 	private function FileMime_File()
 	{
-		if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+		if (GitPHP_Util::IsWindows()) {
 			return '';
 		}
 

--- a/include/git/Commit.class.php
+++ b/include/git/Commit.class.php
@@ -513,6 +513,22 @@
 	}
 
 	/**
+	 * IsMergeCommit
+	 *
+	 * Returns whether this is a merge commit
+	 *
+	 * @access pubilc
+	 * @return boolean true if merge commit
+	 */
+	public function IsMergeCommit()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return count($this->parents) > 1;
+	}
+
+	/**
 	 * ReadData
 	 *
 	 * Read the data for the commit
@@ -523,38 +539,46 @@
 	{
 		$this->dataRead = true;
 
-		/* get data from git_rev_list */
-		$exe = new GitPHP_GitExe($this->GetProject());
-		$args = array();
-		$args[] = '--header';
-		$args[] = '--parents';
-		$args[] = '--max-count=1';
-		$args[] = $this->hash;
-		$ret = $exe->Execute(GIT_REV_LIST, $args);
-		unset($exe);
-
-		$lines = explode("\n", $ret);
-
-		if (!isset($lines[0]))
-			return;
-
-		/* In case we returned something unexpected */
-		$tok = strtok($lines[0], ' ');
-		if ($tok != $this->hash)
-			return;
-
-		/* Read all parents */
-		$tok = strtok(' ');
-		while ($tok !== false) {
-			try {
-				$this->parents[] = $this->GetProject()->GetCommit($tok);
-			} catch (Exception $e) {
-			}
-			$tok = strtok(' ');
-		}
+		$lines = null;
+
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+
+			/* get data from git_rev_list */
+			$exe = new GitPHP_GitExe($this->GetProject());
+			$args = array();
+			$args[] = '--header';
+			$args[] = '--parents';
+			$args[] = '--max-count=1';
+			$args[] = $this->hash;
+			$ret = $exe->Execute(GIT_REV_LIST, $args);
+			unset($exe);
+
+			$lines = explode("\n", $ret);
+
+			if (!isset($lines[0]))
+				return;
+
+			/* In case we returned something unexpected */
+			$tok = strtok($lines[0], ' ');
+			if ($tok != $this->hash)
+				return;
+
+			array_shift($lines);
+
+		} else {
+			
+			$data = $this->GetProject()->GetObject($this->hash);
+			if (empty($data))
+				return;
+
+			$lines = explode("\n", $data);
+
+		}
+
+		$header = true;
 
 		foreach ($lines as $i => $line) {
-			if (preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
+			if ($header && preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
 				/* Tree */
 				try {
 					$tree = $this->GetProject()->GetTree($regs[1]);
@@ -564,26 +588,31 @@
 					}
 				} catch (Exception $e) {
 				}
-			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+			} else if ($header && preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
+				/* Parent */
+				try {
+					$this->parents[] = $this->GetProject()->GetCommit($regs[1]);
+				} catch (Exception $e) {
+				}
+			} else if ($header && preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
 				/* author data */
 				$this->author = $regs[1];
 				$this->authorEpoch = $regs[2];
 				$this->authorTimezone = $regs[3];
-			} else if (preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+			} else if ($header && preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
 				/* committer data */
 				$this->committer = $regs[1];
 				$this->committerEpoch = $regs[2];
 				$this->committerTimezone = $regs[3];
 			} else {
 				/* commit comment */
-				if (!(preg_match('/^[0-9a-fA-F]{40}/', $line) || preg_match('/^parent [0-9a-fA-F]{40}/', $line))) {
-					$trimmed = trim($line);
-					if (empty($this->title) && (strlen($trimmed) > 0))
-						$this->title = $trimmed;
-					if (!empty($this->title)) {
-						if ((strlen($trimmed) > 0) || ($i < (count($lines)-1)))
-							$this->comment[] = $trimmed;
-					}
+				$header = false;
+				$trimmed = trim($line);
+				if (empty($this->title) && (strlen($trimmed) > 0))
+					$this->title = $trimmed;
+				if (!empty($this->title)) {
+					if ((strlen($trimmed) > 0) || ($i < (count($lines)-1)))
+						$this->comment[] = $trimmed;
 				}
 			}
 		}
@@ -734,6 +763,24 @@
 	{
 		$this->hashPathsRead = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadHashPathsGit();
+		} else {
+			$this->ReadHashPathsRaw($this->GetTree());
+		}
+
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadHashPathsGit
+	 *
+	 * Reads hash to path mappings using git exe
+	 *
+	 * @access private
+	 */
+	private function ReadHashPathsGit()
+	{
 		$exe = new GitPHP_GitExe($this->GetProject());
 
 		$args = array();
@@ -756,8 +803,35 @@
 				}
 			}
 		}
-
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadHashPathsRaw
+	 *
+	 * Reads hash to path mappings using raw objects
+	 *
+	 * @access private
+	 */
+	private function ReadHashPathsRaw($tree)
+	{
+		if (!$tree) {
+			return;
+		}
+
+		$contents = $tree->GetContents();
+
+		foreach ($contents as $obj) {
+			if ($obj instanceof GitPHP_Blob) {
+				$hash = $obj->GetHash();
+				$path = $obj->GetPath();
+				$this->blobPaths[trim($path)] = $hash;
+			} else if ($obj instanceof GitPHP_Tree) {
+				$hash = $obj->GetHash();
+				$path = $obj->GetPath();
+				$this->treePaths[trim($path)] = $hash;
+				$this->ReadHashPathsRaw($obj);
+			}
+		}
 	}
 
 	/**
@@ -1004,5 +1078,23 @@
 		return $key;
 	}
 
+	/**
+	 * CompareAge
+	 *
+	 * Compares two commits by age
+	 *
+	 * @access public
+	 * @static
+	 * @param mixed $a first commit
+	 * @param mixed $b second commit
+	 * @return integer comparison result
+	 */
+	public static function CompareAge($a, $b)
+	{
+		if ($a->GetAge() === $b->GetAge())
+			return 0;
+		return ($a->GetAge() < $b->GetAge() ? -1 : 1);
+	}
+
 }
 

--- a/include/git/DiffExe.class.php
+++ b/include/git/DiffExe.class.php
@@ -54,13 +54,11 @@
 	 */
 	public function __construct()
 	{
-		$this->binary = GitPHP_Config::GetInstance()->GetValue('diffbin');
-		if (empty($this->binary)) {
-			if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
-				$this->binary = 'C:\\Progra~1\\Git\\bin\\diff.exe';
-			} else {
-				$this->binary = 'diff';
-			}
+		$binary = GitPHP_Config::GetInstance()->GetValue('diffbin');
+		if (empty($binary)) {
+			$this->binary = GitPHP_DiffExe::DefaultBinary();
+		} else {
+			$this->binary = $binary;
 		}
 
 	}
@@ -241,11 +239,10 @@
 	 */
 	public static function DefaultBinary()
 	{
-		if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+		if (GitPHP_Util::IsWindows()) {
 			// windows
 
-			$arch = php_uname('m');
-			if (strpos($arch, '64') !== false) {
+			if (GitPHP_Util::Is64Bit()) {
 				// match x86_64 and x64 (64 bit)
 				// C:\Program Files (x86)\Git\bin\diff.exe
 				return 'C:\\Progra~2\\Git\\bin\\diff.exe';
@@ -256,7 +253,7 @@
 			}
 		} else {
 			// *nix, just use PATH
-			$this->binary = 'diff';
+			return 'diff';
 		}
 	}
 }

--- a/include/git/FileDiff.class.php
+++ b/include/git/FileDiff.class.php
@@ -351,6 +351,38 @@
 	}
 
 	/**
+	 * GetFromBlob
+	 *
+	 * Gets the from file blob
+	 *
+	 * @access public
+	 * @return mixed blob object
+	 */
+	public function GetFromBlob()
+	{
+		if (empty($this->fromHash))
+			return null;
+
+		return $this->project->GetBlob($this->fromHash);
+	}
+
+	/**
+	 * GetToBlob
+	 *
+	 * Gets the to file blob
+	 *
+	 * @access public
+	 * @return mixed blob object
+	 */
+	public function GetToBlob()
+	{
+		if (empty($this->toHash))
+			return null;
+
+		return $this->project->GetBlob($this->toHash);
+	}
+
+	/**
 	 * GetStatus
 	 *
 	 * Gets the status of the change
@@ -541,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->project->GetBlob($this->fromHash);
-			$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->project->GetBlob($this->toHash);
-			$toTmpFile = 'gitphp_' . $pid . '_to';
-			$tmpdir->AddFile($toTmpFile, $toBlob->GetData());
-
-			$toName = 'b/';
-			if (!empty($file)) {
-				$toName .= $file;
-			} else if (!empty($this->toFile)) {
-				$toName .= $this->toFile;
-			} else {
-				$toName .= $this->toHash;
-			}
-		}
-
-		$this->diffData = GitPHP_DiffExe::Diff((empty($fromTmpFile) ? null : ($tmpdir->GetDir() . $fromTmpFile)), $fromName, (empty($toTmpFile) ? null : ($tmpdir->GetDir() . $toTmpFile)), $toName);
-
-		if (!empty($fromTmpFile)) {
-			$tmpdir->RemoveFile($fromTmpFile);
-		}
-
-		if (!empty($toTmpFile)) {
-			$tmpdir->RemoveFile($toTmpFile);
+		if (function_exists('xdiff_string_diff')) {
+
+			$this->diffData = $this->GetXDiff(3, true, $file);
+
+		} else {
+
+			$tmpdir = GitPHP_TmpDir::GetInstance();
+
+			$pid = 0;
+			if (function_exists('posix_getpid'))
+				$pid = posix_getpid();
+			else
+				$pid = rand();
+
+			$fromTmpFile = null;
+			$toTmpFile = null;
+
+			$fromName = null;
+			$toName = null;
+
+			if ((empty($this->status)) || ($this->status == 'D') || ($this->status == 'M')) {
+				$fromBlob = $this->GetFromBlob();
+				$fromTmpFile = 'gitphp_' . $pid . '_from';
+				$tmpdir->AddFile($fromTmpFile, $fromBlob->GetData());
+
+				$fromName = 'a/';
+				if (!empty($file)) {
+					$fromName .= $file;
+				} else if (!empty($this->fromFile)) {
+					$fromName .= $this->fromFile;
+				} else {
+					$fromName .= $this->fromHash;
+				}
+			}
+
+			if ((empty($this->status)) || ($this->status == 'A') || ($this->status == 'M')) {
+				$toBlob = $this->GetToBlob();
+				$toTmpFile = 'gitphp_' . $pid . '_to';
+				$tmpdir->AddFile($toTmpFile, $toBlob->GetData());
+
+				$toName = 'b/';
+				if (!empty($file)) {
+					$toName .= $file;
+				} else if (!empty($this->toFile)) {
+					$toName .= $this->toFile;
+				} else {
+					$toName .= $this->toHash;
+				}
+			}
+
+			$this->diffData = GitPHP_DiffExe::Diff((empty($fromTmpFile) ? null : escapeshellarg($tmpdir->GetDir() . $fromTmpFile)), $fromName, (empty($toTmpFile) ? null : escapeshellarg($tmpdir->GetDir() . $toTmpFile)), $toName);
+
+			if (!empty($fromTmpFile)) {
+				$tmpdir->RemoveFile($fromTmpFile);
+			}
+
+			if (!empty($toTmpFile)) {
+				$tmpdir->RemoveFile($toTmpFile);
+			}
+
 		}
 
 		if ($explode)
@@ -624,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);
 
@@ -715,6 +759,65 @@
 	}
 
 	/**
+	 * GetXDiff
+	 *
+	 * Get diff using xdiff
+	 *
+	 * @access private
+	 * @param int $context number of context lines
+	 * @param boolean $header true to include standard diff header
+	 * @param string $file override the file name
+	 * @return string diff content
+	 */
+	private function GetXDiff($context = 3, $header = true, $file = null)
+	{
+		if (!function_exists('xdiff_string_diff'))
+			return '';
+
+		$fromData = '';
+		$toData = '';
+		$isBinary = false;
+		$fromName = '/dev/null';
+		$toName = '/dev/null';
+		if (empty($this->status) || ($this->status == 'M') || ($this->status == 'D')) {
+			$fromBlob = $this->GetFromBlob();
+			$isBinary = $isBinary || $fromBlob->IsBinary();
+			$fromData = $fromBlob->GetData(false);
+			$fromName = 'a/';
+			if (!empty($file)) {
+				$fromName .= $file;
+			} else if (!empty($this->fromFile)) {
+				$fromName .= $this->fromFile;
+			} else {
+				$fromName .= $this->fromHash;
+			}
+		}
+		if (empty($this->status) || ($this->status == 'M') || ($this->status == 'A')) {
+			$toBlob = $this->GetToBlob();
+			$isBinary = $isBinary || $toBlob->IsBinary();
+			$toData = $toBlob->GetData(false);
+			$toName = 'b/';
+			if (!empty($file)) {
+				$toName .= $file;
+			} else if (!empty($this->toFile)) {
+				$toName .= $this->toFile;
+			} else {
+				$toName .= $this->toHash;
+			}
+		}
+		$output = '';
+		if ($isBinary) {
+			$output = sprintf(__('Binary files %1$s and %2$s differ'), $fromName, $toName) . "\n";
+		} else {
+			if ($header) {
+				$output = '--- ' . $fromName . "\n" . '+++ ' . $toName . "\n";
+			}
+			$output .= xdiff_string_diff($fromData, $toData, $context);
+		}
+		return $output;
+	}
+
+	/**
 	 * GetCommit
 	 *
 	 * Gets the commit for this filediff

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

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

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

--- a/include/git/Project.class.php
+++ b/include/git/Project.class.php
@@ -14,6 +14,7 @@
 require_once(GITPHP_GITOBJECTDIR . 'Commit.class.php');
 require_once(GITPHP_GITOBJECTDIR . 'Head.class.php');
 require_once(GITPHP_GITOBJECTDIR . 'Tag.class.php');
+require_once(GITPHP_GITOBJECTDIR . 'Pack.class.php');
 
 /**
  * Project class
@@ -25,6 +26,15 @@
 {
 
 	/**
+	 * projectRoot
+	 *
+	 * Stores the project root internally
+	 *
+	 * @access protected
+	 */
+	protected $projectRoot;
+
+	/**
 	 * project
 	 *
 	 * Stores the project internally
@@ -43,13 +53,13 @@
 	protected $owner = "";
 
 	/**
-	 * readOwner
+	 * ownerRead
 	 *
 	 * Stores whether the file owner has been read
 	 *
 	 * @access protected
 	 */
-	protected $readOwner = false;
+	protected $ownerRead = false;
 
 	/**
 	 * description
@@ -189,15 +199,36 @@
 	protected $commitCache = array();
 
 	/**
+	 * packs
+	 *
+	 * Stores the list of packs
+	 *
+	 * @access protected
+	 */
+	protected $packs = array();
+
+	/**
+	 * packsRead
+	 *
+	 * Stores whether packs have been read
+	 *
+	 * @access protected
+	 */
+	protected $packsRead = false;
+
+	/**
 	 * __construct
 	 *
 	 * Class constructor
 	 *
 	 * @access public
+	 * @param string $projectRoot project root
+	 * @param string $project project
 	 * @throws Exception if project is invalid or outside of projectroot
 	 */
-	public function __construct($project)
-	{
+	public function __construct($projectRoot, $project)
+	{
+		$this->projectRoot = GitPHP_Util::AddSlash($projectRoot);
 		$this->SetProject($project);
 	}
 
@@ -211,10 +242,8 @@
 	 */
 	private function SetProject($project)
 	{
-		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
-
-		$realProjectRoot = realpath($projectRoot);
-		$path = $projectRoot . $project;
+		$realProjectRoot = realpath($this->projectRoot);
+		$path = $this->projectRoot . $project;
 		$fullPath = realpath($path);
 
 		if (!is_dir($fullPath)) {
@@ -249,33 +278,101 @@
 	 */
 	public function GetOwner()
 	{
-		if (empty($this->owner) && !$this->readOwner) {
-
-			$exe = new GitPHP_GitExe($this);
-			$args = array();
-			$args[] = 'gitweb.owner';
-			$this->owner = $exe->Execute(GIT_CONFIG, $args);
-			unset($exe);
-			
-			if (empty($this->owner) && function_exists('posix_getpwuid')) {
-				$uid = fileowner($this->GetPath());
-				if ($uid !== false) {
-					$data = posix_getpwuid($uid);
-					if (isset($data['gecos']) && !empty($data['gecos'])) {
-						$this->owner = $data['gecos'];
-					} elseif (isset($data['name']) && !empty($data['name'])) {
-						$this->owner = $data['name'];
-					}
-				}
-			}
-
-			$this->readOwner = true;
+		if (empty($this->owner) && !$this->ownerRead) {
+			$this->ReadOwner();
 		}
 	
 		return $this->owner;
 	}
 
 	/**
+	 * ReadOwner
+	 *
+	 * Reads the project owner
+	 *
+	 * @access protected
+	 */
+	protected function ReadOwner()
+	{
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadOwnerGit();
+		} else {
+			$this->ReadOwnerRaw();
+		}
+
+		if (empty($this->owner) && function_exists('posix_getpwuid')) {
+			$uid = fileowner($this->GetPath());
+			if ($uid !== false) {
+				$data = posix_getpwuid($uid);
+				if (isset($data['gecos']) && !empty($data['gecos'])) {
+					$this->owner = $data['gecos'];
+				} elseif (isset($data['name']) && !empty($data['name'])) {
+					$this->owner = $data['name'];
+				}
+			}
+		}
+
+		$this->ownerRead = true;
+	}
+
+	/**
+	 * ReadOwnerGit
+	 *
+	 * Reads the project owner using the git executable
+	 *
+	 * @access private
+	 */
+	private function ReadOwnerGit()
+	{
+		$exe = new GitPHP_GitExe($this);
+		$args = array();
+		$args[] = 'gitweb.owner';
+		$this->owner = $exe->Execute(GIT_CONFIG, $args);
+		unset($exe);
+	}
+
+	/**
+	 * ReadOwnerRaw
+	 *
+	 * Reads the project owner using the raw config file
+	 *
+	 * @access private
+	 */
+	private function ReadOwnerRaw()
+	{
+		// not worth writing a full config parser right now
+
+		if (!file_exists($this->GetPath() . '/config'))
+			return;
+
+		$configData = explode("\n", file_get_contents($this->GetPath() . '/config'));
+
+		$gitwebSection = false;
+		foreach ($configData as $configLine) {
+			$trimmed = trim($configLine);
+			if (empty($trimmed)) {
+				continue;
+			}
+
+			if (preg_match('/^\[(.+)\]$/', $trimmed, $regs)) {
+				// section header
+				$gitwebSection = ($regs[1] == 'gitweb');
+			} else if ($gitwebSection) {
+				$eq = strpos($trimmed, '=');
+				if ($eq === false) {
+					continue;
+				}
+
+				$key = trim(substr($trimmed, 0, $eq));
+				if ($key == 'owner') {
+					$this->owner = trim(substr($trimmed, $eq+1));
+					break;
+				}
+			}
+		}
+	}
+
+	/**
 	 * SetOwner
 	 *
 	 * Sets the project's owner (from an external source)
@@ -302,6 +399,19 @@
 	}
 
 	/**
+	 * GetProjectRoot
+	 *
+	 * Gets the project root
+	 *
+	 * @access public
+	 * @return string the project root
+	 */
+	public function GetProjectRoot()
+	{
+		return $this->projectRoot;
+	}
+
+	/**
 	 * GetSlug
 	 *
 	 * Gets the project as a filename/url friendly slug
@@ -311,15 +421,12 @@
 	 */
 	public function GetSlug()
 	{
-		$from = array(
-			'/',
-			'.git'
-		);
-		$to = array(
-			'-',
-			''
-		);
-		return str_replace($from, $to, $this->project);
+		$project = $this->project;
+
+		if (substr($project, -4) == '.git')
+			$project = substr($project, 0, -4);
+		
+		return GitPHP_Util::MakeSlug($project);
 	}
 
 	/**
@@ -332,9 +439,7 @@
 	 */
 	public function GetPath()
 	{
-		$projectRoot = GitPHP_Util::AddSlash(GitPHP_Config::GetInstance()->GetValue('projectroot'));
-
-		return $projectRoot . $this->project;
+		return $this->projectRoot . $this->project;
 	}
 
 	/**
@@ -564,11 +669,51 @@
 	{
 		$this->readHeadRef = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadHeadCommitGit();
+		} else {
+			$this->ReadHeadCommitRaw();
+		}
+	}
+
+	/**
+	 * ReadHeadCommitGit
+	 *
+	 * Read head commit using git executable
+	 *
+	 * @access private
+	 */
+	private function ReadHeadCommitGit()
+	{
 		$exe = new GitPHP_GitExe($this);
 		$args = array();
 		$args[] = '--verify';
 		$args[] = 'HEAD';
 		$this->head = trim($exe->Execute(GIT_REV_PARSE, $args));
+	}
+
+	/**
+	 * ReadHeadCommitRaw
+	 *
+	 * Read head commit using raw git head pointer
+	 *
+	 * @access private
+	 */
+	private function ReadHeadCommitRaw()
+	{
+		$head = trim(file_get_contents($this->GetPath() . '/HEAD'));
+		if (preg_match('/^([0-9A-Fa-f]{40})$/', $head, $regs)) {
+			/* Detached HEAD */
+			$this->head = $regs[1];
+		} else if (preg_match('/^ref: (.+)$/', $head, $regs)) {
+			/* standard pointer to head */
+			if (!$this->readRefs)
+				$this->ReadRefList();
+
+			if (isset($this->heads[$regs[1]])) {
+				$this->head = $this->heads[$regs[1]]->GetHash();
+			}
+		}
 	}
 
 	/**
@@ -741,10 +886,26 @@
 	 *
 	 * @access protected
 	 */
-	public function ReadRefList()
+	protected function ReadRefList()
 	{
 		$this->readRefs = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadRefListGit();
+		} else {
+			$this->ReadRefListRaw();
+		}
+	}
+
+	/**
+	 * ReadRefListGit
+	 *
+	 * Reads the list of refs for this project using the git executable
+	 *
+	 * @access private
+	 */
+	private function ReadRefListGit()
+	{
 		$exe = new GitPHP_GitExe($this);
 		$args = array();
 		$args[] = '--heads';
@@ -779,6 +940,117 @@
 	}
 
 	/**
+	 * ReadRefListRaw
+	 *
+	 * Reads the list of refs for this project using the raw git files
+	 *
+	 * @access private
+	 */
+	private function ReadRefListRaw()
+	{
+		$pathlen = strlen($this->GetPath()) + 1;
+
+		// read loose heads
+		$heads = $this->ListDir($this->GetPath() . '/refs/heads');
+		for ($i = 0; $i < count($heads); $i++) {
+			$key = trim(substr($heads[$i], $pathlen), "/\\");
+
+			if (isset($this->heads[$key])) {
+				continue;
+			}
+
+			$hash = trim(file_get_contents($heads[$i]));
+			if (preg_match('/^[0-9A-Fa-f]{40}$/', $hash)) {
+				$head = substr($key, strlen('refs/heads/'));
+				$this->heads[$key] = new GitPHP_Head($this, $head, $hash);
+			}
+		}
+
+		// read loose tags
+		$tags = $this->ListDir($this->GetPath() . '/refs/tags');
+		for ($i = 0; $i < count($tags); $i++) {
+			$key = trim(substr($tags[$i], $pathlen), "/\\");
+
+			if (isset($this->tags[$key])) {
+				continue;
+			}
+
+			$hash = trim(file_get_contents($tags[$i]));
+			if (preg_match('/^[0-9A-Fa-f]{40}$/', $hash)) {
+				$tag = substr($key, strlen('refs/tags/'));
+				$this->tags[$key] = $this->LoadTag($tag, $hash);
+			}
+		}
+
+		// check packed refs
+		if (file_exists($this->GetPath() . '/packed-refs')) {
+			$packedRefs = explode("\n", file_get_contents($this->GetPath() . '/packed-refs'));
+
+			$lastRef = null;
+			foreach ($packedRefs as $ref) {
+
+				if (preg_match('/^\^([0-9A-Fa-f]{40})$/', $ref, $regs)) {
+					// dereference of previous ref
+					if (($lastRef != null) && ($lastRef instanceof GitPHP_Tag)) {
+						$derefCommit = $this->GetCommit($regs[1]);
+						if ($derefCommit) {
+							$lastRef->SetCommit($derefCommit);
+						}
+					}
+				}
+
+				$lastRef = null;
+
+				if (preg_match('/^([0-9A-Fa-f]{40}) refs\/(tags|heads)\/(.+)$/', $ref, $regs)) {
+					// standard tag/head
+					$key = 'refs/' . $regs[2] . '/' . $regs[3];
+					if ($regs[2] == 'tags') {
+						if (!isset($this->tags[$key])) {
+							$lastRef = $this->LoadTag($regs[3], $regs[1]);
+							$this->tags[$key] = $lastRef;
+						}
+					} else if ($regs[2] == 'heads') {
+						if (!isset($this->heads[$key])) {
+							$this->heads[$key] = new GitPHP_Head($this, $regs[3], $regs[1]);
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * ListDir
+	 *
+	 * Recurses into a directory and lists files inside
+	 *
+	 * @access private
+	 * @param string $dir directory
+	 * @return array array of filenames
+	 */
+	private function ListDir($dir)
+	{
+		$files = array();
+		if ($dh = opendir($dir)) {
+			while (($file = readdir($dh)) !== false) {
+				if (($file == '.') || ($file == '..')) {
+					continue;
+				}
+				$fullFile = $dir . '/' . $file;
+				if (is_dir($fullFile)) {
+					$subFiles = $this->ListDir($fullFile);
+					if (count($subFiles) > 0) {
+						$files = array_merge($files, $subFiles);
+					}
+				} else {
+					$files[] = $fullFile;
+				}
+			}
+		}
+		return $files;
+	}
+
+	/**
 	 * GetTags
 	 *
 	 * Gets list of tags for this project by age descending
@@ -792,6 +1064,24 @@
 		if (!$this->readRefs)
 			$this->ReadRefList();
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			return $this->GetTagsGit($count);
+		} else {
+			return $this->GetTagsRaw($count);
+		}
+	}
+
+	/**
+	 * GetTagsGit
+	 *
+	 * Gets list of tags for this project by age descending using git executable
+	 *
+	 * @access private
+	 * @param integer $count number of tags to load
+	 * @return array array of tags
+	 */
+	private function GetTagsGit($count = 0)
+	{
 		$exe = new GitPHP_GitExe($this);
 		$args = array();
 		$args[] = '--sort=-creatordate';
@@ -818,6 +1108,27 @@
 	}
 
 	/**
+	 * GetTagsRaw
+	 *
+	 * Gets list of tags for this project by age descending using raw git objects
+	 *
+	 * @access private
+	 * @param integer $count number of tags to load
+	 * @return array array of tags
+	 */
+	private function GetTagsRaw($count = 0)
+	{
+		$tags = $this->tags;
+		usort($tags, array('GitPHP_Tag', 'CompareCreationEpoch'));
+
+		if (($count > 0) && (count($tags) > $count)) {
+			$tags = array_slice($tags, 0, $count);
+		}
+
+		return $tags;
+	}
+
+	/**
 	 * GetTag
 	 *
 	 * Gets a single tag
@@ -830,6 +1141,9 @@
 	{
 		if (empty($tag))
 			return null;
+
+		if (!$this->readRefs)
+			$this->ReadRefList();
 
 		$key = 'refs/tags/' . $tag;
 
@@ -877,6 +1191,24 @@
 		if (!$this->readRefs)
 			$this->ReadRefList();
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			return $this->GetHeadsGit($count);
+		} else {
+			return $this->GetHeadsRaw($count);
+		}
+	}
+
+	/**
+	 * GetHeadsGit
+	 *
+	 * Gets the list of sorted heads using the git executable
+	 *
+	 * @access private
+	 * @param integer $count number of tags to load
+	 * @return array array of heads
+	 */
+	private function GetHeadsGit($count = 0)
+	{
 		$exe = new GitPHP_GitExe($this);
 		$args = array();
 		$args[] = '--sort=-committerdate';
@@ -903,6 +1235,26 @@
 	}
 
 	/**
+	 * GetHeadsRaw
+	 *
+	 * Gets the list of sorted heads using raw git objects
+	 *
+	 * @access private
+	 * @param integer $count number of tags to load
+	 * @return array array of heads
+	 */
+	private function GetHeadsRaw($count = 0)
+	{
+		$heads = $this->heads;
+		usort($heads, array('GitPHP_Head', 'CompareAge'));
+
+		if (($count > 0) && (count($heads) > $count)) {
+			$heads = array_slice($heads, 0, $count);
+		}
+		return $heads;
+	}
+
+	/**
 	 * GetHead
 	 *
 	 * Gets a single head
@@ -916,6 +1268,9 @@
 		if (empty($head))
 			return null;
 
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
 		$key = 'refs/heads/' . $head;
 
 		if (!isset($this->heads[$key])) {
@@ -930,13 +1285,13 @@
 	 *
 	 * Gets log entries as an array of hashes
 	 *
-	 * @access public
+	 * @access private
 	 * @param string $hash hash to start the log at
 	 * @param integer $count number of entries to get
 	 * @param integer $skip number of entries to skip
 	 * @return array array of hashes
 	 */
-	public function GetLogHash($hash, $count = 50, $skip = 0)
+	private function GetLogHash($hash, $count = 50, $skip = 0)
 	{
 		return $this->RevList($hash, $count, $skip);
 	}
@@ -954,11 +1309,87 @@
 	 */
 	public function GetLog($hash, $count = 50, $skip = 0)
 	{
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false) || ($skip > GitPHP_Config::GetInstance()->GetValue('largeskip', 200)) ) {
+			return $this->GetLogGit($hash, $count, $skip);
+		} else {
+			return $this->GetLogRaw($hash, $count, $skip);
+		}
+	}
+
+	/**
+	 * GetLogGit
+	 *
+	 * Gets log entries using git exe
+	 *
+	 * @access private
+	 * @param string $hash hash to start the log at
+	 * @param integer $count number of entries to get
+	 * @param integer $skip number of entries to skip
+	 * @return array array of commit objects
+	 */
+	private function GetLogGit($hash, $count = 50, $skip = 0)
+	{
 		$log = $this->GetLogHash($hash, $count, $skip);
 		$len = count($log);
 		for ($i = 0; $i < $len; ++$i) {
 			$log[$i] = $this->GetCommit($log[$i]);
 		}
+		return $log;
+	}
+
+	/**
+	 * GetLogRaw
+	 *
+	 * Gets log entries using raw git objects
+	 * Based on history walking code from glip
+	 *
+	 * @access private
+	 */
+	private function GetLogRaw($hash, $count = 50, $skip = 0)
+	{
+		$total = $count + $skip;
+
+		$inc = array();
+		$num = 0;
+		$queue = array($this->GetCommit($hash));
+		while (($commit = array_shift($queue)) !== null) {
+			$parents = $commit->GetParents();
+			foreach ($parents as $parent) {
+				if (!isset($inc[$parent->GetHash()])) {
+					$inc[$parent->GetHash()] = 1;
+					$queue[] = $parent;
+					$num++;
+				} else {
+					$inc[$parent->GetHash()]++;
+				}
+			}
+			if ($num >= $total)
+				break;
+		}
+
+		$queue = array($this->GetCommit($hash));
+		$log = array();
+		$num = 0;
+		while (($commit = array_pop($queue)) !== null) {
+			array_push($log, $commit);
+			$num++;
+			if ($num == $total) {
+				break;
+			}
+			$parents = $commit->GetParents();
+			foreach ($parents as $parent) {
+				if (isset($inc[$parent->GetHash()])) {
+					if (--$inc[$parent->GetHash()] == 0) {
+						$queue[] = $parent;
+					}
+				}
+			}
+		}
+
+		if ($skip > 0) {
+			$log = array_slice($log, $skip, $count);
+		}
+		usort($log, array('GitPHP_Commit', 'CompareAge'));
 		return $log;
 	}
 
@@ -1204,6 +1635,22 @@
 	{
 		$this->epochRead = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadEpochGit();
+		} else {
+			$this->ReadEpochRaw();
+		}
+	}
+
+	/**
+	 * ReadEpochGit
+	 *
+	 * Reads this project's epoch using git executable
+	 *
+	 * @access private
+	 */
+	private function ReadEpochGit()
+	{
 		$exe = new GitPHP_GitExe($this);
 
 		$args = array();
@@ -1221,5 +1668,103 @@
 		unset($exe);
 	}
 
+	/**
+	 * ReadEpochRaw
+	 *
+	 * Reads this project's epoch using raw objects
+	 *
+	 * @access private
+	 */
+	private function ReadEpochRaw()
+	{
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
+		$epoch = 0;
+		foreach ($this->heads as $head) {
+			$commit = $head->GetCommit();
+			if ($commit) {
+				if ($commit->GetCommitterEpoch() > $epoch) {
+					$epoch = $commit->GetCommitterEpoch();
+				}
+			}
+		}
+		if ($epoch > 0) {
+			$this->epoch = $epoch;
+		}
+	}
+
+	/**
+	 * GetObject
+	 *
+	 * Gets the raw content of an object
+	 *
+	 * @access public
+	 * @param string $hash object hash
+	 * @return string object data
+	 */
+	public function GetObject($hash, &$type = 0)
+	{
+		if (!preg_match('/^[0-9A-Fa-f]{40}$/', $hash)) {
+			return false;
+		}
+
+		// first check if it's unpacked
+		$path = $this->GetPath() . '/objects/' . substr($hash, 0, 2) . '/' . substr($hash, 2);
+		if (file_exists($path)) {
+			list($header, $data) = explode("\0", gzuncompress(file_get_contents($path)), 2);
+			sscanf($header, "%s %d", $typestr, $size);
+			switch ($typestr) {
+				case 'commit':
+					$type = GitPHP_Pack::OBJ_COMMIT;
+					break;
+				case 'tree':
+					$type = GitPHP_Pack::OBJ_TREE;
+					break;
+				case 'blob':
+					$type = GitPHP_Pack::OBJ_BLOB;
+					break;
+				case 'tag':
+					$type = GitPHP_Pack::OBJ_TAG;
+					break;
+			}
+			return $data;
+		}
+
+		if (!$this->packsRead) {
+			$this->ReadPacks();
+		}
+
+		// then try packs
+		foreach ($this->packs as $pack) {
+			$data = $pack->GetObject($hash, $type);
+			if ($data !== false) {
+				return $data;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * ReadPacks
+	 *
+	 * Read the list of packs in the repository
+	 *
+	 * @access private
+	 */
+	private function ReadPacks()
+	{
+		$dh = opendir($this->GetPath() . '/objects/pack');
+		if ($dh !== false) {
+			while (($file = readdir($dh)) !== false) {
+				if (preg_match('/^pack-([0-9A-Fa-f]{40})\.idx$/', $file, $regs)) {
+					$this->packs[] = new GitPHP_Pack($this, $regs[1]);
+				}
+			}
+		}
+		$this->packsRead = true;
+	}
+
 }
 

--- a/include/git/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
@@ -68,7 +69,11 @@
 		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);

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

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

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

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

--- /dev/null
+++ b/include/git/ProjectListScmManager.class.php
@@ -1,1 +1,130 @@
+<?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')
+				continue;
+			if ($repository->public != 'true')
+				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) {
+				}
+			}
+		}
+	}
+
+	/**
+	 * IsSCMManager
+	 *
+	 * Tests if this file is an SCM manager config file
+	 *
+	 * @access protected
+	 * @returns true if file is an SCM manager config
+	 */
+	public static function IsSCMManager($file)
+	{
+		if (empty($file))
+			return false;
+
+		if (!(is_string($file) && is_file($file)))
+			return false;
+
+		$use_errors = libxml_use_internal_errors(true);
+
+		$xml = simplexml_load_file($file);
+
+		libxml_clear_errors();
+		libxml_use_internal_errors($use_errors);
+
+		if (!$xml)
+			return false;
+
+		if ($xml->getName() !== 'repository-db')
+			return false;
+
+		return true;
+	}
+
+}
+

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

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

--- a/include/git/Tree.class.php
+++ b/include/git/Tree.class.php
@@ -114,6 +114,24 @@
 	{
 		$this->contentsRead = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadContentsGit();
+		} else {
+			$this->ReadContentsRaw();
+		}
+
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadContentsGit
+	 *
+	 * Reads the tree contents using the git executable
+	 *
+	 * @access private
+	 */
+	private function ReadContentsGit()
+	{
 		$exe = new GitPHP_GitExe($this->GetProject());
 
 		$args = array();
@@ -157,7 +175,58 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadContentsRaw
+	 *
+	 * Reads the tree contents using the raw git object
+	 *
+	 * @access private
+	 */
+	private function ReadContentsRaw()
+	{
+		$treeData = $this->GetProject()->GetObject($this->hash);
+
+		$start = 0;
+		$len = strlen($treeData);
+		while ($start < $len) {
+			$pos = strpos($treeData, "\0", $start);
+
+			list($mode, $path) = explode(' ', substr($treeData, $start, $pos-$start), 2);
+			$mode = str_pad($mode, 6, '0', STR_PAD_LEFT);
+			$hash = bin2hex(substr($treeData, $pos+1, 20));
+			$start = $pos + 21;
+
+			$octmode = octdec($mode);
+
+			if ($octmode == 57344) {
+				// submodules not currently supported
+				continue;
+			}
+
+			if (!empty($this->path))
+				$path = $this->path . '/' . $path;
+
+			$obj = null;
+			if ($octmode & 0x4000) {
+				// tree
+				$obj = $this->GetProject()->GetTree($hash);
+			} else {
+				// blob
+				$obj = $this->GetProject()->GetBlob($hash);
+			}
+
+			if (!$obj) {
+				continue;
+			}
+
+			$obj->SetMode($mode);
+			$obj->SetPath($path);
+			if ($this->commit)
+				$obj->SetCommit($this->commit);
+			$this->contents[] = $obj;
+		}
 	}
 
 	/**

--- a/include/version.php
+++ b/include/version.php
@@ -12,7 +12,7 @@
 /**
  * Defines the version
  */
-$gitphp_version = "0.2.3";
+$gitphp_version = "0.2.4";
 
 /**
  * Defines the app string (app name and version)

file:a/index.php -> file:b/index.php
--- a/index.php
+++ b/index.php
@@ -130,9 +130,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);
 

file:b/js/commitdiff.js (new)
--- /dev/null
+++ b/js/commitdiff.js
@@ -1,1 +1,77 @@
+/*
+ * GitPHP javascript commitdiff
+ * 
+ * Javascript enhancements to make side-by-side
+ * commitdiff more usable
+ *
+ * @author Christopher Han <xiphux@gmail.com>
+ * @copyright Copyright (c) 2011 Christopher Han
+ * @package GitPHP
+ */
 
+var TOCYloc = null;
+var TOCposition = null;
+var TOCtop = null;
+
+function initSBSCommitDiff() {
+	var sbsTOC = $('div.commitDiffSBS div.SBSTOC');
+	if (sbsTOC.size() < 1) {
+		return;
+	}
+
+	TOCYloc = sbsTOC.position().top;
+	TOCposition = sbsTOC.css('position');
+	TOCtop = sbsTOC.css('top');
+	$(window).scroll(function() {
+		var windowYloc = $(document).scrollTop();
+		if (windowYloc > TOCYloc) {
+			sbsTOC.css('position', 'fixed');
+			sbsTOC.css('top', '0px');
+		} else {
+			sbsTOC.css('position', TOCposition);
+			sbsTOC.css('top', TOCtop);
+		}
+	});
+
+	$('a.SBSTOCItem').click(function() {
+		var clickedItem = $(this).get(0);
+		$('a.SBSTOCItem').each(function(index, value) {
+			if (clickedItem == value) {
+				$(this).parent().addClass('activeItem');
+			} else {
+				$(this).parent().removeClass('activeItem');
+			}
+		});
+		var clickedId = $(this).attr('href').substring(1);
+		$('div.diffBlob').each(function() {
+			if ($(this).attr('id') == clickedId) {
+				$(this).slideDown('fast');
+			} else {
+				$(this).slideUp('fast');
+			}
+		});
+		$('a.showAll').show();
+		if ($(document).scrollTop() > $('div.SBSContent').offset().top) {
+			$('html, body').animate({
+				scrollTop: $('div.SBSContent').offset().top
+			}, 200);
+		}
+		return false;
+	});
+	$('a.showAll').click(function() {
+		$('a.SBSTOCItem').parent().removeClass('activeItem');
+		$('div.diffBlob').slideDown('fast');
+		$(this).hide();
+		if ($(document).scrollTop() > $('div.SBSContent').offset().top) {
+			$('html, body').animate({
+				scrollTop: $('div.SBSContent').offset().top
+			}, 200);
+		}
+		return false;
+	});
+};
+
+$(document).ready(function() {
+	initSBSCommitDiff();
+});
+

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

--- a/templates/commitdiff.tpl
+++ b/templates/commitdiff.tpl
@@ -31,8 +31,46 @@
      {$line|htmlspecialchars|buglink:$bugpattern:$bugurl}<br />
    {/foreach}
    <br />
+
+   {if $sidebyside && ($treediff->Count() > 1)}
+    <div class="commitDiffSBS">
+
+     <div class="SBSTOC">
+       <ul>
+       <li class="listcount">
+       {t count=$treediff->Count() 1=$treediff->Count() plural="%1 files changed:"}%1 file changed:{/t} <a href="#" class="showAll">{t}(show all){/t}</a></li>
+       {foreach from=$treediff item=filediff}
+       <li>
+       <a href="#{$filediff->GetFromHash()}_{$filediff->GetToHash()}" class="SBSTOCItem">
+       {if $filediff->GetStatus() == 'A'}
+         {if $filediff->GetToFile()}{$filediff->GetToFile()}{else}{$filediff->GetToHash()}{/if} {t}(new){/t}
+       {elseif $filediff->GetStatus() == 'D'}
+         {if $filediff->GetFromFile()}{$filediff->GetFromFile()}{else}{$filediff->GetToFile()}{/if} {t}(deleted){/t}
+       {elseif $filediff->GetStatus() == 'M'}
+         {if $filediff->GetFromFile()}
+	   {assign var=fromfilename value=$filediff->GetFromFile()}
+	 {else}
+	   {assign var=fromfilename value=$filediff->GetFromHash()}
+	 {/if}
+	 {if $filediff->GetToFile()}
+	   {assign var=tofilename value=$filediff->GetToFile()}
+	 {else}
+	   {assign var=tofilename value=$filediff->GetToHash()}
+	 {/if}
+	 {$fromfilename}{if $fromfilename != $tofilename} -&gt; {$tofilename}{/if}
+       {/if}
+       </a>
+       </li>
+       {/foreach}
+       </ul>
+     </div>
+
+     <div class="SBSContent">
+   {/if}
+
    {* Diff each file changed *}
    {foreach from=$treediff item=filediff}
+     <div class="diffBlob" id="{$filediff->GetFromHash()}_{$filediff->GetToHash()}">
      <div class="diff_info">
      {if ($filediff->GetStatus() == 'D') || ($filediff->GetStatus() == 'M')}
        {assign var=localfromtype value=$filediff->GetFromFileType(1)}
@@ -60,7 +98,17 @@
      {else}
         {include file='filediff.tpl' diff=$filediff->GetDiff('', true, true)}
      {/if}
+     </div>
    {/foreach}
+
+   {if $sidebyside && ($treediff->Count() > 1)}
+     </div>
+     <div class="SBSFooter"></div>
+
+    </div>
+   {/if}
+
+
  </div>
 
  {include file='footer.tpl'}

--- a/templates/filediffsidebyside.tpl
+++ b/templates/filediffsidebyside.tpl
@@ -10,19 +10,37 @@
  * @subpackage Template
  *}
 <table class="diffTable">
-  {foreach from=$diffsplit item=lineinfo}
-    {if $lineinfo[0]=='added'}
-    <tr class="diff-added">
-    {elseif $lineinfo[0]=='deleted'}
-    <tr class="diff-deleted">
-    {elseif $lineinfo[0]=='modified'}
-    <tr class="diff-modified">
-    {else}
-    <tr>
-    {/if}
-      <td class="diff-left">{if $lineinfo[1]}{$lineinfo[1]|escape}{else}&nbsp;{/if}</td>
-      <td>{if $lineinfo[2]}{$lineinfo[2]|escape}{else}&nbsp;{/if}</td>
-    </tr>
-  {/foreach}
+  {if $filediff->GetStatus() == 'D'}
+    {assign var=delblob value=$filediff->GetFromBlob()}
+    {foreach from=$delblob->GetData(true) item=blobline}
+      <tr class="diff-deleted">
+        <td class="diff-left">{$blobline|escape}</td>
+	<td>&nbsp;</td>
+      </tr>
+    {/foreach}
+  {elseif $filediff->GetStatus() == 'A'}
+    {assign var=newblob value=$filediff->GetToBlob()}
+    {foreach from=$newblob->GetData(true) item=blobline}
+      <tr class="diff-added">
+        <td class="diff-left">&nbsp;</td>
+	<td>{$blobline|escape}</td>
+      </tr>
+    {/foreach}
+  {else}
+    {foreach from=$diffsplit item=lineinfo}
+      {if $lineinfo[0]=='added'}
+      <tr class="diff-added">
+      {elseif $lineinfo[0]=='deleted'}
+      <tr class="diff-deleted">
+      {elseif $lineinfo[0]=='modified'}
+      <tr class="diff-modified">
+      {else}
+      <tr>
+      {/if}
+        <td class="diff-left">{if $lineinfo[1]}{$lineinfo[1]|escape}{else}&nbsp;{/if}</td>
+        <td>{if $lineinfo[2]}{$lineinfo[2]|escape}{else}&nbsp;{/if}</td>
+      </tr>
+    {/foreach}
+  {/if}
 </table>
 

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