mschaefers
2012-12-03 ba6ae959b8e21c714c69f66254e82837d45a3ed2
Merge branch 'master' of https://github.com/gitblit/gitblit into enhancedLdap

Conflicts:
distrib/gitblit.properties
4 files added
29 files modified
1020 ■■■■ changed files
.classpath 2 ●●●●● patch | view | raw | blame | history
NOTICE 18 ●●●●● patch | view | raw | blame | history
build.xml 7 ●●●●● patch | view | raw | blame | history
distrib/gitblit.properties 105 ●●●●● patch | view | raw | blame | history
docs/04_design.mkd 2 ●●●●● patch | view | raw | blame | history
docs/04_releases.mkd 6 ●●●● patch | view | raw | blame | history
resources/gitblit.css 6 ●●●● patch | view | raw | blame | history
resources/script_16x16.png patch | view | raw | blame | history
src/com/gitblit/Constants.java 10 ●●●●● patch | view | raw | blame | history
src/com/gitblit/DownloadZipServlet.java 58 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 50 ●●●● patch | view | raw | blame | history
src/com/gitblit/authority/GitblitAuthority.java 21 ●●●●● patch | view | raw | blame | history
src/com/gitblit/build/Build.java 20 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/CompressionUtils.java 315 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/HttpUtils.java 42 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/JGitUtils.java 73 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/X509Utils.java 68 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebSession.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/BasePage.java 38 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/CommitPage.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/CommitPage.java 10 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/TreePage.html 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/TreePage.java 23 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/BranchesPanel.java 21 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html 12 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java 77 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/HistoryPanel.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/HistoryPanel.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/LogPanel.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/LogPanel.java 2 ●●● patch | view | raw | blame | history
tests/com/gitblit/tests/JGitUtilsTest.java 7 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/X509UtilsTest.java 7 ●●●● patch | view | raw | blame | history
.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" />
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
---------------------------------------------------------------------------
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
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" />
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
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)
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
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;
resources/script_16x16.png
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 {
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);
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;
    }
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")));
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;
src/com/gitblit/utils/CompressionUtils.java
New file
@@ -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;
    }
}
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;
    }
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;
    }
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;
    }
}
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() {
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 {
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>
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);
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);
                }
            }
        }
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>
    
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
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);
src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html
New file
@@ -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>
src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java
New file
@@ -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));
    }
}
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>
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);
                    
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>
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);
                
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();
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;