Load project log using raw git objects
Load project log using raw git objects

Based on the log walking code from glip

Walking the log in raw php is a bit of a mixed bag as far as
performance. If you're walking commits close to the tip of the head
performance is good, because you save the shell call to git-rev-list and
performance wise it's light to load the parents of the first 50 commits.
However, in raw PHP we have to do the walking ourselves, which means we
can't --skip the first 100 or so commits - so when listing commits
several pages away from the head, we have to walk all the way from the
tip down to that page, and then discard the more recent commits we don't
care about.
So the loading time increases for each log page further away from the
tip (earlier commits).

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

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

--- a/css/gitphpskin.css
+++ b/css/gitphpskin.css
@@ -414,6 +414,8 @@
 	float: left;
 	width: 19%;
 	word-wrap: break-word;
+	background-color: #ffffff;
+	border-bottom: 1px solid #edece6;
 }
 
 div.commitDiffSBS div.SBSTOC a

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

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

--- a/include/git/Commit.class.php
+++ b/include/git/Commit.class.php
@@ -523,34 +523,40 @@
 	{
 		$this->dataRead = true;
 
-		/* get data from git_rev_list */
-		$exe = new GitPHP_GitExe($this->GetProject());
-		$args = array();
-		$args[] = '--header';
-		$args[] = '--parents';
-		$args[] = '--max-count=1';
-		$args[] = $this->hash;
-		$ret = $exe->Execute(GIT_REV_LIST, $args);
-		unset($exe);
-
-		$lines = explode("\n", $ret);
-
-		if (!isset($lines[0]))
-			return;
-
-		/* In case we returned something unexpected */
-		$tok = strtok($lines[0], ' ');
-		if ($tok != $this->hash)
-			return;
-
-		/* Read all parents */
-		$tok = strtok(' ');
-		while ($tok !== false) {
-			try {
-				$this->parents[] = $this->GetProject()->GetCommit($tok);
-			} catch (Exception $e) {
-			}
-			$tok = strtok(' ');
+		$lines = null;
+
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+
+			/* get data from git_rev_list */
+			$exe = new GitPHP_GitExe($this->GetProject());
+			$args = array();
+			$args[] = '--header';
+			$args[] = '--parents';
+			$args[] = '--max-count=1';
+			$args[] = $this->hash;
+			$ret = $exe->Execute(GIT_REV_LIST, $args);
+			unset($exe);
+
+			$lines = explode("\n", $ret);
+
+			if (!isset($lines[0]))
+				return;
+
+			/* In case we returned something unexpected */
+			$tok = strtok($lines[0], ' ');
+			if ($tok != $this->hash)
+				return;
+
+			array_shift($lines);
+
+		} else {
+			
+			$data = $this->GetProject()->GetObject($this->hash);
+			if (empty($data))
+				return;
+
+			$lines = explode("\n", $data);
+
 		}
 
 		foreach ($lines as $i => $line) {
@@ -562,6 +568,12 @@
 						$tree->SetCommit($this);
 						$this->tree = $tree;
 					}
+				} catch (Exception $e) {
+				}
+			} else if (preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
+				/* Parent */
+				try {
+					$this->parents[] = $this->GetProject()->GetCommit($regs[1]);
 				} catch (Exception $e) {
 				}
 			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
@@ -576,14 +588,12 @@
 				$this->committerTimezone = $regs[3];
 			} else {
 				/* commit comment */
-				if (!(preg_match('/^[0-9a-fA-F]{40}/', $line) || preg_match('/^parent [0-9a-fA-F]{40}/', $line))) {
-					$trimmed = trim($line);
-					if (empty($this->title) && (strlen($trimmed) > 0))
-						$this->title = $trimmed;
-					if (!empty($this->title)) {
-						if ((strlen($trimmed) > 0) || ($i < (count($lines)-1)))
-							$this->comment[] = $trimmed;
-					}
+				$trimmed = trim($line);
+				if (empty($this->title) && (strlen($trimmed) > 0))
+					$this->title = $trimmed;
+				if (!empty($this->title)) {
+					if ((strlen($trimmed) > 0) || ($i < (count($lines)-1)))
+						$this->comment[] = $trimmed;
 				}
 			}
 		}
@@ -1004,5 +1014,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
@@ -617,7 +617,7 @@
 			}
 		}
 
-		$this->diffData = GitPHP_DiffExe::Diff((empty($fromTmpFile) ? null : ($tmpdir->GetDir() . $fromTmpFile)), $fromName, (empty($toTmpFile) ? null : ($tmpdir->GetDir() . $toTmpFile)), $toName);
+		$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);

--- 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,538 @@
-
+<?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;
+
+	/**
+	 * __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;
+		}
+
+		$offset = false;
+
+		$index = fopen($this->project->GetPath() . '/objects/pack/pack-' . $this->hash . '.idx', '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);
+		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);
+
+			$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
@@ -187,6 +188,24 @@
 	 * @access protected
 	 */
 	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
@@ -564,11 +583,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 +800,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 +854,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
@@ -831,6 +1017,9 @@
 		if (empty($tag))
 			return null;
 
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
 		$key = 'refs/tags/' . $tag;
 
 		if (!isset($this->tags[$key])) {
@@ -916,6 +1105,9 @@
 		if (empty($head))
 			return null;
 
+		if (!$this->readRefs)
+			$this->ReadRefList();
+
 		$key = 'refs/heads/' . $head;
 
 		if (!isset($this->heads[$key])) {
@@ -930,13 +1122,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 +1146,85 @@
 	 */
 	public function GetLog($hash, $count = 50, $skip = 0)
 	{
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			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 (--$inc[$parent->GetHash()] == 0) {
+					$queue[] = $parent;
+				}
+			}
+		}
+
+		if ($skip > 0) {
+			$log = array_slice($log, $skip, $count);
+		}
+		usort($log, array('GitPHP_Commit', 'CompareAge'));
 		return $log;
 	}
 
@@ -1221,5 +1487,77 @@
 		unset($exe);
 	}
 
+	/**
+	 * 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/Tag.class.php
+++ b/include/git/Tag.class.php
@@ -322,6 +322,24 @@
 	{
 		$this->dataRead = true;
 
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false)) {
+			$this->ReadDataGit();
+		} else {
+			$this->ReadDataRaw();
+		}
+
+		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadDataGit
+	 *
+	 * Reads the tag data using the git executable
+	 *
+	 * @access private
+	 */
+	private function ReadDataGit()
+	{
 		$exe = new GitPHP_GitExe($this->GetProject());
 		$args = array();
 		$args[] = '-t';
@@ -406,8 +424,84 @@
 				}
 				break;
 		}
-
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+	}
+
+	/**
+	 * ReadDataRaw
+	 *
+	 * Reads the tag data using the raw git object
+	 *
+	 * @access private
+	 */
+	private function ReadDataRaw()
+	{
+		$data = $this->GetProject()->GetObject($this->GetHash(), $type);
+		
+		if ($type == GitPHP_Pack::OBJ_COMMIT) {
+			/* light tag */
+			$this->object = $this->GetProject()->GetCommit($this->GetHash());
+			$this->commit = $this->object;
+			$this->type = 'commit';
+			GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+			return;
+		}
+
+		$lines = explode("\n", $data);
+
+		if (!isset($lines[0]))
+			return;
+
+		$objectHash = null;
+
+		$readInitialData = false;
+		foreach ($lines as $i => $line) {
+			if (!$readInitialData) {
+				if (preg_match('/^object ([0-9a-fA-F]{40})$/', $line, $regs)) {
+					$objectHash = $regs[1];
+					continue;
+				} else if (preg_match('/^type (.+)$/', $line, $regs)) {
+					$this->type = $regs[1];
+					continue;
+				} else if (preg_match('/^tag (.+)$/', $line, $regs)) {
+					continue;
+				} else if (preg_match('/^tagger (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+					$this->tagger = $regs[1];
+					$this->taggerEpoch = $regs[2];
+					$this->taggerTimezone = $regs[3];
+					continue;
+				}
+			}
+
+			$trimmed = trim($line);
+
+			if ((strlen($trimmed) > 0) || ($readInitialData === true)) {
+				$this->comment[] = $line;
+			}
+			$readInitialData = true;
+		}
+
+		switch ($this->type) {
+			case 'commit':
+				try {
+					$this->object = $this->GetProject()->GetCommit($objectHash);
+					$this->commit = $this->object;
+				} catch (Exception $e) {
+				}
+				break;
+			case 'tag':
+				$objectData = $this->GetProject()->GetObject($objectHash);
+				$lines = explode("\n", $objectData);
+				foreach ($lines as $i => $line) {
+					if (preg_match('/^tag (.+)$/', $line, $regs)) {
+						$name = trim($regs[1]);
+						$this->object = $this->GetProject()->GetTag($name);
+						if ($this->object) {
+							$this->object->SetHash($objectHash);
+						}
+					}
+				}
+				break;
+		}
 	}
 
 	/**
@@ -597,9 +691,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)

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

--- 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.3\n"
 "Report-Msgid-Bugs-To: xiphux@gmail.com\n"
-"POT-Creation-Date: 2010-12-02 22:09-0600\n"
+"POT-Creation-Date: 2011-06-18 21:03-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,7 +293,7 @@
 
 # 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 ""
@@ -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 ""
 
@@ -514,28 +519,28 @@
 # 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:221
 #, 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:225
 #, 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:229
 #, 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:235
 #, php-format
 msgid "%1$s is outside of the projectroot"
 msgstr ""
@@ -775,3 +780,10 @@
 "\" 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 ""
+

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

comments