James Moger
2012-11-30 59b817a55b04b4bd8c5950a2d97998d3af6d44e3
Support alternate compressed download formats (issue-174)
3 files added
9 files modified
595 ■■■■ changed files
distrib/gitblit.properties 13 ●●●●● patch | view | raw | blame | history
docs/04_releases.mkd 3 ●●●●● patch | view | raw | blame | history
src/com/gitblit/DownloadZipServlet.java 58 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/CompressionUtils.java 315 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/JGitUtils.java 69 ●●●●● 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/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/CompressedDownloadsPanel.html 12 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java 77 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/JGitUtilsTest.java 7 ●●●●● patch | view | raw | blame | history
distrib/gitblit.properties
@@ -482,6 +482,19 @@
# 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
# commit traversal. There are scenarios where you may want to completely disable
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
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/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/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;
@@ -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/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/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/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));
    }
}
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();