Also fix displayed commit for renamed files
Also fix displayed commit for renamed files

--- a/config/projects.conf.php.example
+++ b/config/projects.conf.php.example
@@ -93,6 +93,8 @@
  *	     Compatibility mode relies more on the git executable for loading
  *	     data, at the expense of performance.  Use if you are having
  *	     trouble loading data for this project.
+ *
+ * 'website': the website url for the project.
  */
 //$git_projects_settings['php/gitphp.git'] = array(
 //	'category' => 'PHP',
@@ -102,7 +104,8 @@
 //	'pushurl' => '',
 //	'bugpattern' => '/#([0-9]+)/',
 //	'bugurl' => 'http://mantis.xiphux.com/view.php?id=${1}',
-//	'compat' => false
+//	'compat' => false,
+//	'website' => 'http://xiphux.com/programming/gitphp/'
 //);
 //$git_projects_settings['gentoo.git'] = array(
 //	'description' => 'Gentoo portage overlay',

file:b/css/.gitignore (new)
--- /dev/null
+++ b/css/.gitignore
@@ -1,1 +1,2 @@
+*.min.css
 

--- a/css/gitphp.css
+++ b/css/gitphp.css
@@ -23,6 +23,21 @@
  */
 div.title a.title {
 	display: block; 
+}
+
+/*
+ * Project list
+ */
+.projectList .projectRow .projectOwner {
+	white-space: nowrap;
+}
+
+.projectList .projectRow .projectAge {
+	white-space: nowrap;
+}
+
+.projectList .projectRow .link {
+	white-space: nowrap;
 }
 
 

--- a/css/gitphpskin.css
+++ b/css/gitphpskin.css
@@ -117,6 +117,16 @@
 	float: left; 
 	color: #555555;
 	font-style: italic; 
+}
+
+div.page_footer_text a {
+	color: #555555;
+	font-style: italic;
+	text-decoration: none;
+}
+
+div.page_footer_text a:hover {
+	text-decoration: underline;
 }
 
 

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

--- a/include/controller/ControllerBase.class.php
+++ b/include/controller/ControllerBase.class.php
@@ -250,7 +250,7 @@
 			// backwards compatibility
 			$stylesheet = 'gitphpskin.css';
 		}
-		$this->tpl->assign('stylesheet', $stylesheet);
+		$this->tpl->assign('stylesheet', preg_replace('/\.css$/', '', $stylesheet));
 
 		$this->tpl->assign('javascript', GitPHP_Config::GetInstance()->GetValue('javascript', true));
 		$this->tpl->assign('pagetitle', GitPHP_Config::GetInstance()->GetValue('title', $gitphp_appstring));

--- a/include/controller/Controller_Blob.class.php
+++ b/include/controller/Controller_Blob.class.php
@@ -168,7 +168,7 @@
 		}
 
 		$blob = $this->project->GetBlob($this->params['hash']);
-		if ($this->params['file'])
+		if (!empty($this->params['file']))
 			$blob->SetPath($this->params['file']);
 		$blob->SetCommit($commit);
 		$this->tpl->assign('blob', $blob);

--- a/include/controller/Controller_Snapshot.class.php
+++ b/include/controller/Controller_Snapshot.class.php
@@ -38,10 +38,18 @@
 	 */
 	public function __construct()
 	{
-		parent::__construct();
+		if (isset($_GET['p'])) {
+			$this->project = GitPHP_ProjectList::GetInstance()->GetProject(str_replace(chr(0), '', $_GET['p']));
+			if (!$this->project) {
+				throw new GitPHP_MessageException(sprintf(__('Invalid project %1$s'), $_GET['p']), true);
+			}
+		}
+
 		if (!$this->project) {
 			throw new GitPHP_MessageException(__('Project is required'), true);
 		}
+
+		$this->ReadQuery();
 	}
 
 	/**
@@ -54,7 +62,6 @@
 	 */
 	protected function GetTemplate()
 	{
-		return 'snapshot.tpl';
 	}
 
 	/**
@@ -118,10 +125,24 @@
 	{
 		$this->archive = new GitPHP_Archive($this->project, null, $this->params['format'], (isset($this->params['path']) ? $this->params['path'] : ''), (isset($this->params['prefix']) ? $this->params['prefix'] : ''));
 
-		$headers = $this->archive->GetHeaders();
-		
-		if (count($headers) > 0)
-			$this->headers = array_merge($this->headers, $headers);
+		switch ($this->archive->GetFormat()) {
+			case GITPHP_COMPRESS_TAR:
+				$this->headers[] = 'Content-Type: application/x-tar';
+				break;
+			case GITPHP_COMPRESS_BZ2:
+				$this->headers[] = 'Content-Type: application/x-bzip2';
+				break;
+			case GITPHP_COMPRESS_GZ:
+				$this->headers[] = 'Content-Type: application/x-gzip';
+				break;
+			case GITPHP_COMPRESS_ZIP:
+				$this->headers[] = 'Content-Type: application/x-zip';
+				break;
+			default:
+				throw new Exception('Unknown compression type');
+		}
+
+		$this->headers[] = 'Content-Disposition: attachment; filename=' . $this->archive->GetFilename();
 	}
 
 	/**
@@ -141,8 +162,69 @@
 			$commit = $this->project->GetCommit($this->params['hash']);
 
 		$this->archive->SetObject($commit);
-
-		$this->tpl->assign('archive', $this->archive->GetData());
+	}
+
+	/**
+	 * Render
+	 *
+	 * Render this controller
+	 *
+	 * @access public
+	 */
+	public function Render()
+	{
+		$this->LoadData();
+
+		$cache = GitPHP_Config::GetInstance()->GetValue('cache', false);
+		$cachehandle = false;
+		$cachefile = '';
+		if ($cache && is_dir(GITPHP_CACHE)) {
+			$key = ($this->archive->GetObject() ? $this->archive->GetObject()->GetHash() : '') . '|' . (isset($this->params['path']) ? $this->params['path'] : '') . '|' . (isset($this->params['prefix']) ? $this->params['prefix'] : '');
+			$cachefile = sha1($key) . '-' . $this->archive->GetFilename();
+			$cachedfilepath = GITPHP_CACHE . $cachefile;
+
+			if (file_exists($cachedfilepath)) {
+				// read cached file
+				$cachehandle = fopen($cachedfilepath, 'rb');
+				if ($cachehandle) {
+					while (!feof($cachehandle)) {
+						print fread($cachehandle, 1048576);
+						flush();
+					}
+					fclose($cachehandle);
+					return;
+				}
+			}
+		}
+
+		if ($this->archive->Open()) {
+
+			$tmpcachefile = '';
+
+			if ($cache && !empty($cachefile)) {
+				// write cached file too
+				$tmpcachefile = 'tmp-' . $cachefile;
+				$cachehandle = fopen(GITPHP_CACHE . $tmpcachefile, 'wb');
+			}
+
+			while (($data = $this->archive->Read()) !== false) {
+
+				print $data;
+				flush();
+
+				if ($cache && $cachehandle) {
+					fwrite($cachehandle, $data);
+				}
+
+			}
+			$this->archive->Close();
+
+			if ($cachehandle) {
+				fclose($cachehandle);
+				sleep(1);
+				rename(GITPHP_CACHE . $tmpcachefile, GITPHP_CACHE . $cachefile);
+			}
+		}
 	}
 
 }

--- a/include/git/Archive.class.php
+++ b/include/git/Archive.class.php
@@ -81,6 +81,24 @@
 	protected $prefix = '';
 
 	/**
+	 * handle
+	 *
+	 * Stores the process handle
+	 *
+	 * @access protected
+	 */
+	protected $handle = false;
+
+	/**
+	 * tempfile
+	 *
+	 * Stores the temp file name
+	 *
+	 * @access protected
+	 */
+	protected $tempfile = '';
+
+	/**
 	 * __construct
 	 *
 	 * Instantiates object
@@ -319,54 +337,22 @@
 	}
 
 	/**
-	 * GetHeaders
-	 *
-	 * Gets the headers to send to the browser for this file
-	 *
-	 * @access public
-	 * @return array header strings
-	 */
-	public function GetHeaders()
-	{
-		$headers = array();
-
-		switch ($this->format) {
-			case GITPHP_COMPRESS_TAR:
-				$headers[] = 'Content-Type: application/x-tar';
-				break;
-			case GITPHP_COMPRESS_BZ2:
-				$headers[] = 'Content-Type: application/x-bzip2';
-				break;
-			case GITPHP_COMPRESS_GZ:
-				$headers[] = 'Content-Type: application/x-gzip';
-				break;
-			case GITPHP_COMPRESS_ZIP:
-				$headers[] = 'Content-Type: application/x-zip';
-				break;
-			default:
-				throw new Exception('Unknown compression type');
-		}
-
-		if (count($headers) > 0) {
-			$headers[] = 'Content-Disposition: attachment; filename=' . $this->GetFilename();
-		}
-
-		return $headers;
-	}
-
-	/**
-	 * GetData
-	 *
-	 * Gets the archive data
-	 *
-	 * @access public
-	 * @return string archive data
-	 */
-	public function GetData()
+	 * Open
+	 *
+	 * Opens a descriptor for reading archive data
+	 *
+	 * @access public
+	 * @return boolean true on success
+	 */
+	public function Open()
 	{
 		if (!$this->gitObject)
 		{
 			throw new Exception('Invalid object for archive');
+		}
+
+		if ($this->handle) {
+			return true;
 		}
 
 		$exe = new GitPHP_GitExe($this->GetProject());
@@ -387,16 +373,93 @@
 		$args[] = '--prefix=' . $this->GetPrefix();
 		$args[] = $this->gitObject->GetHash();
 
-		$data = $exe->Execute(GIT_ARCHIVE, $args);
+		$this->handle = $exe->Open(GIT_ARCHIVE, $args);
 		unset($exe);
 
-		switch ($this->format) {
-			case GITPHP_COMPRESS_BZ2:
-				$data = bzcompress($data, GitPHP_Config::GetInstance()->GetValue('compresslevel', 4));
-				break;
-			case GITPHP_COMPRESS_GZ:
-				$data = gzencode($data, GitPHP_Config::GetInstance()->GetValue('compresslevel', -1));
-				break;
+		if ($this->format == GITPHP_COMPRESS_GZ) {
+			// hack to get around the fact that gzip files
+			// can't be compressed on the fly and the php zlib stream
+			// doesn't seem to daisy chain with any non-file streams
+
+			$this->tempfile = tempnam(sys_get_temp_dir(), "GitPHP");
+
+			$compress = GitPHP_Config::GetInstance()->GetValue('compresslevel');
+
+			$mode = 'wb';
+			if (is_int($compress) && ($compress >= 1) && ($compress <= 9))
+				$mode .= $compress;
+
+			$temphandle = gzopen($this->tempfile, $mode);
+			if ($temphandle) {
+				while (!feof($this->handle)) {
+					gzwrite($temphandle, fread($this->handle, 1048576));
+				}
+				gzclose($temphandle);
+
+				$temphandle = fopen($this->tempfile, 'rb');
+			}
+			
+			if ($this->handle) {
+				pclose($this->handle);
+			}
+			$this->handle = $temphandle;
+		}
+
+		return ($this->handle !== false);
+	}
+
+	/**
+	 * Close
+	 *
+	 * Close the archive data descriptor
+	 *
+	 * @access public
+	 * @return boolean true on success
+	 */
+	public function Close()
+	{
+		if (!$this->handle) {
+			return true;
+		}
+
+		if ($this->format == GITPHP_COMPRESS_GZ) {
+			fclose($this->handle);
+			if (!empty($this->tempfile)) {
+				unlink($this->tempfile);
+				$this->tempfile = '';
+			}
+		} else {
+			pclose($this->handle);
+		}
+
+		$this->handle = null;
+		
+		return true;
+	}
+
+	/**
+	 * Read
+	 *
+	 * Read a chunk of the archive data
+	 *
+	 * @access public
+	 * @param int $size size of data to read
+	 * @return string archive data
+	 */
+	public function Read($size = 1048576)
+	{
+		if (!$this->handle) {
+			return false;
+		}
+
+		if (feof($this->handle)) {
+			return false;
+		}
+
+		$data = fread($this->handle, $size);
+
+		if ($this->format == GITPHP_COMPRESS_BZ2) {
+			$data = bzcompress($data, GitPHP_Config::GetInstance()->GetValue('compresslevel', 4));
 		}
 
 		return $data;

--- a/include/git/Blob.class.php
+++ b/include/git/Blob.class.php
@@ -144,7 +144,7 @@
 			$this->data = $this->GetProject()->GetObject($this->hash);
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**

--- a/include/git/Commit.class.php
+++ b/include/git/Commit.class.php
@@ -617,7 +617,7 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -658,8 +658,10 @@
 		$projectRefs = $this->GetProject()->GetRefs('tags');
 
 		foreach ($projectRefs as $ref) {
-			if ($ref->GetCommit()->GetHash() === $this->hash) {
-				$tags[] = $ref;
+			if (($ref->GetType() == 'tag') || ($ref->GetType() == 'commit')) {
+				if ($ref->GetCommit()->GetHash() === $this->hash) {
+					$tags[] = $ref;
+				}
 			}
 		}
 
@@ -708,7 +710,7 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -769,7 +771,7 @@
 			$this->ReadHashPathsRaw($this->GetTree());
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**

--- a/include/git/GitExe.class.php
+++ b/include/git/GitExe.class.php
@@ -97,21 +97,53 @@
 	 */
 	public function Execute($command, $args)
 	{
+		$fullCommand = $this->CreateCommand($command, $args);
+
+		GitPHP_Log::GetInstance()->Log('Begin executing "' . $fullCommand . '"');
+
+		$ret = shell_exec($fullCommand);
+
+		GitPHP_Log::GetInstance()->Log('Finish executing "' . $fullCommand . '"' .
+			"\nwith result: " . $ret);
+
+		return $ret;
+	}
+
+	/**
+	 * Open
+	 *
+	 * Opens a resource to a command
+	 *
+	 * @param string $command the command to execute
+	 * @param array $args arguments
+	 * @return resource process handle
+	 */
+	public function Open($command, $args, $mode = 'r')
+	{
+		$fullCommand = $this->CreateCommand($command, $args);
+
+		return popen($fullCommand, $mode);
+	}
+
+	/**
+	 * BuildCommand
+	 *
+	 * Creates a command
+	 *
+	 * @access protected
+	 *
+	 * @param string $command the command to execute
+	 * @param array $args arguments
+	 * @return string result of command
+	 */
+	protected function CreateCommand($command, $args)
+	{
 		$gitDir = '';
 		if ($this->project) {
 			$gitDir = '--git-dir=' . $this->project->GetPath();
 		}
 		
-		$fullCommand = $this->binary . ' ' . $gitDir . ' ' . $command . ' ' . implode(' ', $args);
-
-		GitPHP_Log::GetInstance()->Log('Begin executing "' . $fullCommand . '"');
-
-		$ret = shell_exec($fullCommand);
-
-		GitPHP_Log::GetInstance()->Log('Finish executing "' . $fullCommand . '"' .
-			"\nwith result: " . $ret);
-
-		return $ret;
+		return $this->binary . ' ' . $gitDir . ' ' . $command . ' ' . implode(' ', $args);
 	}
 
 	/**

--- a/include/git/Project.class.php
+++ b/include/git/Project.class.php
@@ -219,6 +219,15 @@
 /*}}}2*/
 
 	/**
+	 * website
+	 *
+	 * Stores the website url internally
+	 *
+	 * @access protected
+	 */
+	protected $website = null;
+
+	/**
 	 * commitCache
 	 *
 	 * Caches fetched commit objects in case of
@@ -724,6 +733,36 @@
 	public function SetBugPattern($bPat)
 	{
 		$this->bugPattern = $bPat;
+	}
+
+/*}}}2*/
+
+/* website accessors {{{2*/
+
+	/**
+	 * GetWebsite
+	 *
+	 * Gets the website for this repository, if specified
+	 *
+	 * @access public
+	 * @return string website
+	 */
+	public function GetWebsite()
+	{
+		return $this->website;
+	}
+
+	/**
+	 * SetWebsite
+	 *
+	 * Sets the website for this repository
+	 *
+	 * @access public
+	 * @param string $site website
+	 */
+	public function SetWebsite($site)
+	{
+		$this->website = $site;
 	}
 
 /*}}}2*/
@@ -991,7 +1030,7 @@
 
 			if (!isset($this->commitCache[$hash])) {
 				$cacheKey = 'project|' . $this->project . '|commit|' . $hash;
-				$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+				$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($cacheKey);
 				if ($cached)
 					$this->commitCache[$hash] = $cached;
 				else
@@ -1335,7 +1374,7 @@
 			return;
 
 		$cacheKey = 'project|' . $this->project . '|tag|' . $tag;
-		$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+		$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($cacheKey);
 		if ($cached) {
 			return $cached;
 		} else {
@@ -1563,7 +1602,6 @@
 		if ($skip > 0) {
 			$log = array_slice($log, $skip, $count);
 		}
-		usort($log, array('GitPHP_Commit', 'CompareAge'));
 		return $log;
 	}
 
@@ -1585,7 +1623,7 @@
 			return null;
 
 		$cacheKey = 'project|' . $this->project . '|blob|' . $hash;
-		$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+		$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($cacheKey);
 		if ($cached)
 			return $cached;
 
@@ -1610,7 +1648,7 @@
 			return null;
 
 		$cacheKey = 'project|' . $this->project . '|tree|' . $hash;
-		$cached = GitPHP_Cache::GetInstance()->Get($cacheKey);
+		$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($cacheKey);
 		if ($cached)
 			return $cached;
 

--- a/include/git/ProjectList.class.php
+++ b/include/git/ProjectList.class.php
@@ -66,6 +66,7 @@
 		if (self::$instance)
 			return;
 
+
 		if (!empty($file) && is_file($file) && include($file)) {
 			if (isset($git_projects)) {
 				if (is_string($git_projects)) {
@@ -84,8 +85,10 @@
 			}
 		}
 
-		if (!self::$instance)
+		if (!self::$instance) {
+
 			self::$instance = new GitPHP_ProjectListDirectory(GitPHP_Config::GetInstance()->GetValue('projectroot'));
+		}
 
 		if (isset($git_projects_settings) && !$legacy)
 			self::$instance->ApplySettings($git_projects_settings);

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

--- a/include/git/ProjectListDirectory.class.php
+++ b/include/git/ProjectListDirectory.class.php
@@ -61,7 +61,25 @@
 	 */
 	protected function PopulateProjects()
 	{
+		$key = 'projectdir|' . $this->projectDir . '|projectlist|directory';
+		$cached = GitPHP_Cache::GetObjectCacheInstance()->Get($key);
+		if ($cached && (count($cached) > 0)) {
+			foreach ($cached as $proj) {
+				$this->AddProject($proj);
+			}
+			GitPHP_Log::GetInstance()->Log('Loaded ' . count($this->projects) . ' projects from cache');
+			return;
+		}
+
 		$this->RecurseDir($this->projectDir);
+
+		if (count($this->projects) > 0) {
+			$projects = array();
+			foreach ($this->projects as $proj) {
+				$projects[] = $proj->GetProject();;
+			}
+			GitPHP_Cache::GetObjectCacheInstance()->Set($key, $projects, GitPHP_Config::GetInstance()->GetValue('cachelifetime', 3600));
+		}
 	}
 
 	/**
@@ -73,7 +91,7 @@
 	 */
 	private function RecurseDir($dir)
 	{
-		if (!is_dir($dir))
+		if (!(is_dir($dir) && is_readable($dir)))
 			return;
 
 		GitPHP_Log::GetInstance()->Log(sprintf('Searching directory %1$s', $dir));
@@ -86,15 +104,7 @@
 					if (is_file($fullPath . '/HEAD')) {
 						GitPHP_Log::GetInstance()->Log(sprintf('Found project %1$s', $fullPath));
 						$projectPath = substr($fullPath, $trimlen);
-						try {
-							$proj = new GitPHP_Project($this->projectDir, $projectPath);
-							$proj->SetCategory(trim(substr($dir, strlen($this->projectDir)), '/'));
-							if ((!GitPHP_Config::GetInstance()->GetValue('exportedonly', false)) || $proj->GetDaemonEnabled()) {
-								$this->projects[$projectPath] = $proj;
-							}
-						} catch (Exception $e) {
-							GitPHP_Log::GetInstance()->Log($e->getMessage());
-						}
+						$this->AddProject($projectPath);
 					} else {
 						$this->RecurseDir($fullPath);
 					}
@@ -106,5 +116,28 @@
 		}
 	}
 
+	/**
+	 * AddProject
+	 *
+	 * Add project to collection
+	 *
+	 * @access private
+	 */
+	private function AddProject($projectPath)
+	{
+		try {
+			$proj = new GitPHP_Project($this->projectDir, $projectPath);
+			$category = trim(dirname($projectPath));
+			if (!(empty($category) || (strpos($category, '.') === 0))) {
+				$proj->SetCategory($category);
+			}
+			if ((!GitPHP_Config::GetInstance()->GetValue('exportedonly', false)) || $proj->GetDaemonEnabled()) {
+				$this->projects[$projectPath] = $proj;
+			}
+		} catch (Exception $e) {
+			GitPHP_Log::GetInstance()->Log($e->getMessage());
+		}
+	}
+
 }
 

--- a/include/git/Tag.class.php
+++ b/include/git/Tag.class.php
@@ -282,6 +282,22 @@
 	}
 
 	/**
+	 * GetAge
+	 *
+	 * Gets the tag age
+	 *
+	 * @access public
+	 * @return string age
+	 */
+	public function GetAge()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return time() - $this->taggerEpoch;
+	}
+
+	/**
 	 * GetComment
 	 *
 	 * Gets the tag comment
@@ -336,7 +352,7 @@
 			$this->ReadDataRaw();
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -359,7 +375,7 @@
 			$this->object = $this->GetProject()->GetCommit($this->GetHash());
 			$this->commit = $this->object;
 			$this->type = 'commit';
-			GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+			GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 			return;
 		}
 
@@ -431,6 +447,9 @@
 					}
 				}
 				break;
+			case 'blob':
+				$this->object = $this->GetProject()->GetBlob($objectHash);
+				break;
 		}
 	}
 
@@ -450,7 +469,7 @@
 			$this->object = $this->GetProject()->GetCommit($this->GetHash());
 			$this->commit = $this->object;
 			$this->type = 'commit';
-			GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+			GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 			return;
 		}
 
@@ -509,6 +528,9 @@
 					}
 				}
 				break;
+			case 'blob':
+				$this->object = $this->GetProject()->GetBlob($objectHash);
+				break;
 		}
 	}
 
@@ -538,7 +560,7 @@
 			}
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**
@@ -560,6 +582,8 @@
 			$this->object = $this->object->GetHash();
 		} else if ($this->type == 'tag') {
 			$this->object = $this->object->GetName();
+		} else if ($this->type == 'blob') {
+			$this->object = $this->object->GetHash();
 		}
 
 		$this->objectReferenced = true;
@@ -584,6 +608,8 @@
 			$this->object = $this->GetProject()->GetCommit($this->object);
 		} else if ($this->type == 'tag') {
 			$this->object = $this->GetProject()->GetTag($this->object);
+		} else if ($this->type == 'blob') {
+			$this->object = $this->GetProject()->GetBlob($this->object);
 		}
 
 		$this->objectReferenced = false;

--- a/include/git/Tree.class.php
+++ b/include/git/Tree.class.php
@@ -120,7 +120,7 @@
 			$this->ReadContentsRaw();
 		}
 
-		GitPHP_Cache::GetInstance()->Set($this->GetCacheKey(), $this);
+		GitPHP_Cache::GetObjectCacheInstance()->Set($this->GetCacheKey(), $this);
 	}
 
 	/**

file:a/index.php -> file:b/index.php
--- a/index.php
+++ b/index.php
@@ -25,6 +25,8 @@
 define('GITPHP_CONTROLLERDIR', GITPHP_INCLUDEDIR . 'controller/');
 define('GITPHP_CACHEDIR', GITPHP_INCLUDEDIR . 'cache/');
 define('GITPHP_LOCALEDIR', GITPHP_BASEDIR . 'locale/');
+
+define('GITPHP_CACHE', GITPHP_BASEDIR . 'cache/');
 
 include_once(GITPHP_INCLUDEDIR . 'version.php');
 

--- a/locale/gitphp.pot
+++ b/locale/gitphp.pot
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: GitPHP 0.2.4\n"
 "Report-Msgid-Bugs-To: xiphux@gmail.com\n"
-"POT-Creation-Date: 2011-07-22 23:42-0500\n"
+"POT-Creation-Date: 2011-08-25 20:54-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"
@@ -19,8 +19,10 @@
 "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
 
 # Used as link to and title for page displaying a blob, which is what git calls a single file
+#: templates/tag.tpl
 #: templates/blobdiff.tpl
 #: templates/commit.tpl
+#: templates/taglist.tpl
 #: templates/treelist.tpl
 #: templates/searchfiles.tpl
 #: templates/history.tpl
@@ -59,7 +61,7 @@
 #: templates/treelist.tpl
 #: templates/projectlist.tpl
 #: templates/shortloglist.tpl
-#: include/controller/Controller_Snapshot.class.php:85
+#: include/controller/Controller_Snapshot.class.php:92
 msgid "snapshot"
 msgstr ""
 
@@ -428,7 +430,7 @@
 #: include/controller/Controller_Commit.class.php:34
 #: include/controller/Controller_Log.class.php:34
 #: include/controller/Controller_Blame.class.php:34
-#: include/controller/Controller_Snapshot.class.php:43
+#: include/controller/Controller_Snapshot.class.php:49
 #: include/controller/Controller_Blob.class.php:34
 #: include/controller/Controller_Tag.class.php:34
 #: include/controller/Controller_Tags.class.php:34
@@ -454,6 +456,7 @@
 
 # Error message when user tries to access a project that doesn't exist
 # %1$s: the project the user tried to access
+#: include/controller/Controller_Snapshot.class.php:44
 #: include/controller/ControllerBase.class.php:93
 #, php-format
 msgid "Invalid project %1$s"
@@ -519,28 +522,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:250
+#: include/git/Project.class.php:326
 #, 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:254
+#: include/git/Project.class.php:330
 #, 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:258
+#: include/git/Project.class.php:334
 #, 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:264
+#: include/git/Project.class.php:340
 #, php-format
 msgid "%1$s is outside of the projectroot"
 msgstr ""
@@ -573,6 +576,7 @@
 
 # Error message when user tries to specify a file with a list of the projects, but it isn't a file
 # %1$s: the path the user specified
+#: include/git/ProjectListScmManager.class.php:37
 #: include/git/ProjectListFile.class.php:38
 #, php-format
 msgid "%1$s is not a file"
@@ -662,7 +666,7 @@
 
 # Error message when user hasn't defined a project root in the config
 # "projectroot" refers to a root directory where the user's git projects are stored
-#: index.php:123
+#: index.php:125
 msgid "A projectroot must be set in the config"
 msgstr ""
 
@@ -763,7 +767,7 @@
 # Error message displayed when the git executable isn't found or doesn't work
 # %1$s: the git executable the system is trying to run
 # %2$s: the config value the user needs to set to specify the correct path
-#: index.php:131
+#: index.php:133
 #, php-format
 msgid ""
 "Could not run the git executable \"%1$s\".  You may need to set the \"%2$s\" "
@@ -773,7 +777,7 @@
 # Error message displayed when the diff executable isn't found or doesn't work
 # %1$s: the diff executable the system is trying to run
 # %2$s: the config value the user needs to set to specify the correct path
-#: index.php:136
+#: index.php:138
 #, php-format
 msgid ""
 "Could not run the diff executable \"%1$s\".  You may need to set the \"%2$s"
@@ -795,3 +799,8 @@
 msgid "Binary files %1$s and %2$s differ"
 msgstr ""
 
+# Used to label the url of the website of the project
+#: templates/project.tpl
+msgid "website"
+msgstr ""
+

--- a/templates/blob.tpl
+++ b/templates/blob.tpl
@@ -16,8 +16,10 @@
    {else}
      {t}HEAD{/t}
    {/if}
+   {if $blob->GetPath()}
     | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=history&amp;h={$commit->GetHash()}&amp;f={$blob->GetPath()}">{t}history{/t}</a>
    {if !$datatag} | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blame&amp;h={$blob->GetHash()}&amp;f={$blob->GetPath()}&amp;hb={$commit->GetHash()}" id="blameLink">{t}blame{/t}</a>{/if}
+   {/if}
    <br />
  </div>
 

--- a/templates/commit.tpl
+++ b/templates/commit.tpl
@@ -125,7 +125,7 @@
        {elseif $diffline->GetStatus() == "M" || $diffline->GetStatus() == "T"}
          <td>
            {if $diffline->GetToHash() != $diffline->GetFromHash()}
-             <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blobdiff&amp;h={$diffline->GetToHash()}&amp;hp={$diffline->GetFromHash()}&amp;hb={$par->GetHash()}&amp;f={$diffline->GetToFile()}" class="list">
+             <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blobdiff&amp;h={$diffline->GetToHash()}&amp;hp={$diffline->GetFromHash()}&amp;hb={$commit->GetHash()}&amp;f={$diffline->GetToFile()}" class="list">
 	       {$diffline->GetToFile()}
 	     </a>
            {else}
@@ -178,7 +178,7 @@
          <td class="link">
            <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blob&amp;h={$diffline->GetToHash()}&amp;hb={$commit->GetHash()}&amp;f={$diffline->GetToFile()}">{t}blob{/t}</a>
 	   {if $diffline->GetToHash() != $diffline->GetFromHash()}
-	     | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blobdiff&amp;h={$diffline->GetToHash()}&amp;hp={$diffline->GetFromHash()}&amp;hb={$par->GetHash()}&amp;f={$diffline->GetToFile()}">{t}diff{/t}</a>
+	     | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blobdiff&amp;h={$diffline->GetToHash()}&amp;hp={$diffline->GetFromHash()}&amp;hb={$commit->GetHash()}&amp;f={$diffline->GetToFile()}">{t}diff{/t}</a>
 	   {/if}
 	     | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=history&amp;h={$commit->GetHash()}&amp;f={$diffline->GetFromFile()}">{t}history{/t}</a>
              | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blob_plain&amp;h={$diffline->GetToHash()}&amp;f={$diffline->GetToFile()}">{t}plain{/t}</a>
@@ -207,7 +207,7 @@
          <td class="link">
 	   <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blob&amp;h={$diffline->GetToHash()}&amp;hb={$commit->GetHash()}&amp;f={$diffline->GetToFile()}">{t}blob{/t}</a>
 	   {if $diffline->GetToHash() != $diffline->GetFromHash()}
-	     | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blobdiff&amp;h={$diffline->GetToHash()}&amp;hp={$diffline->GetFromHash()}&amp;hb={$par->GetHash()}&amp;f={$diffline->GetToFile()}">{t}diff{/t}</a>
+	     | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blobdiff&amp;h={$diffline->GetToHash()}&amp;hp={$diffline->GetFromHash()}&amp;hb={$commit->GetHash()}&amp;f={$diffline->GetToFile()}">{t}diff{/t}</a>
 	   {/if}
 	    | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blob_plain&amp;h={$diffline->GetToHash()}&amp;f={$diffline->GetToFile()}">{t}plain{/t}</a>
 	 </td>

--- a/templates/footer.tpl
+++ b/templates/footer.tpl
@@ -7,7 +7,13 @@
  *}
     <div class="page_footer">
       {if $project}
-        <div class="page_footer_text">{$project->GetDescription()}</div>
+        <div class="page_footer_text">
+	{if $project->GetWebsite()}
+	<a href="{$project->GetWebsite()}">{$project->GetDescription()}</a>
+	{else}
+	{$project->GetDescription()}
+	{/if}
+	</div>
         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=rss" class="rss_logo">{t}RSS{/t}</a>
         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=atom" class="rss_logo">{t}Atom{/t}</a>
       {else}

--- a/templates/header.tpl
+++ b/templates/header.tpl
@@ -16,8 +16,16 @@
       <link rel="alternate" title="{$project->GetProject()} log (Atom)" href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=atom" type="application/atom+xml" />
       <link rel="alternate" title="{$project->GetProject()} log (RSS)" href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=rss" type="application/rss+xml" />
     {/if}
+    {if file_exists('css/gitphp.min.css')}
+    <link rel="stylesheet" href="css/gitphp.min.css" type="text/css" />
+    {else}
     <link rel="stylesheet" href="css/gitphp.css" type="text/css" />
-    <link rel="stylesheet" href="css/{$stylesheet}" type="text/css" />
+    {/if}
+    {if file_exists("css/$stylesheet.min.css")}
+    <link rel="stylesheet" href="css/{$stylesheet}.min.css" type="text/css" />
+    {else}
+    <link rel="stylesheet" href="css/{$stylesheet}.css" type="text/css" />
+    {/if}
     {if $extracss}
     <style type="text/css">
     {$extracss}

--- a/templates/project.tpl
+++ b/templates/project.tpl
@@ -26,6 +26,9 @@
    {/if}
    {if $project->GetPushUrl()}
      <tr><td>{t}push url{/t}</td><td><a href="{$project->GetPushUrl()}">{$project->GetPushUrl()}</a></td></tr>
+   {/if}
+   {if $project->GetWebsite()}
+     <tr><td>{t}website{/t}</td><td><a href="{$project->GetWebsite()}">{$project->GetWebsite()}</a></td></tr>
    {/if}
  </table>
 

--- a/templates/tag.tpl
+++ b/templates/tag.tpl
@@ -16,7 +16,11 @@
  {assign var=object value=$tag->GetObject()}
  {assign var=objtype value=$tag->GetType()}
  <div class="title">
+   {if $objtype == 'blob'}
+   <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blob&amp;h={$object->GetHash()}" class="title">{$tag->GetName()}</a>
+   {else}
    <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$object->GetHash()}" class="title">{$tag->GetName()}</a>
+   {/if}
  </div>
  <div class="title_text">
    <table cellspacing="0">
@@ -28,6 +32,9 @@
        {elseif $objtype == 'tag'}
          <td class="monospace"><a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tag&amp;h={$object->GetName()}" class="list">{$object->GetHash()}</a></td>
          <td class="link"><a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tag&amp;h={$object->GetName()}">{t}tag{/t}</a></td>
+       {elseif $objtype == 'blob'}
+         <td class="monospace"><a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blob&amp;h={$object->GetHash()}" class="list">{$object->GetHash()}</a></td>
+         <td class="link"><a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blob&amp;h={$object->GetHash()}">{t}blob{/t}</a></td>
        {/if}
      </tr>
      {if $tag->GetTagger()}

--- a/templates/taglist.tpl
+++ b/templates/taglist.tpl
@@ -15,11 +15,13 @@
 	   {assign var=object value=$tag->GetObject()}
 	   {assign var=tagcommit value=$tag->GetCommit()}
 	   {assign var=objtype value=$tag->GetType()}
-           <td><em>{$tagcommit->GetAge()|agestring}</em></td>
+           <td><em>{if $tagcommit}{$tagcommit->GetAge()|agestring}{else}{$tag->GetAge()|agestring}{/if}</em></td>
            <td>
 	   {if $objtype == 'commit'}
 		   <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$object->GetHash()}" class="list"><strong>{$tag->GetName()}</strong></a>
 	   {elseif $objtype == 'tag'}
+		   <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tag&amp;h={$tag->GetName()}" class="list"><strong>{$tag->GetName()}</strong></a>
+	   {elseif $objtype == 'blob'}
 		   <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tag&amp;h={$tag->GetName()}" class="list"><strong>{$tag->GetName()}</strong></a>
 	   {/if}
 	   </td>
@@ -33,8 +35,12 @@
              {if !$tag->LightTag()}
    	       <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tag&amp;h={$tag->GetName()}">{t}tag{/t}</a> | 
              {/if}
+	     {if $objtype == 'blob'}
+		<a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=blob&amp;h={$object->GetHash()}">{t}blob{/t}</a>
+	     {else}
              <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$tagcommit->GetHash()}">{t}commit{/t}</a>
 	      | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=shortlog&amp;h={$tagcommit->GetHash()}">{t}shortlog{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=log&amp;h={$tagcommit->GetHash()}">{t}log{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=snapshot&amp;h={$tagcommit->GetHash()}" class="snapshotTip">{t}snapshot{/t}</a>
+	      {/if}
            </td>
        </tr>
      {/foreach}

--- a/util/minify.sh
+++ b/util/minify.sh
@@ -17,6 +17,10 @@
 JSEXT=".js"
 MINEXT=".min.js"
 
+CSSDIR="css"
+CSSEXT=".css"
+MINCSSEXT=".min.css"
+
 rm -f ${JSDIR}/*${MINEXT}
 
 for i in ${JSDIR}/*${JSEXT}; do
@@ -24,3 +28,10 @@
 	java -jar "${COMPRESSORDIR}/${COMPRESSORJAR}" --charset utf-8 -o "${i%$JSEXT}${MINEXT}" "${i}"
 done
 
+rm -f ${CSSDIR}/*${MINCSSEXT}
+
+for i in ${CSSDIR}/*${CSSEXT}; do
+	echo "Minifying ${i}..."
+	java -jar "${COMPRESSORDIR}/${COMPRESSORJAR}" --charset utf-8 -o "${i%$CSSEXT}${MINCSSEXT}" "${i}"
+done
+

comments