From ba6ae959b8e21c714c69f66254e82837d45a3ed2 Mon Sep 17 00:00:00 2001
From: mschaefers <mschaefers@scoop-gmbh.de>
Date: Mon, 03 Dec 2012 03:49:23 -0500
Subject: [PATCH] Merge branch 'master' of https://github.com/gitblit/gitblit into enhancedLdap

---
 NOTICE                                                      |   18 +
 .classpath                                                  |    2 
 src/com/gitblit/utils/X509Utils.java                        |   68 +++
 tests/com/gitblit/tests/X509UtilsTest.java                  |    7 
 src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html |   12 
 src/com/gitblit/authority/GitblitAuthority.java             |   21 +
 src/com/gitblit/wicket/pages/RepositoryPage.java            |    2 
 src/com/gitblit/GitBlit.java                                |   50 ++
 src/com/gitblit/wicket/pages/CommitPage.java                |   10 
 docs/04_releases.mkd                                        |    6 
 tests/com/gitblit/tests/JGitUtilsTest.java                  |    7 
 docs/04_design.mkd                                          |    2 
 src/com/gitblit/Constants.java                              |   10 
 src/com/gitblit/utils/JGitUtils.java                        |   73 ----
 src/com/gitblit/utils/CompressionUtils.java                 |  315 ++++++++++++++++++
 src/com/gitblit/DownloadZipServlet.java                     |   58 +++
 src/com/gitblit/wicket/pages/TreePage.java                  |   23 
 src/com/gitblit/wicket/GitBlitWebSession.java               |    4 
 src/com/gitblit/wicket/pages/TreePage.html                  |    6 
 distrib/gitblit.properties                                  |  105 +++--
 src/com/gitblit/wicket/pages/CommitPage.html                |    2 
 src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java |   77 ++++
 src/com/gitblit/build/Build.java                            |   20 +
 src/com/gitblit/wicket/panels/LogPanel.html                 |    2 
 resources/script_16x16.png                                  |    0 
 src/com/gitblit/utils/HttpUtils.java                        |   42 -
 src/com/gitblit/wicket/panels/HistoryPanel.java             |    2 
 src/com/gitblit/wicket/pages/BasePage.java                  |   38 +-
 src/com/gitblit/wicket/panels/BranchesPanel.java            |   21 +
 build.xml                                                   |    7 
 src/com/gitblit/wicket/panels/LogPanel.java                 |    2 
 src/com/gitblit/wicket/panels/HistoryPanel.html             |    2 
 resources/gitblit.css                                       |    6 
 33 files changed, 795 insertions(+), 225 deletions(-)

diff --git a/.classpath b/.classpath
index 772d0e6..55018bf 100644
--- a/.classpath
+++ b/.classpath
@@ -34,6 +34,8 @@
 	<classpathentry kind="lib" path="ext/unboundid-ldapsdk-2.3.0.jar" sourcepath="ext/src/unboundid-ldapsdk-2.3.0-sources.jar" />
 	<classpathentry kind="lib" path="ext/ivy-2.2.0.jar" sourcepath="ext/src/ivy-2.2.0-sources.jar" />
 	<classpathentry kind="lib" path="ext/jcalendar-1.3.2.jar" />
+	<classpathentry kind="lib" path="ext/commons-compress-1.4.1.jar" sourcepath="ext/src/commons-compress-1.4.1-sources.jar" />
+	<classpathentry kind="lib" path="ext/xz-1.0.jar" sourcepath="ext/src/xz-1.0-sources.jar" />
 	<classpathentry kind="lib" path="ext/junit-4.10.jar" sourcepath="ext/src/junit-4.10-sources.jar" />
 	<classpathentry kind="lib" path="ext/hamcrest-core-1.1.jar" />
 	<classpathentry kind="output" path="bin/classes" />
diff --git a/NOTICE b/NOTICE
index 6c2e253..4daa120 100644
--- a/NOTICE
+++ b/NOTICE
@@ -238,4 +238,20 @@
    GNU LESSER GENERAL PUBLIC LICENSE. (http://www.unboundid.com/products/ldap-sdk/docs/LICENSE-LGPLv2.1.txt)
 
    http://www.toedter.com/en/jcalendar
-   
\ No newline at end of file
+   
+---------------------------------------------------------------------------
+Commons-Compress
+---------------------------------------------------------------------------
+   Commons-Compress, released under the
+   Apache Software License, Version 2.0.
+
+   http://commons.apache.org/compress
+
+---------------------------------------------------------------------------
+XZ for Java
+---------------------------------------------------------------------------
+   XZ for Java, released under the
+   Public Domain
+
+   http://tukaani.org/xz/java.html
+
diff --git a/build.xml b/build.xml
index 59d79b5..07fd0ca 100644
--- a/build.xml
+++ b/build.xml
@@ -95,6 +95,7 @@
 		<property name="authority.zipfile" value="authority-${gb.version}.zip" />
 		<property name="gbapi.zipfile" value="gbapi-${gb.version}.zip" />
 		<property name="express.zipfile" value="express-${gb.version}.zip" />
+		<property name="distribution.pomfileTmplt" value="tmplt.pom.xml" />
 	</target>
 	
 	
@@ -495,6 +496,11 @@
 				<exclude name="com/gitblit/Launcher*.class" />
 			</fileset>
 		</copy>
+		<copy todir="${project.jar.dir}/static">
+			<fileset dir="${project.resources.dir}">
+				<exclude name="thumbs.db" />
+			</fileset>
+		</copy>
 
 		<!-- Build the JAR file -->
 		<jar basedir="${project.jar.dir}" destfile="${distribution.jarfile}" compress="true" />
@@ -767,6 +773,7 @@
 			<resource file="${basedir}/resources/settings_32x32.png" />
 			<resource file="${basedir}/resources/search-icon.png" />
 			<resource file="${basedir}/resources/mail_16x16.png" />
+			<resource file="${basedir}/resources/script_16x16.png" />
 			<resource file="${basedir}/resources/blank.png" />
 			<resource file="${basedir}/resources/bullet_green.png" />
 			<resource file="${basedir}/resources/bullet_orange.png" />
diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index 3419e00..a1ec305 100644
--- a/distrib/gitblit.properties
+++ b/distrib/gitblit.properties
@@ -93,7 +93,7 @@
 
 # Only serve/display bare repositories.
 # If there are non-bare repositories in git.repositoriesFolder and this setting
-# is true, they will be excluded from the ui.
+# is true, they will be excluded from the ui. 
 #
 # SINCE 0.9.0
 git.onlyAccessBareRepositories = false
@@ -172,7 +172,7 @@
 
 # The default period, in days, between GCs for a repository.  If the total filesize
 # of the loose object exceeds *git.garbageCollectionThreshold* or the repository's
-# custom threshold, this period will be short-circuited.
+# custom threshold, this period will be short-circuited. 
 #
 # e.g. if a repository collects 100KB of loose objects every day with a 500KB
 # threshold and a period of 7 days, it will take 5 days for the loose objects to
@@ -261,7 +261,7 @@
 # typically has to deal with ~10-12 MiB XML files, so 15 m would be a reasonable
 # setting in that environment. Setting this too high may cause the JVM to run out
 # of heap space when handling very big binary files, such as device firmware or
-# CD-ROM ISO images. Make sure to adjust your JVM heap accordingly.
+# CD-ROM ISO images. Make sure to adjust your JVM heap accordingly. 
 #
 # Default is 50 MiB on all platforms.
 #
@@ -278,7 +278,7 @@
 # a call to munmap() can be made by the JVM native code.
 #
 # In server applications (such as Gitblit) that need to access many pack files,
-# setting this to true risks artificially running out of virtual address space,
+# setting this to true risks artificially running out of virtual address space, 
 # as the garbage collector cannot reclaim unused mapped spaces fast enough.
 #
 # Default on JGit is false. Although potentially slower, it yields much more
@@ -316,7 +316,7 @@
 # to change.
 #
 # Script names are case-sensitive on case-sensitive file systems.  You may omit
-# the traditional ".groovy" from this list if your file extension is ".groovy"
+# the traditional ".groovy" from this list if your file extension is ".groovy" 
 #
 # NOTE:
 # These scripts are only executed when pushing to *Gitblit*, not to other Git
@@ -336,7 +336,7 @@
 # You might trigger a continuous-integration build here or send a notification.
 #
 # Script names are case-sensitive on case-sensitive file systems.  You may omit
-# the traditional ".groovy" from this list if your file extension is ".groovy"
+# the traditional ".groovy" from this list if your file extension is ".groovy" 
 #
 # NOTE:
 # These scripts are only executed when pushing to *Gitblit*, not to other Git
@@ -344,7 +344,7 @@
 # repositories. These are NOT repository-specific scripts!  Within the script
 # you may customize the control-flow for a specific repository by checking the
 # *repository* variable.
-#
+# 
 # SPACE-DELIMITED
 # CASE-SENSITIVE
 # SINCE 0.8.0
@@ -353,7 +353,7 @@
 # Repository custom fields for Groovy Hook mechanism
 #
 # List of key=label pairs of custom fields to prompt for in the Edit Repository
-# page.  These keys are stored in the repository's git config file in the
+# page.  These keys are stored in the repository's git config file in the 
 # section [gitblit "customFields"].  Key names are alphanumeric only.  These
 # fields are intended to be used for the Groovy hook mechanism where a script
 # can adjust it's execution based on the custom fields stored in the repository
@@ -363,7 +363,7 @@
 #
 # SPACE-DELIMITED
 # SINCE 1.0.0
-groovy.customFields =
+groovy.customFields = 
 
 #
 # Authentication Settings
@@ -411,13 +411,13 @@
 # combined-md5 is the hash of username.toLowerCase()+password.
 # Default is md5.
 #
-# SINCE 0.5.0
+# SINCE 0.5.0 
 realm.passwordStorage = md5
 
 # Minimum valid length for a plain text password.
 # Default value is 5.  Absolute minimum is 4.
 #
-# SINCE 0.5.0
+# SINCE 0.5.0 
 realm.minPasswordLength = 5
 
 #
@@ -432,30 +432,30 @@
 # repositories, create users, and edit repository metadata.
 #
 # If *web.authenticateAdminPages*=false, any user can execute the aforementioned
-# functions.
+# functions. 
 #
-# SINCE 0.5.0
+# SINCE 0.5.0 
 web.allowAdministration = true
 
-# Allows rpc clients to list repositories and possibly manage or administer the
+# Allows rpc clients to list repositories and possibly manage or administer the 
 # Gitblit server, if the authenticated account has administrator permissions.
 # See *web.enableRpcManagement* and *web.enableRpcAdministration*.
 #
-# SINCE 0.7.0
+# SINCE 0.7.0 
 web.enableRpcServlet = true
 
 # Allows rpc clients to manage repositories and users of the Gitblit instance,
 # if the authenticated account has administrator permissions.
 # Requires *web.enableRpcServlet=true*.
 #
-# SINCE 0.7.0
+# SINCE 0.7.0 
 web.enableRpcManagement = false
 
 # Allows rpc clients to control the server settings and monitor the health of this
 # this Gitblit instance, if the authenticated account has administrator permissions.
 # Requires *web.enableRpcServlet=true* and *web.enableRpcManagement*.
 #
-# SINCE 0.7.0
+# SINCE 0.7.0 
 web.enableRpcAdministration = false
 
 # Full path to a configurable robots.txt file.  With this file you can control
@@ -463,7 +463,7 @@
 # http://googlewebmastercentral.blogspot.com/2008/06/improving-on-robots-exclusion-protocol.html
 #
 # SINCE 1.0.0
-web.robots.txt =
+web.robots.txt = 
 
 # If true, the web ui layout will respond and adapt to the browser's dimensions.
 # if false, the web ui will use a 940px fixed-width layout.
@@ -479,8 +479,21 @@
 
 # Allow dynamic zip downloads.
 #
-# SINCE 0.5.0
+# SINCE 0.5.0   
 web.allowZipDownloads = true
+
+# If *web.allowZipDownloads=true* the following formats will be displayed for
+# download compressed archive links:
+#
+# zip   = standard .zip
+# tar   = standard tar format (preserves *nix permissions and symlinks)
+# gz    = gz-compressed tar
+# xz    = xz-compressed tar
+# bzip2 = bzip2-compressed tar
+#
+# SPACE-DELIMITED
+# SINCE 1.2.0
+web.compressedDownloads = zip gz
 
 # Allow optional Lucene integration. Lucene indexing is an opt-in feature.
 # A repository may specify branches to index with Lucene instead of using Git
@@ -510,7 +523,7 @@
 
 # Show the size of each repository on the repositories page.
 # This requires recursive traversal of each repository folder.  This may be
-# non-performant on some operating systems and/or filesystems.
+# non-performant on some operating systems and/or filesystems. 
 #
 # SINCE 0.5.2
 web.showRepositorySizes = true
@@ -519,13 +532,13 @@
 # of the Repositories and Activity pages.  Keep them very simple because you
 # are likely to run into encoding issues if they are too complex.
 #
-# Use !!! to separate the filters
+# Use !!! to separate the filters 
 #
 # SINCE 0.8.0
 web.customFilters =
 
 # Show federation registrations (without token) and the current pull status
-# to non-administrator users.
+# to non-administrator users. 
 #
 # SINCE 0.6.0
 web.showFederationRegistrations = false
@@ -549,12 +562,12 @@
 # is always appended to the encoding list.  If all encodings fail to cleanly
 # decode the blob content, UTF-8 will be used with the standard malformed
 # input/unmappable character replacement strings.
-#
+# 
 # SPACE-DELIMITED
 # SINCE 1.0.0
 web.blobEncodings = UTF-8 ISO-8859-1
 
-# Manually set the default timezone to be used by Gitblit for display in the
+# Manually set the default timezone to be used by Gitblit for display in the 
 # web ui.  This value is independent of the JVM timezone.  Specifying a blank
 # value will default to the JVM timezone.
 # e.g. America/New_York, US/Pacific, UTC, Europe/Berlin
@@ -626,7 +639,7 @@
 #
 # SPACE-DELIMITED
 # SINCE 0.5.0
-web.otherUrls =
+web.otherUrls = 
 
 # Choose how to present the repositories list.
 #   grouped = group nested/subfolder repositories together (no sorting)
@@ -642,8 +655,8 @@
 # SINCE 0.5.0
 web.repositoryRootGroupName = main
 
-# Display the repository swatch color next to the repository name link in the
-# repositories list.
+# Display the repository swatch color next to the repository name link in the 
+# repositories list. 
 #
 # SINCE 0.8.0
 web.repositoryListSwatches = true
@@ -667,7 +680,7 @@
 # Generates a line graph of repository activity over time on the Summary page.
 # This uses the Google Charts API.
 #
-# SINCE 0.5.0
+# SINCE 0.5.0 
 web.generateActivityGraph = true
 
 # The number of days to show on the activity page.
@@ -719,7 +732,7 @@
 #
 # SPACE-DELIMITED
 # SINCE 0.5.0
-web.imageExtensions = bmp jpg gif png
+web.imageExtensions = bmp jpg gif png 
 
 # Registered extensions for binary blobs
 #
@@ -728,7 +741,7 @@
 web.binaryExtensions = jar pdf tar.gz zip
 
 # Aggressive heap management will run the garbage collector on every generated
-# page.  This slows down page generation a little but improves heap consumption.
+# page.  This slows down page generation a little but improves heap consumption. 
 #
 # SINCE 0.5.0
 web.aggressiveHeapManagement = false
@@ -788,13 +801,13 @@
 # from address for generated emails
 #
 # SINCE 0.6.0
-mail.fromAddress =
+mail.fromAddress = 
 
 # List of email addresses for the Gitblit administrators
 #
 # SPACE-DELIMITED
 # SINCE 0.6.0
-mail.adminAddresses =
+mail.adminAddresses = 
 
 # List of email addresses for sending push email notifications.
 #
@@ -858,7 +871,7 @@
 # SINCE 0.6.0
 federation.defaultFrequency = 60 mins
 
-# Federation Sets are named groups of repositories.  The Federation Sets are
+# Federation Sets are named groups of repositories.  The Federation Sets are 
 # available for selection in the repository settings page.  You can assign a
 # repository to one or more sets and then distribute the token for the set.
 # This allows you to grant federation pull access to a subset of your available
@@ -867,7 +880,7 @@
 # SPACE-DELIMITED
 # CASE-SENSITIVE
 # SINCE 0.6.0
-federation.sets =
+federation.sets = 
 
 # Federation pull registrations
 # Registrations are read once, at startup.
@@ -899,8 +912,8 @@
 #   clone from the origin until pushed to or otherwise manipulated.
 #
 # mergeAccounts:
-#   if true, remote accounts and their permissions are merged into your
-#   users.properties file
+#   if true, remote accounts and their permissions are merged into your 
+#   users.properties file 
 #
 # notifyOnError:
 #   if true and the mail configuration is properly set, administrators will be
@@ -919,8 +932,8 @@
 #federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4
 #federation.example1.frequency = 120 mins
 #federation.example1.folder =
-#federation.example1.bare = true
-#federation.example1.mirror = true
+#federation.example1.bare = true 
+#federation.example1.mirror = true 
 #federation.example1.mergeAccounts = true
 
 #
@@ -936,7 +949,7 @@
 
 # Login username for LDAP searches.
 # If this value is unspecified, anonymous LDAP login will be used.
-#
+# 
 # e.g. mydomain\\username
 #
 # SINCE 1.0.0
@@ -976,7 +989,7 @@
 
 # Filter criteria for LDAP users
 #
-# Query pattern to use when searching for a user account. This may be any valid
+# Query pattern to use when searching for a user account. This may be any valid 
 # LDAP query expression, including the standard (&) and (|) operators.
 #
 # Variables may be injected via the ${variableName} syntax.
@@ -989,14 +1002,14 @@
 # Root node for all LDAP groups to be used as Gitblit Teams
 #
 # This is the root node from which subtree team searches will begin.
-# If blank, Gitblit will search ALL nodes.
+# If blank, Gitblit will search ALL nodes.  
 #
 # SINCE 1.0.0
 realm.ldap.groupBase = OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
 
 # Filter criteria for LDAP groups
 #
-# Query pattern to use when searching for a team. This may be any valid
+# Query pattern to use when searching for a team. This may be any valid 
 # LDAP query expression, including the standard (&) and (|) operators.
 #
 # Variables may be injected via the ${variableName} syntax.
@@ -1005,7 +1018,7 @@
 #    ${dn} - The Distinguished Name of the user logged in
 #
 # All attributes from the LDAP User record are available. For example, if a user
-# has an attribute "fullName" set to "John", "(fn=${fullName})" will be
+# has an attribute "fullName" set to "John", "(fn=${fullName})" will be 
 # translated to "(fn=John)".
 #
 # SINCE 1.0.0
@@ -1027,7 +1040,7 @@
 #
 # This may be a single attribute, or a string of multiple attributes.  Examples:
 #  displayName - Uses the attribute 'displayName' on the user record
-#  ${personalTitle}. ${givenName} ${surname} - Will concatenate the 3
+#  ${personalTitle}. ${givenName} ${surname} - Will concatenate the 3 
 #       attributes together, with a '.' after personalTitle
 #
 # SINCE 1.0.0
@@ -1039,7 +1052,7 @@
 # This may be a single attribute, or a string of multiple attributes.  Examples:
 #  email - Uses the attribute 'email' on the user record
 #  ${givenName}.${surname}@gitblit.com -Will concatenate the 2 attributes
-#       together with a '.' and '@' creating something like first.last@gitblit.com
+#       together with a '.' and '@' creating something like first.last@gitblit.com 
 #
 # SINCE 1.0.0
 realm.ldap.email = email
@@ -1078,7 +1091,7 @@
 # Server Settings
 #
 
-# The temporary folder to decompress the embedded gitblit webapp.
+# The temporary folder to decompress the embedded gitblit webapp. 
 #
 # SINCE 0.5.0
 # RESTART REQUIRED
diff --git a/docs/04_design.mkd b/docs/04_design.mkd
index c2a7359..622e4a2 100644
--- a/docs/04_design.mkd
+++ b/docs/04_design.mkd
@@ -43,6 +43,8 @@
 - [UnboundID](http://www.unboundid.com) (LGPL 2.1)
 - [Ivy](http://ant.apache.org/ivy) (Apache 2.0)
 - [JCalendar](http://www.toedter.com/en/jcalendar) (LGPL 2.1)
+- [Commons-Compress](http://commons.apache.org/compress) (Apache 2.0)
+- [XZ for Java](http://tukaani.org/xz/java.html) (Public Domain)
 
 ### Other Build Dependencies
 - [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)
diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd
index 9704de0..3f03160 100644
--- a/docs/04_releases.mkd
+++ b/docs/04_releases.mkd
@@ -60,6 +60,8 @@
 - Added Gitblit Certificate Authority, an X509 certificate generation tool for Gitblit GO to encourage use of client certificate authentication.
 - Added setting to control length of shortened commit ids  
     **New:** *web.shortCommitIdLength=8*  
+- Added alternate compressed download formats: tar.gz, tar.xz, tar.bzip2 (issue 174)  
+    **New:** *web.compressedDownloads = zip gz*
 - Added simple project pages.  A project is a subfolder off the *git.repositoriesFolder*.
 - Added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)
 - Delete branch feature (issue 121, Github/ajermakovics)
@@ -70,6 +72,7 @@
 
 #### changes
 
+- Access restricted servlets (e.g. DownloadZip, RSS, etc) will try to authenticate any Gitblit cookie found in the request before resorting to BASIC authentication.
 - Added *groovy* and *scala* to *web.prettyPrintExtensions*
 - Added short commit id column to log and history tables (issue 168)
 - Teams can now specify the *admin*, *create*, and *fork* roles to simplify user administration
@@ -90,7 +93,8 @@
 - updated to Lucene 3.6.1
 - updated to BouncyCastle 1.47
 - added JCalendar 1.3.2
-
+- added Commons-Compress 1.4.1
+- added XZ for Java 1.0
 <hr/>
 
 ### Older Releases
diff --git a/resources/gitblit.css b/resources/gitblit.css
index e5363c8..811b08a 100644
--- a/resources/gitblit.css
+++ b/resources/gitblit.css
@@ -984,11 +984,15 @@
 	white-space: nowrap;
 }
 
-span.sha1, span.sha1 a, span.sha1 a span, .commit_message {
+span.sha1, span.sha1 a, span.sha1 a span, .commit_message, span.shortsha1 {
 	font-family: consolas, monospace;
 	font-size: 13px;
 }
 
+span.shortsha1 {
+	font-size: 12px;
+}
+
 td.mode {
 	text-align: right;
 	font-family: monospace;
diff --git a/resources/script_16x16.png b/resources/script_16x16.png
new file mode 100644
index 0000000..ec93a22
--- /dev/null
+++ b/resources/script_16x16.png
Binary files differ
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index cd6b070..4669c4c 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -397,7 +397,15 @@
 			return ordinal() > s.ordinal();
 		}
 	}
-	
+
+	public static enum AuthenticationType {
+		CREDENTIALS, COOKIE, CERTIFICATE;
+		
+		public boolean isStandard() {
+			return ordinal() <= COOKIE.ordinal();
+		}
+	}
+
 	@Documented
 	@Retention(RetentionPolicy.RUNTIME)
 	public @interface Unused {
diff --git a/src/com/gitblit/DownloadZipServlet.java b/src/com/gitblit/DownloadZipServlet.java
index 2655934..0feee87 100644
--- a/src/com/gitblit/DownloadZipServlet.java
+++ b/src/com/gitblit/DownloadZipServlet.java
@@ -29,6 +29,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.utils.CompressionUtils;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.MarkdownUtils;
 import com.gitblit.utils.StringUtils;
@@ -45,6 +46,25 @@
 	private static final long serialVersionUID = 1L;
 
 	private transient Logger logger = LoggerFactory.getLogger(DownloadZipServlet.class);
+	
+	public static enum Format {
+		zip(".zip"), tar(".tar"), gz(".tar.gz"), xz(".tar.xz"), bzip2(".tar.bzip2");
+		
+		public final String extension;
+		
+		Format(String ext) {
+			this.extension = ext;
+		}
+		
+		public static Format fromName(String name) {
+			for (Format format : values()) {
+				if (format.name().equalsIgnoreCase(name)) {
+					return format;
+				}
+			}
+			return zip;
+		}
+	}
 
 	public DownloadZipServlet() {
 		super();
@@ -57,15 +77,17 @@
 	 * @param repository
 	 * @param objectId
 	 * @param path
+	 * @param format
 	 * @return an url
 	 */
-	public static String asLink(String baseURL, String repository, String objectId, String path) {
+	public static String asLink(String baseURL, String repository, String objectId, String path, Format format) {
 		if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
 			baseURL = baseURL.substring(0, baseURL.length() - 1);
 		}
 		return baseURL + Constants.ZIP_PATH + "?r=" + repository
 				+ (path == null ? "" : ("&p=" + path))
-				+ (objectId == null ? "" : ("&h=" + objectId));
+				+ (objectId == null ? "" : ("&h=" + objectId))
+				+ (format == null ? "" : ("&format=" + format.name()));
 	}
 
 	/**
@@ -84,16 +106,22 @@
 			response.sendError(HttpServletResponse.SC_FORBIDDEN);
 			return;
 		}
-
+		
+		Format format = Format.zip;
 		String repository = request.getParameter("r");
 		String basePath = request.getParameter("p");
 		String objectId = request.getParameter("h");
-
+		String f = request.getParameter("format");
+		if (!StringUtils.isEmpty(f)) {
+			format = Format.fromName(f);
+		}
+		
 		try {
 			String name = repository;
 			if (name.indexOf('/') > -1) {
 				name = name.substring(name.lastIndexOf('/') + 1);
 			}
+			name = StringUtils.stripDotGit(name);
 
 			if (!StringUtils.isEmpty(basePath)) {
 				name += "-" + basePath.replace('/', '_');
@@ -122,15 +150,31 @@
 
 			String contentType = "application/octet-stream";
 			response.setContentType(contentType + "; charset=" + response.getCharacterEncoding());
-			response.setHeader("Content-Disposition", "attachment; filename=\"" + name + ".zip"
-					+ "\"");
+			response.setHeader("Content-Disposition", "attachment; filename=\"" + name + format.extension + "\"");
 			response.setDateHeader("Last-Modified", date.getTime());
 			response.setHeader("Cache-Control", "no-cache");
 			response.setHeader("Pragma", "no-cache");
 			response.setDateHeader("Expires", 0);
 
 			try {
-				JGitUtils.zip(r, basePath, objectId, response.getOutputStream());
+				switch (format) {
+				case zip:
+					CompressionUtils.zip(r, basePath, objectId, response.getOutputStream());
+					break;
+				case tar:
+					CompressionUtils.tar(r, basePath, objectId, response.getOutputStream());
+					break;
+				case gz:
+					CompressionUtils.gz(r, basePath, objectId, response.getOutputStream());
+					break;
+				case xz:
+					CompressionUtils.xz(r, basePath, objectId, response.getOutputStream());
+					break;
+				case bzip2:
+					CompressionUtils.bzip2(r, basePath, objectId, response.getOutputStream());
+					break;
+				}
+				
 				response.flushBuffer();
 			} catch (Throwable t) {
 				logger.error("Failed to write attachment to client", t);
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index c05a924..69135c4 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -58,6 +58,7 @@
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 
+import org.apache.wicket.RequestCycle;
 import org.apache.wicket.protocol.http.WebResponse;
 import org.apache.wicket.resource.ContextRelativeResource;
 import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
@@ -75,6 +76,7 @@
 
 import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthenticationType;
 import com.gitblit.Constants.AuthorizationControl;
 import com.gitblit.Constants.FederationRequest;
 import com.gitblit.Constants.FederationStrategy;
@@ -107,6 +109,8 @@
 import com.gitblit.utils.ObjectCache;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.TimeUtils;
+import com.gitblit.utils.X509Utils.X509Metadata;
+import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.WicketUtils;
 
 /**
@@ -537,7 +541,7 @@
 	 * @param cookies
 	 * @return a user object or null
 	 */
-	public UserModel authenticate(Cookie[] cookies) {
+	protected UserModel authenticate(Cookie[] cookies) {
 		if (userService == null) {
 			return null;
 		}
@@ -555,21 +559,51 @@
 	}
 
 	/**
-	 * Authenticate a user based on HTTP request paramters.
-	 * This method is inteded to be used as fallback when other
-	 * means of authentication are failing (username / password or cookies).
+	 * Authenticate a user based on HTTP request parameters.
+	 * 
+	 * Authentication by X509Certificate is tried first and then by cookie.
+	 * 
 	 * @param httpRequest
 	 * @return a user object or null
 	 */
 	public UserModel authenticate(HttpServletRequest httpRequest) {
+		// try to authenticate by certificate
 		boolean checkValidity = settings.getBoolean(Keys.git.enforceCertificateValidity, true);
 		String [] oids = getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]);
 		UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids);
 		if (model != null) {
-			UserModel user = GitBlit.self().getUserModel(model.username);
-			logger.info(MessageFormat.format("{0} authenticated by client certificate from {1}",
-					user.username, httpRequest.getRemoteAddr()));
-			return user;
+			// grab real user model and preserve certificate serial number
+			UserModel user = getUserModel(model.username);
+			if (user != null) {
+				RequestCycle requestCycle = RequestCycle.get();
+				if (requestCycle != null) {
+					// flag the Wicket session, if this is a Wicket request
+					GitBlitWebSession session = GitBlitWebSession.get();
+					session.authenticationType = AuthenticationType.CERTIFICATE;
+				}
+				X509Metadata metadata = HttpUtils.getCertificateMetadata(httpRequest);
+				logger.info(MessageFormat.format("{0} authenticated by client certificate {1} from {2}",
+						user.username, metadata.serialNumber, httpRequest.getRemoteAddr()));
+				return user;
+			}
+		}
+		
+		// try to authenticate by cookie
+		Cookie[] cookies = httpRequest.getCookies();
+		if (allowCookieAuthentication() && cookies != null && cookies.length > 0) {
+			// Grab cookie from Browser Session
+			UserModel user = authenticate(cookies);
+			if (user != null) {
+				RequestCycle requestCycle = RequestCycle.get();
+				if (requestCycle != null) {
+					// flag the Wicket session, if this is a Wicket request
+					GitBlitWebSession session = GitBlitWebSession.get();
+					session.authenticationType = AuthenticationType.COOKIE;
+				}
+				logger.info(MessageFormat.format("{0} authenticated by cookie from {1}",
+						user.username, httpRequest.getRemoteAddr()));
+				return user;
+			}
 		}
 		return null;
 	}
diff --git a/src/com/gitblit/authority/GitblitAuthority.java b/src/com/gitblit/authority/GitblitAuthority.java
index 5ee6af5..d97a8e3 100644
--- a/src/com/gitblit/authority/GitblitAuthority.java
+++ b/src/com/gitblit/authority/GitblitAuthority.java
@@ -67,6 +67,7 @@
 import javax.swing.JScrollPane;
 import javax.swing.JSplitPane;
 import javax.swing.JTable;
+import javax.swing.JTextArea;
 import javax.swing.JTextField;
 import javax.swing.RowFilter;
 import javax.swing.SwingConstants;
@@ -92,6 +93,7 @@
 import com.gitblit.client.Translation;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.FileUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.TimeUtils;
 import com.gitblit.utils.X509Utils;
@@ -343,6 +345,7 @@
 
 		X509Metadata metadata = new X509Metadata("localhost", caKeystorePassword);
 		setMetadataDefaults(metadata);
+		metadata.notAfter = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR);
 		X509Utils.prepareX509Infrastructure(metadata, folder, this);
 		return true;
 	}
@@ -681,6 +684,23 @@
 			}
 		});
 		
+		JButton logButton = new JButton(new ImageIcon(getClass().getResource("/script_16x16.png")));
+		logButton.setFocusable(false);
+		logButton.setToolTipText(Translation.get("gb.log"));		
+		logButton.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				File log = new File(folder, X509Utils.CERTS + File.separator + "log.txt");
+				if (log.exists()) {
+					String content = FileUtils.readContent(log,  "\n");
+					JTextArea textarea = new JTextArea(content);
+					JScrollPane scrollPane = new JScrollPane(textarea);
+					scrollPane.setPreferredSize(new Dimension(700, 400));
+					JOptionPane.showMessageDialog(GitblitAuthority.this, scrollPane, log.getAbsolutePath(), JOptionPane.INFORMATION_MESSAGE);
+				}
+			}
+		});
+		
 		final JTextField filterTextfield = new JTextField(15);
 		filterTextfield.addActionListener(new ActionListener() {
 			public void actionPerformed(ActionEvent e) {
@@ -697,6 +717,7 @@
 		buttonControls.add(certificateDefaultsButton);
 		buttonControls.add(newSSLCertificate);
 		buttonControls.add(emailBundle);
+		buttonControls.add(logButton);
 
 		JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, Utils.MARGIN, Utils.MARGIN));
 		userControls.add(new JLabel(Translation.get("gb.filter")));
diff --git a/src/com/gitblit/build/Build.java b/src/com/gitblit/build/Build.java
index 4c5fa19..e8e6b45 100644
--- a/src/com/gitblit/build/Build.java
+++ b/src/com/gitblit/build/Build.java
@@ -106,6 +106,8 @@
 		downloadFromApache(MavenObject.UNBOUND_ID, BuildType.RUNTIME);
 		downloadFromApache(MavenObject.IVY, BuildType.RUNTIME);
 		downloadFromApache(MavenObject.JCALENDAR, BuildType.RUNTIME);
+		downloadFromApache(MavenObject.COMMONS_COMPRESS, BuildType.RUNTIME);
+		downloadFromApache(MavenObject.XZ, BuildType.RUNTIME);
 
 		downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
 		downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.RUNTIME);
@@ -143,7 +145,9 @@
 		downloadFromApache(MavenObject.UNBOUND_ID, BuildType.COMPILETIME);
 		downloadFromApache(MavenObject.IVY, BuildType.COMPILETIME);
 		downloadFromApache(MavenObject.JCALENDAR, BuildType.COMPILETIME);
-		
+		downloadFromApache(MavenObject.COMMONS_COMPRESS, BuildType.COMPILETIME);
+		downloadFromApache(MavenObject.XZ, BuildType.COMPILETIME);
+
 		downloadFromEclipse(MavenObject.JGIT, BuildType.COMPILETIME);
 		downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.COMPILETIME);
 
@@ -779,7 +783,19 @@
                 127000, 0, 0,
                 "323a672aeacb5f5f4461be3b7f7d9d3e4bda80d4",
 				null, "");
-		
+
+		public static final MavenObject COMMONS_COMPRESS = new MavenObject(
+                "commons-compress", "org/apache/commons", "commons-compress", "1.4.1",
+                242000, 265000, 0,
+                "b02e84a993d88568417536240e970c4b809126fd",
+				"277d39267403965a7a192474794a29bac6760a25", "");
+
+		public static final MavenObject XZ = new MavenObject(
+                "xz", "org/tukaani", "xz", "1.0",
+                95000, 120000, 0,
+                "ecff5cb8b1189514c9d1d8d68eb77ac372e000c9",
+				"f95e32a5d2dd8da643c4419814415b9704312993", "");
+
 		public final String name;
 		public final String group;
 		public final String artifact;
diff --git a/src/com/gitblit/utils/CompressionUtils.java b/src/com/gitblit/utils/CompressionUtils.java
new file mode 100644
index 0000000..7b0d047
--- /dev/null
+++ b/src/com/gitblit/utils/CompressionUtils.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.compressors.CompressorStreamFactory;
+import org.apache.commons.compress.utils.IOUtils;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Collection of static methods for retrieving information from a repository.
+ * 
+ * @author James Moger
+ * 
+ */
+public class CompressionUtils {
+
+	static final Logger LOGGER = LoggerFactory.getLogger(CompressionUtils.class);
+
+	/**
+	 * Log an error message and exception.
+	 * 
+	 * @param t
+	 * @param repository
+	 *            if repository is not null it MUST be the {0} parameter in the
+	 *            pattern.
+	 * @param pattern
+	 * @param objects
+	 */
+	private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
+		List<Object> parameters = new ArrayList<Object>();
+		if (objects != null && objects.length > 0) {
+			for (Object o : objects) {
+				parameters.add(o);
+			}
+		}
+		if (repository != null) {
+			parameters.add(0, repository.getDirectory().getAbsolutePath());
+		}
+		LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
+	}
+
+	/**
+	 * Zips the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean zip(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		RevCommit commit = JGitUtils.getCommit(repository, objectId);
+		if (commit == null) {
+			return false;
+		}
+		boolean success = false;
+		RevWalk rw = new RevWalk(repository);
+		TreeWalk tw = new TreeWalk(repository);
+		try {
+			tw.addTree(commit.getTree());
+			ZipOutputStream zos = new ZipOutputStream(os);
+			zos.setComment("Generated by Gitblit");
+			if (!StringUtils.isEmpty(basePath)) {
+				PathFilter f = PathFilter.create(basePath);
+				tw.setFilter(f);
+			}
+			tw.setRecursive(true);
+			while (tw.next()) {
+				if (tw.getFileMode(0) == FileMode.GITLINK) {
+					continue;
+				}
+				ZipEntry entry = new ZipEntry(tw.getPathString());
+				entry.setSize(tw.getObjectReader().getObjectSize(tw.getObjectId(0),
+						Constants.OBJ_BLOB));
+				entry.setComment(commit.getName());
+				zos.putNextEntry(entry);
+
+				ObjectId entid = tw.getObjectId(0);
+				FileMode entmode = tw.getFileMode(0);
+				RevBlob blob = (RevBlob) rw.lookupAny(entid, entmode.getObjectType());
+				rw.parseBody(blob);
+
+				ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
+				byte[] tmp = new byte[4096];
+				InputStream in = ldr.openStream();
+				int n;
+				while ((n = in.read(tmp)) > 0) {
+					zos.write(tmp, 0, n);
+				}
+				in.close();
+			}
+			zos.finish();
+			success = true;
+		} catch (IOException e) {
+			error(e, repository, "{0} failed to zip files from commit {1}", commit.getName());
+		} finally {
+			tw.release();
+			rw.dispose();
+		}
+		return success;
+	}
+	
+	/**
+	 * tar the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean tar(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		return tar(null, repository, basePath, objectId, os);
+	}
+	
+	/**
+	 * tar.gz the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean gz(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		return tar(CompressorStreamFactory.GZIP, repository, basePath, objectId, os);
+	}
+	
+	/**
+	 * tar.xz the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean xz(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		return tar(CompressorStreamFactory.XZ, repository, basePath, objectId, os);
+	}
+	
+	/**
+	 * tar.bzip2 the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean bzip2(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		
+		return tar(CompressorStreamFactory.BZIP2, repository, basePath, objectId, os);
+	}
+	
+	/**
+	 * Compresses/archives the contents of the tree at the (optionally)
+	 * specified revision and the (optionally) specified basepath to the
+	 * supplied outputstream.
+	 * 
+	 * @param algorithm
+	 *            compression algorithm for tar (optional)
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	private static boolean tar(String algorithm, Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		RevCommit commit = JGitUtils.getCommit(repository, objectId);
+		if (commit == null) {
+			return false;
+		}
+		
+		OutputStream cos = os;
+		if (!StringUtils.isEmpty(algorithm)) {
+			try {
+				cos = new CompressorStreamFactory().createCompressorOutputStream(algorithm, os);
+			} catch (CompressorException e1) {
+				error(e1, repository, "{0} failed to open {1} stream", algorithm);
+			}
+		}
+		boolean success = false;
+		RevWalk rw = new RevWalk(repository);
+		TreeWalk tw = new TreeWalk(repository);
+		try {
+			tw.addTree(commit.getTree());
+			TarArchiveOutputStream tos = new TarArchiveOutputStream(cos);
+			tos.setAddPaxHeadersForNonAsciiNames(true);
+			tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
+			if (!StringUtils.isEmpty(basePath)) {
+				PathFilter f = PathFilter.create(basePath);
+				tw.setFilter(f);
+			}
+			tw.setRecursive(true);
+			while (tw.next()) {
+				FileMode mode = tw.getFileMode(0);
+				if (mode == FileMode.GITLINK) {
+					continue;
+				}
+				ObjectId id = tw.getObjectId(0);
+				
+				// new entry
+				TarArchiveEntry entry = new TarArchiveEntry(tw.getPathString());
+				entry.setSize(tw.getObjectReader().getObjectSize(id, Constants.OBJ_BLOB));
+				
+				if (FileMode.SYMLINK.equals(mode)) {
+					// symlink
+					entry.setMode(mode.getBits());
+					
+					// read the symlink target
+					ByteArrayOutputStream bs = new ByteArrayOutputStream();
+					RevBlob blob = (RevBlob) rw.lookupAny(id, mode.getObjectType());
+					rw.parseBody(blob);				
+					ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
+					IOUtils.copy(ldr.openStream(), bs);
+					entry.setLinkName(bs.toString("UTF-8"));
+				} else {
+					// regular file or executable file
+					entry.setMode(mode.getBits());
+				}				
+				entry.setModTime(commit.getAuthorIdent().getWhen());
+
+				tos.putArchiveEntry(entry);
+				
+				if (!FileMode.SYMLINK.equals(mode)) {
+					// write the blob
+					RevBlob blob = (RevBlob) rw.lookupAny(id, mode.getObjectType());
+					rw.parseBody(blob);				
+					ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
+					IOUtils.copy(ldr.openStream(), tos);
+				}
+				
+				// close entry
+				tos.closeArchiveEntry();
+			}
+			tos.finish();
+			tos.close();
+			cos.close();
+			success = true;
+		} catch (IOException e) {
+			error(e, repository, "{0} failed to {1} stream files from commit {2}", algorithm, commit.getName());
+		} finally {
+			tw.release();
+			rw.dispose();
+		}
+		return success;
+	}
+}
diff --git a/src/com/gitblit/utils/HttpUtils.java b/src/com/gitblit/utils/HttpUtils.java
index 68a3506..b40088c 100644
--- a/src/com/gitblit/utils/HttpUtils.java
+++ b/src/com/gitblit/utils/HttpUtils.java
@@ -20,14 +20,13 @@
 import java.security.cert.X509Certificate;
 import java.text.MessageFormat;
 import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.models.UserModel;
+import com.gitblit.utils.X509Utils.X509Metadata;
 
 /**
  * Collection of utility methods for http requests.
@@ -145,21 +144,11 @@
 	 * @return
 	 */
 	public static UserModel getUserModelFromCertificate(X509Certificate cert, String... usernameOIDs) {
-		UserModel user = new UserModel(null);
-		user.isAuthenticated = false;
+		X509Metadata metadata = X509Utils.getMetadata(cert);
 		
-		// manually split DN into OID components
-		// this is instead of parsing with LdapName which:
-		// (1) I don't trust the order of values
-		// (2) it filters out values like EMAILADDRESS
-		String dn = cert.getSubjectDN().getName();
-		Map<String, String> oids = new HashMap<String, String>();
-		for (String kvp : dn.split(",")) {
-			String [] val = kvp.trim().split("=");
-			String oid = val[0].toUpperCase().trim();
-			String data = val[1].trim();
-			oids.put(oid, data);
-		}
+		UserModel user = new UserModel(metadata.commonName);
+		user.emailAddress = metadata.emailAddress;
+		user.isAuthenticated = false;
 		
 		if (usernameOIDs == null || usernameOIDs.length == 0) {
 			// use default usename<->CN mapping
@@ -169,24 +158,23 @@
 		// determine username from OID fingerprint
 		StringBuilder an = new StringBuilder();
 		for (String oid : usernameOIDs) {
-			String val = getOIDValue(oid.toUpperCase(), oids);
+			String val = metadata.getOID(oid.toUpperCase(), null);
 			if (val != null) {
 				an.append(val).append(' ');
 			}
 		}
-		user.username = an.toString().trim();
-		
-		// extract email address, if available
-		user.emailAddress = getOIDValue("E", oids);
-		if (user.emailAddress == null) {
-			user.emailAddress = getOIDValue("EMAILADDRESS", oids);
-		}		
+		user.username = an.toString().trim();		
 		return user;
 	}
 	
-	private static String getOIDValue(String oid, Map<String, String> oids) {
-		if (oids.containsKey(oid)) {
-			return oids.get(oid);
+	public static X509Metadata getCertificateMetadata(HttpServletRequest httpRequest) {
+		if (httpRequest.getAttribute("javax.servlet.request.X509Certificate") != null) {
+			X509Certificate[] certChain = (X509Certificate[]) httpRequest
+					.getAttribute("javax.servlet.request.X509Certificate");
+			if (certChain != null) {
+				X509Certificate cert = certChain[0];
+				return X509Utils.getMetadata(cert);
+			}
 		}
 		return null;
 	}
diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java
index bc44f00..beaa27d 100644
--- a/src/com/gitblit/utils/JGitUtils.java
+++ b/src/com/gitblit/utils/JGitUtils.java
@@ -19,7 +19,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -30,8 +29,6 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.regex.Pattern;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
 
 import org.eclipse.jgit.api.CloneCommand;
 import org.eclipse.jgit.api.FetchCommand;
@@ -1531,7 +1528,7 @@
 		try {
 			// search for the branch in local heads
 			for (RefModel ref : JGitUtils.getLocalBranches(repository, false, -1)) {
-				if (ref.displayName.endsWith(name)) {
+				if (ref.reference.getName().endsWith(name)) {
 					branch = ref;
 					break;
 				}
@@ -1540,7 +1537,7 @@
 			// search for the branch in remote heads
 			if (branch == null) {
 				for (RefModel ref : JGitUtils.getRemoteBranches(repository, false, -1)) {
-					if (ref.displayName.endsWith(name)) {
+					if (ref.reference.getName().endsWith(name)) {
 						branch = ref;
 						break;
 					}
@@ -1720,72 +1717,6 @@
 			}
 		} catch (Throwable t) {
 			error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName);
-		}
-		return success;
-	}
-
-	/**
-	 * Zips the contents of the tree at the (optionally) specified revision and
-	 * the (optionally) specified basepath to the supplied outputstream.
-	 * 
-	 * @param repository
-	 * @param basePath
-	 *            if unspecified, entire repository is assumed.
-	 * @param objectId
-	 *            if unspecified, HEAD is assumed.
-	 * @param os
-	 * @return true if repository was successfully zipped to supplied output
-	 *         stream
-	 */
-	public static boolean zip(Repository repository, String basePath, String objectId,
-			OutputStream os) {
-		RevCommit commit = getCommit(repository, objectId);
-		if (commit == null) {
-			return false;
-		}
-		boolean success = false;
-		RevWalk rw = new RevWalk(repository);
-		TreeWalk tw = new TreeWalk(repository);
-		try {
-			tw.addTree(commit.getTree());
-			ZipOutputStream zos = new ZipOutputStream(os);
-			zos.setComment("Generated by Gitblit");
-			if (!StringUtils.isEmpty(basePath)) {
-				PathFilter f = PathFilter.create(basePath);
-				tw.setFilter(f);
-			}
-			tw.setRecursive(true);
-			while (tw.next()) {
-				if (tw.getFileMode(0) == FileMode.GITLINK) {
-					continue;
-				}
-				ZipEntry entry = new ZipEntry(tw.getPathString());
-				entry.setSize(tw.getObjectReader().getObjectSize(tw.getObjectId(0),
-						Constants.OBJ_BLOB));
-				entry.setComment(commit.getName());
-				zos.putNextEntry(entry);
-
-				ObjectId entid = tw.getObjectId(0);
-				FileMode entmode = tw.getFileMode(0);
-				RevBlob blob = (RevBlob) rw.lookupAny(entid, entmode.getObjectType());
-				rw.parseBody(blob);
-
-				ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
-				byte[] tmp = new byte[4096];
-				InputStream in = ldr.openStream();
-				int n;
-				while ((n = in.read(tmp)) > 0) {
-					zos.write(tmp, 0, n);
-				}
-				in.close();
-			}
-			zos.finish();
-			success = true;
-		} catch (IOException e) {
-			error(e, repository, "{0} failed to zip files from commit {1}", commit.getName());
-		} finally {
-			tw.release();
-			rw.dispose();
 		}
 		return success;
 	}
diff --git a/src/com/gitblit/utils/X509Utils.java b/src/com/gitblit/utils/X509Utils.java
index 24afb8d..cfad9ec 100644
--- a/src/com/gitblit/utils/X509Utils.java
+++ b/src/com/gitblit/utils/X509Utils.java
@@ -111,6 +111,12 @@
 
 	private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
 	
+	private static final int KEY_LENGTH = 2048;
+	
+	private static final String KEY_ALGORITHM = "RSA";
+	
+	private static final String SIGNING_ALGORITHM = "SHA512withRSA";
+	
 	public static final boolean unlimitedStrength;
 	
 	private static final Logger logger = LoggerFactory.getLogger(X509Utils.class);
@@ -182,6 +188,9 @@
 		
 		// displayname of user for README in bundle
 		public String userDisplayname;
+
+		// serialnumber of generated or read certificate
+		public String serialNumber;
 
 		public X509Metadata(String cn, String pwd) {
 			if (StringUtils.isEmpty(cn)) {
@@ -472,8 +481,8 @@
 	 * @throws Exception
 	 */
 	private static KeyPair newKeyPair() throws Exception {
-		KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", BC);
-		kpGen.initialize(2048, new SecureRandom());
+		KeyPairGenerator kpGen = KeyPairGenerator.getInstance(KEY_ALGORITHM, BC);
+		kpGen.initialize(KEY_LENGTH, new SecureRandom());
 		return kpGen.generateKeyPair();
 	}
 	
@@ -547,7 +556,7 @@
 			certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
 			certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));
 
-			ContentSigner caSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
+			ContentSigner caSigner = new JcaContentSignerBuilder(SIGNING_ALGORITHM)
 					.setProvider(BC).build(caPrivateKey);
 			X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC)
 					.getCertificate(certBuilder.build(caSigner));
@@ -562,6 +571,10 @@
 			saveKeyStore(targetStoreFile, serverStore, sslMetadata.password);
 			
 	        x509log.log(MessageFormat.format("New SSL certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
+	        
+	        // update serial number in metadata object
+	        sslMetadata.serialNumber = cert.getSerialNumber().toString();
+
 			return cert;
 		} catch (Throwable t) {
 			throw new RuntimeException("Failed to generate SSL certificate!", t);
@@ -582,7 +595,7 @@
 		try {
 			KeyPair caPair = newKeyPair();
 			
-			ContentSigner caSigner = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPair.getPrivate());
+			ContentSigner caSigner = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPair.getPrivate());
 			
 			// clone metadata
 			X509Metadata caMetadata = metadata.clone(CA_CN, metadata.password);
@@ -623,6 +636,9 @@
 			
 			x509log.log(MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getIssuerDN().getName()));
 
+	        // update serial number in metadata object
+	        caMetadata.serialNumber = cert.getSerialNumber().toString();
+
 			return cert;
 		} catch (Throwable t) {
 			throw new RuntimeException("Failed to generate Gitblit CA certificate!", t);
@@ -649,7 +665,7 @@
 			X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
 			
 			// build and sign CRL with CA private key
-			ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);
+			ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPrivateKey);
 			X509CRLHolder crl = crlBuilder.build(signer);
 
 			File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");
@@ -743,6 +759,17 @@
 	        		zos.write(FileUtils.readContent(pemFile));
 	        		zos.closeEntry();
 	        	}
+	        	
+	        	// include user's public certificate
+	        	zos.putNextEntry(new ZipEntry(clientMetadata.commonName + ".cer"));
+        		zos.write(cert.getEncoded());
+        		zos.closeEntry();
+        		
+	        	// include CA public certificate
+	        	zos.putNextEntry(new ZipEntry("ca.cer"));
+        		zos.write(caCert.getEncoded());
+        		zos.closeEntry();
+        		
 	        	if (readme != null) {
 	        		zos.putNextEntry(new ZipEntry("README.TXT"));
 	        		zos.write(readme.getBytes("UTF-8"));
@@ -799,7 +826,7 @@
 				certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
 			}
 
-			ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);
+			ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPrivateKey);
 
 			X509Certificate userCert = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certBuilder.build(signer));
 			PKCS12BagAttributeCarrier bagAttr = (PKCS12BagAttributeCarrier)pair.getPrivate();
@@ -851,6 +878,9 @@
 	        
 	        // save certificate after successfully creating the key stores
 	        saveCertificate(userCert, certFile);
+	        
+	        // update serial number in metadata object
+	        clientMetadata.serialNumber = userCert.getSerialNumber().toString();
 	        
 	        return userCert;
 		} catch (Throwable t) {
@@ -1065,4 +1095,30 @@
 		}
 		return false;
 	}
+	
+	public static X509Metadata getMetadata(X509Certificate cert) {
+		// manually split DN into OID components
+		// this is instead of parsing with LdapName which:
+		// (1) I don't trust the order of values
+		// (2) it filters out values like EMAILADDRESS
+		String dn = cert.getSubjectDN().getName();
+		Map<String, String> oids = new HashMap<String, String>();
+		for (String kvp : dn.split(",")) {
+			String [] val = kvp.trim().split("=");
+			String oid = val[0].toUpperCase().trim();
+			String data = val[1].trim();
+			oids.put(oid, data);
+		}
+		
+		X509Metadata metadata = new X509Metadata(oids.get("CN"), "whocares");
+		metadata.oids.putAll(oids);
+		metadata.serialNumber = cert.getSerialNumber().toString();
+		metadata.notAfter = cert.getNotAfter();
+		metadata.notBefore = cert.getNotBefore();
+		metadata.emailAddress = metadata.getOID("E", null);
+		if (metadata.emailAddress == null) {
+			metadata.emailAddress = metadata.getOID("EMAILADDRESS", null);
+		}
+		return metadata;
+	}
 }
diff --git a/src/com/gitblit/wicket/GitBlitWebSession.java b/src/com/gitblit/wicket/GitBlitWebSession.java
index 015d97a..5195a1f 100644
--- a/src/com/gitblit/wicket/GitBlitWebSession.java
+++ b/src/com/gitblit/wicket/GitBlitWebSession.java
@@ -29,6 +29,7 @@
 import org.apache.wicket.protocol.http.WebSession;
 import org.apache.wicket.protocol.http.request.WebClientInfo;
 
+import com.gitblit.Constants.AuthenticationType;
 import com.gitblit.models.UserModel;
 
 public final class GitBlitWebSession extends WebSession {
@@ -45,9 +46,12 @@
 	
 	private AtomicBoolean isForking;
 	
+	public AuthenticationType authenticationType;
+	
 	public GitBlitWebSession(Request request) {
 		super(request);
 		isForking = new AtomicBoolean();
+		authenticationType = AuthenticationType.CREDENTIALS;
 	}
 
 	public void invalidate() {
diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java
index 05640ad..d1ee271 100644
--- a/src/com/gitblit/wicket/pages/BasePage.java
+++ b/src/com/gitblit/wicket/pages/BasePage.java
@@ -29,7 +29,6 @@
 import java.util.TimeZone;
 import java.util.regex.Pattern;
 
-import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 
 import org.apache.wicket.Application;
@@ -131,22 +130,18 @@
 	}	
 
 	private void login() {
-		// try to authenticate by servlet request
-		UserModel user = GitBlit.self().authenticate(((WebRequest) getRequestCycle().getRequest()).getHttpServletRequest());
-
-		if (user == null) {
-			// try to authenticate by cookie
-			Cookie[] cookies = ((WebRequest) getRequestCycle().getRequest()).getCookies();
-			if (GitBlit.self().allowCookieAuthentication() && cookies != null && cookies.length > 0) {
-				// Grab cookie from Browser Session
-				user = GitBlit.self().authenticate(cookies);
-			}
+		GitBlitWebSession session = GitBlitWebSession.get();
+		if (session.isLoggedIn() && !session.isSessionInvalidated()) {
+			// already have a session
+			return;
 		}
+		
+		// try to authenticate by servlet request
+		HttpServletRequest httpRequest = ((WebRequest) getRequestCycle().getRequest()).getHttpServletRequest();
+		UserModel user = GitBlit.self().authenticate(httpRequest);
 
 		// Login the user
 		if (user != null) {
-			// Set the user into the session
-			GitBlitWebSession session = GitBlitWebSession.get();
 			// issue 62: fix session fixation vulnerability
 			session.replaceSession();
 			session.setUser(user);
@@ -431,14 +426,19 @@
 		public UserFragment(String id, String markupId, MarkupContainer markupProvider) {
 			super(id, markupId, markupProvider);
 
-			if (GitBlitWebSession.get().isLoggedIn()) {
-				// username, logout, and change password
-				add(new Label("username", GitBlitWebSession.get().getUser().getDisplayName() + ":"));
-				add(new LinkPanel("loginLink", null, markupProvider.getString("gb.logout"),
-						LogoutPage.class));
+			GitBlitWebSession session = GitBlitWebSession.get();
+			if (session.isLoggedIn()) {				
+				UserModel user = session.getUser();
 				boolean editCredentials = GitBlit.self().supportsCredentialChanges();
+				boolean standardLogin = session.authenticationType.isStandard();
+
+				// username, logout, and change password
+				add(new Label("username", user.getDisplayName() + ":"));
+				add(new LinkPanel("loginLink", null, markupProvider.getString("gb.logout"),
+						LogoutPage.class).setVisible(standardLogin));
+				
 				// quick and dirty hack for showing a separator
-				add(new Label("separator", "|").setVisible(editCredentials));
+				add(new Label("separator", "|").setVisible(standardLogin && editCredentials));
 				add(new BookmarkablePageLink<Void>("changePasswordLink", 
 						ChangePasswordPage.class).setVisible(editCredentials));
 			} else {
diff --git a/src/com/gitblit/wicket/pages/CommitPage.html b/src/com/gitblit/wicket/pages/CommitPage.html
index 20e6b60..79a038c 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.html
+++ b/src/com/gitblit/wicket/pages/CommitPage.html
@@ -30,7 +30,7 @@
 		<tr class="hidden-phone"><th><wicket:message key="gb.tree">tree</wicket:message></th>
 			<td><span class="sha1" wicket:id="commitTree">[commit tree]</span>
 				<span class="link">
-					<a wicket:id="treeLink"><wicket:message key="gb.tree"></wicket:message></a> | <a wicket:id="zipLink"><wicket:message key="gb.zip"></wicket:message></a>
+					<a wicket:id="treeLink"><wicket:message key="gb.tree"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
 				</span>
 			</td></tr>
 		<tr class="hidden-phone"><th valign="top"><wicket:message key="gb.parent">parent</wicket:message></th>
diff --git a/src/com/gitblit/wicket/pages/CommitPage.java b/src/com/gitblit/wicket/pages/CommitPage.java
index 7bc6b41..b2a8112 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/com/gitblit/wicket/pages/CommitPage.java
@@ -22,7 +22,6 @@
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
-import org.apache.wicket.markup.html.link.ExternalLink;
 import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.markup.repeater.data.DataView;
 import org.apache.wicket.markup.repeater.data.ListDataProvider;
@@ -32,16 +31,15 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import com.gitblit.Constants;
-import com.gitblit.DownloadZipServlet;
 import com.gitblit.GitBlit;
-import com.gitblit.Keys;
 import com.gitblit.models.GitNote;
-import com.gitblit.models.SubmoduleModel;
 import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.SubmoduleModel;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.CommitHeaderPanel;
 import com.gitblit.wicket.panels.CommitLegendPanel;
+import com.gitblit.wicket.panels.CompressedDownloadsPanel;
 import com.gitblit.wicket.panels.GravatarImage;
 import com.gitblit.wicket.panels.LinkPanel;
 import com.gitblit.wicket.panels.RefsPanel;
@@ -95,8 +93,8 @@
 				newCommitParameter()));
 		add(new BookmarkablePageLink<Void>("treeLink", TreePage.class, newCommitParameter()));
 		final String baseUrl = WicketUtils.getGitblitURL(getRequest());
-		add(new ExternalLink("zipLink", DownloadZipServlet.asLink(baseUrl, repositoryName,
-				objectId, null)).setVisible(GitBlit.getBoolean(Keys.web.allowZipDownloads, true)));
+		
+		add(new CompressedDownloadsPanel("compressedLinks", baseUrl, repositoryName, objectId, null));
 
 		// Parent Commits
 		ListDataProvider<String> parentsDp = new ListDataProvider<String>(parents);
diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java
index c90e353..346edc3 100644
--- a/src/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -115,7 +115,7 @@
 				boolean canAccess = user.hasBranchPermission(repositoryName,
 								branch.reference.getName());
 				if (!canAccess) {
-					error(getString("gb.accessDeined"), true);
+					error(getString("gb.accessDenied"), true);
 				}
 			}
 		}
diff --git a/src/com/gitblit/wicket/pages/TreePage.html b/src/com/gitblit/wicket/pages/TreePage.html
index 0047ff0..b7e55ed 100644
--- a/src/com/gitblit/wicket/pages/TreePage.html
+++ b/src/com/gitblit/wicket/pages/TreePage.html
@@ -9,7 +9,7 @@
 
 	<!-- blob nav links -->	
 	<div class="page_nav2">
-		<a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="headLink"><wicket:message key="gb.head"></wicket:message></a> | <a wicket:id="zipLink"><wicket:message key="gb.zip"></wicket:message></a>
+		<a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="headLink"><wicket:message key="gb.head"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
 	</div>	
 	
 	<!-- commit header -->
@@ -32,14 +32,14 @@
 	<!--  submodule links -->
 	<wicket:fragment wicket:id="submoduleLinks">
 		<span class="link">
-			<a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="zip"><wicket:message key="gb.zip"></wicket:message></a>
+			<a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
 		</span>
 	</wicket:fragment>
 
 	<!--  tree links -->
 	<wicket:fragment wicket:id="treeLinks">
 		<span class="link">
-			<span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="zip"><wicket:message key="gb.zip"></wicket:message></a>
+			<span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
 		</span>
 	</wicket:fragment>
 	
diff --git a/src/com/gitblit/wicket/pages/TreePage.java b/src/com/gitblit/wicket/pages/TreePage.java
index 973634b..345814f 100644
--- a/src/com/gitblit/wicket/pages/TreePage.java
+++ b/src/com/gitblit/wicket/pages/TreePage.java
@@ -20,7 +20,6 @@
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
-import org.apache.wicket.markup.html.link.ExternalLink;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.markup.repeater.data.DataView;
@@ -30,15 +29,13 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 
-import com.gitblit.DownloadZipServlet;
-import com.gitblit.GitBlit;
-import com.gitblit.Keys;
 import com.gitblit.models.PathModel;
 import com.gitblit.models.SubmoduleModel;
 import com.gitblit.utils.ByteFormat;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.CommitHeaderPanel;
+import com.gitblit.wicket.panels.CompressedDownloadsPanel;
 import com.gitblit.wicket.panels.LinkPanel;
 import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
 
@@ -58,9 +55,8 @@
 				WicketUtils.newPathParameter(repositoryName, objectId, path)));
 		add(new BookmarkablePageLink<Void>("headLink", TreePage.class,
 				WicketUtils.newPathParameter(repositoryName, Constants.HEAD, path)));
-		add(new ExternalLink("zipLink", DownloadZipServlet.asLink(getRequest()
-				.getRelativePathPrefixToContextRoot(), repositoryName, objectId, path))
-				.setVisible(GitBlit.getBoolean(Keys.web.allowZipDownloads, true)));
+		add(new CompressedDownloadsPanel("compressedLinks", getRequest()
+				.getRelativePathPrefixToContextRoot(), repositoryName, objectId, path));
 
 		add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
 
@@ -114,10 +110,10 @@
 										entry.path)));
 						links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
 								WicketUtils.newPathParameter(repositoryName, entry.commitId,
-										entry.path)));
-						links.add(new ExternalLink("zip", DownloadZipServlet.asLink(baseUrl,
-								repositoryName, objectId, entry.path)).setVisible(GitBlit
-								.getBoolean(Keys.web.allowZipDownloads, true)));
+										entry.path)));						
+						links.add(new CompressedDownloadsPanel("compressedLinks", baseUrl,
+								repositoryName, objectId, entry.path));
+
 						item.add(links);
 					} else if (entry.isSubmodule()) {
 						// submodule
@@ -143,9 +139,8 @@
 						links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
 								WicketUtils.newPathParameter(submodulePath, submoduleId,
 										"")).setEnabled(hasSubmodule));
-						links.add(new ExternalLink("zip", DownloadZipServlet.asLink(baseUrl,
-								submodulePath, submoduleId, "")).setVisible(GitBlit
-								.getBoolean(Keys.web.allowZipDownloads, true)).setEnabled(hasSubmodule));
+						links.add(new CompressedDownloadsPanel("compressedLinks", baseUrl,
+								submodulePath, submoduleId, "").setEnabled(hasSubmodule));
 						item.add(links);						
 					} else {
 						// blob link
diff --git a/src/com/gitblit/wicket/panels/BranchesPanel.java b/src/com/gitblit/wicket/panels/BranchesPanel.java
index cfe9f5f..26fedc1 100644
--- a/src/com/gitblit/wicket/panels/BranchesPanel.java
+++ b/src/com/gitblit/wicket/panels/BranchesPanel.java
@@ -36,8 +36,10 @@
 import com.gitblit.SyndicationServlet;
 import com.gitblit.models.RefModel;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.pages.BranchesPage;
 import com.gitblit.wicket.pages.CommitPage;
@@ -58,9 +60,24 @@
 
 		// branches
 		List<RefModel> branches = new ArrayList<RefModel>();
-		branches.addAll(JGitUtils.getLocalBranches(r, false, maxCount));
+		UserModel user = GitBlitWebSession.get().getUser();
+		if (user == null) {
+			user = UserModel.ANONYMOUS;
+		}
+
+		List<RefModel> localBranches = JGitUtils.getLocalBranches(r, false, -1);
+		for (RefModel refModel : localBranches) {
+			if (user.hasBranchPermission(model.name, refModel.reference.getName())) {
+				branches.add(refModel);
+			}
+		}
 		if (model.showRemoteBranches) {
-			branches.addAll(JGitUtils.getRemoteBranches(r, false, maxCount));
+			List<RefModel> remoteBranches = JGitUtils.getRemoteBranches(r, false, -1);
+			for (RefModel refModel : remoteBranches) {
+				if (user.hasBranchPermission(model.name, refModel.reference.getName())) {
+					branches.add(refModel);
+				}
+			}
 		}
 		Collections.sort(branches);
 		Collections.reverse(branches);
diff --git a/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html b/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html
new file mode 100644
index 0000000..7123d0a
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<wicket:panel>
+	<span wicket:id="compressedLinks">
+		<span wicket:id="linkSep">|</span><span wicket:id="compressedLink">ref</span>
+	</span>	
+</wicket:panel>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java b/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java
new file mode 100644
index 0000000..b22c758
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.DownloadZipServlet;
+import com.gitblit.DownloadZipServlet.Format;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+
+public class CompressedDownloadsPanel extends Panel {
+
+	private static final long serialVersionUID = 1L;
+
+	public CompressedDownloadsPanel(String id, final String baseUrl, final String repositoryName, final String objectId, final String path) {
+		super(id);
+		
+		List<String> types = GitBlit.getStrings(Keys.web.compressedDownloads);
+		if (types.isEmpty()) {
+			types.add(Format.zip.name());
+			types.add(Format.gz.name());
+		}
+		
+		ListDataProvider<String> refsDp = new ListDataProvider<String>(types);
+		DataView<String> refsView = new DataView<String>("compressedLinks", refsDp) {
+			private static final long serialVersionUID = 1L;
+			int counter;
+
+			@Override
+			protected void onBeforeRender() {
+				super.onBeforeRender();
+				counter = 0;
+			}
+			
+			@Override
+			public void populateItem(final Item<String> item) {
+				String compressionType = item.getModelObject();
+				Format format = Format.fromName(compressionType);
+				
+				String href = DownloadZipServlet.asLink(baseUrl, repositoryName,
+						objectId, path, format);
+				Component c = new LinkPanel("compressedLink", null, format.name(), href);
+				item.add(c);
+				Label lb = new Label("linkSep", "|");
+				lb.setVisible(counter > 0);
+				lb.setRenderBodyOnly(true);
+				item.add(lb.setEscapeModelStrings(false));
+				item.setRenderBodyOnly(true);
+				counter++;
+			}
+		};
+		add(refsView);
+		
+		setVisible(GitBlit.getBoolean(Keys.web.allowZipDownloads, true));
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/HistoryPanel.html b/src/com/gitblit/wicket/panels/HistoryPanel.html
index fed0807..811eeed 100644
--- a/src/com/gitblit/wicket/panels/HistoryPanel.html
+++ b/src/com/gitblit/wicket/panels/HistoryPanel.html
@@ -20,7 +20,7 @@
          		<td class="icon"><img wicket:id="commitIcon" /></td>
          		<td class="hidden-phone author"><span wicket:id="commitAuthor">[commit author]</span></td>
          		<td class="message"><table class="nestedTable"><tr><td><span style="vertical-align:middle;" wicket:id="commitShortMessage">[commit short message]</span></td><td><div style="text-align:right;" wicket:id="commitRefs">[commit refs]</div></td></tr></table></td>
-         		<td class="hidden-phone hidden-tablet"><span class="link" wicket:id="hashLabel">[hash label]</span><span wicket:id="hashLink">[hash link]</span></td>
+         		<td class="hidden-phone hidden-tablet rightAlign"><span class="link" wicket:id="hashLabel">[hash label]</span><span wicket:id="hashLink">[hash link]</span></td>
          		<td class="hidden-phone rightAlign">
          			<span wicket:id="historyLinks">[history links]</span>
 				</td>
diff --git a/src/com/gitblit/wicket/panels/HistoryPanel.java b/src/com/gitblit/wicket/panels/HistoryPanel.java
index 152bbae..0f58603 100644
--- a/src/com/gitblit/wicket/panels/HistoryPanel.java
+++ b/src/com/gitblit/wicket/panels/HistoryPanel.java
@@ -171,7 +171,7 @@
 					LinkPanel commitHash = new LinkPanel("hashLink", null, entry.getName().substring(0, hashLen),
 							TreePage.class, WicketUtils.newObjectParameter(
 									repositoryName, entry.getName()));
-					WicketUtils.setCssClass(commitHash, "sha1");
+					WicketUtils.setCssClass(commitHash, "shortsha1");
 					WicketUtils.setHtmlTooltip(commitHash, entry.getName());					
 					item.add(commitHash);
 					
diff --git a/src/com/gitblit/wicket/panels/LogPanel.html b/src/com/gitblit/wicket/panels/LogPanel.html
index d0770a3..2b2605a 100644
--- a/src/com/gitblit/wicket/panels/LogPanel.html
+++ b/src/com/gitblit/wicket/panels/LogPanel.html
@@ -16,7 +16,7 @@
          		<td class="hidden-phone author"><span wicket:id="commitAuthor">[commit author]</span></td>
          		<td class="hidden-phone icon"><img wicket:id="commitIcon" /></td>
          		<td class="message"><table class="nestedTable"><tr><td><span style="vertical-align:middle;" wicket:id="commitShortMessage">[commit short message]</span></td><td><div style="text-align:right;" wicket:id="commitRefs">[commit refs]</div></td></tr></table></td>
-         		<td class="hidden-phone hidden-tablet"><span wicket:id="hashLink">[hash link]</span></td>
+         		<td class="hidden-phone hidden-tablet rightAlign"><span wicket:id="hashLink">[hash link]</span></td>
          		<td class="hidden-phone hidden-tablet rightAlign">
          			<span class="link">
 						<a wicket:id="diff"><wicket:message key="gb.diff"></wicket:message></a> | <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
diff --git a/src/com/gitblit/wicket/panels/LogPanel.java b/src/com/gitblit/wicket/panels/LogPanel.java
index 0686dee..0539764 100644
--- a/src/com/gitblit/wicket/panels/LogPanel.java
+++ b/src/com/gitblit/wicket/panels/LogPanel.java
@@ -131,7 +131,7 @@
 				LinkPanel commitHash = new LinkPanel("hashLink", null, entry.getName().substring(0, hashLen),
 						CommitPage.class, WicketUtils.newObjectParameter(
 								repositoryName, entry.getName()));
-				WicketUtils.setCssClass(commitHash, "sha1");
+				WicketUtils.setCssClass(commitHash, "shortsha1");
 				WicketUtils.setHtmlTooltip(commitHash, entry.getName());
 				item.add(commitHash);
 				
diff --git a/tests/com/gitblit/tests/JGitUtilsTest.java b/tests/com/gitblit/tests/JGitUtilsTest.java
index 7e4d630..ce72a46 100644
--- a/tests/com/gitblit/tests/JGitUtilsTest.java
+++ b/tests/com/gitblit/tests/JGitUtilsTest.java
@@ -50,6 +50,7 @@
 import com.gitblit.models.PathModel;
 import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.models.RefModel;
+import com.gitblit.utils.CompressionUtils;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.StringUtils;
 
@@ -446,16 +447,16 @@
 
 	@Test
 	public void testZip() throws Exception {
-		assertFalse(JGitUtils.zip(null, null, null, null));
+		assertFalse(CompressionUtils.zip(null, null, null, null));
 		Repository repository = GitBlitSuite.getHelloworldRepository();
 		File zipFileA = new File(GitBlitSuite.REPOSITORIES, "helloworld.zip");
 		FileOutputStream fosA = new FileOutputStream(zipFileA);
-		boolean successA = JGitUtils.zip(repository, null, Constants.HEAD, fosA);
+		boolean successA = CompressionUtils.zip(repository, null, Constants.HEAD, fosA);
 		fosA.close();
 
 		File zipFileB = new File(GitBlitSuite.REPOSITORIES, "helloworld-java.zip");
 		FileOutputStream fosB = new FileOutputStream(zipFileB);
-		boolean successB = JGitUtils.zip(repository, "java.java", Constants.HEAD, fosB);
+		boolean successB = CompressionUtils.zip(repository, "java.java", Constants.HEAD, fosB);
 		fosB.close();
 
 		repository.close();
diff --git a/tests/com/gitblit/tests/X509UtilsTest.java b/tests/com/gitblit/tests/X509UtilsTest.java
index 85d30de..5d17e18 100644
--- a/tests/com/gitblit/tests/X509UtilsTest.java
+++ b/tests/com/gitblit/tests/X509UtilsTest.java
@@ -118,7 +118,12 @@
 		File zip = X509Utils.newClientBundle(userMetadata, storeFile, caPassword, log);
 		assertTrue(zip.exists());
 		
-		List<String> expected = Arrays.asList(userMetadata.commonName + ".pem", userMetadata.commonName + ".p12", "README.TXT");
+		List<String> expected = Arrays.asList(
+				userMetadata.commonName + ".pem",
+				userMetadata.commonName + ".p12",
+				userMetadata.commonName + ".cer",
+				"ca.cer",
+				"README.TXT");
 		
 		ZipInputStream zis = new ZipInputStream(new FileInputStream(zip));
 		ZipEntry entry = null;

--
Gitblit v1.9.1