From 59b817a55b04b4bd8c5950a2d97998d3af6d44e3 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 30 Nov 2012 18:05:35 -0500
Subject: [PATCH] Support alternate compressed download formats (issue-174)

---
 src/com/gitblit/utils/CompressionUtils.java                 |  315 +++++++++++++++++++++++++++++++
 src/com/gitblit/wicket/pages/CommitPage.java                |   10 
 src/com/gitblit/DownloadZipServlet.java                     |   58 +++++
 docs/04_releases.mkd                                        |    3 
 src/com/gitblit/wicket/pages/TreePage.java                  |   23 -
 tests/com/gitblit/tests/JGitUtilsTest.java                  |    7 
 src/com/gitblit/wicket/pages/TreePage.html                  |    6 
 distrib/gitblit.properties                                  |   13 +
 src/com/gitblit/wicket/pages/CommitPage.html                |    2 
 src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html |   12 +
 src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java |   77 +++++++
 src/com/gitblit/utils/JGitUtils.java                        |   69 ------
 12 files changed, 492 insertions(+), 103 deletions(-)

diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index ee050a4..233fdfa 100644
--- a/distrib/gitblit.properties
+++ b/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
diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd
index 70a853f..3f03160 100644
--- a/docs/04_releases.mkd
+++ b/docs/04_releases.mkd
@@ -60,6 +60,8 @@
 - Added Gitblit Certificate Authority, an X509 certificate generation tool for Gitblit GO to encourage use of client certificate authentication.
 - Added setting to control length of shortened commit ids  
     **New:** *web.shortCommitIdLength=8*  
+- Added alternate compressed download formats: tar.gz, tar.xz, tar.bzip2 (issue 174)  
+    **New:** *web.compressedDownloads = zip gz*
 - Added simple project pages.  A project is a subfolder off the *git.repositoriesFolder*.
 - Added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)
 - Delete branch feature (issue 121, Github/ajermakovics)
@@ -70,6 +72,7 @@
 
 #### changes
 
+- Access restricted servlets (e.g. DownloadZip, RSS, etc) will try to authenticate any Gitblit cookie found in the request before resorting to BASIC authentication.
 - Added *groovy* and *scala* to *web.prettyPrintExtensions*
 - Added short commit id column to log and history tables (issue 168)
 - Teams can now specify the *admin*, *create*, and *fork* roles to simplify user administration
diff --git a/src/com/gitblit/DownloadZipServlet.java b/src/com/gitblit/DownloadZipServlet.java
index 2655934..0feee87 100644
--- a/src/com/gitblit/DownloadZipServlet.java
+++ b/src/com/gitblit/DownloadZipServlet.java
@@ -29,6 +29,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.utils.CompressionUtils;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.MarkdownUtils;
 import com.gitblit.utils.StringUtils;
@@ -45,6 +46,25 @@
 	private static final long serialVersionUID = 1L;
 
 	private transient Logger logger = LoggerFactory.getLogger(DownloadZipServlet.class);
+	
+	public static enum Format {
+		zip(".zip"), tar(".tar"), gz(".tar.gz"), xz(".tar.xz"), bzip2(".tar.bzip2");
+		
+		public final String extension;
+		
+		Format(String ext) {
+			this.extension = ext;
+		}
+		
+		public static Format fromName(String name) {
+			for (Format format : values()) {
+				if (format.name().equalsIgnoreCase(name)) {
+					return format;
+				}
+			}
+			return zip;
+		}
+	}
 
 	public DownloadZipServlet() {
 		super();
@@ -57,15 +77,17 @@
 	 * @param repository
 	 * @param objectId
 	 * @param path
+	 * @param format
 	 * @return an url
 	 */
-	public static String asLink(String baseURL, String repository, String objectId, String path) {
+	public static String asLink(String baseURL, String repository, String objectId, String path, Format format) {
 		if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
 			baseURL = baseURL.substring(0, baseURL.length() - 1);
 		}
 		return baseURL + Constants.ZIP_PATH + "?r=" + repository
 				+ (path == null ? "" : ("&p=" + path))
-				+ (objectId == null ? "" : ("&h=" + objectId));
+				+ (objectId == null ? "" : ("&h=" + objectId))
+				+ (format == null ? "" : ("&format=" + format.name()));
 	}
 
 	/**
@@ -84,16 +106,22 @@
 			response.sendError(HttpServletResponse.SC_FORBIDDEN);
 			return;
 		}
-
+		
+		Format format = Format.zip;
 		String repository = request.getParameter("r");
 		String basePath = request.getParameter("p");
 		String objectId = request.getParameter("h");
-
+		String f = request.getParameter("format");
+		if (!StringUtils.isEmpty(f)) {
+			format = Format.fromName(f);
+		}
+		
 		try {
 			String name = repository;
 			if (name.indexOf('/') > -1) {
 				name = name.substring(name.lastIndexOf('/') + 1);
 			}
+			name = StringUtils.stripDotGit(name);
 
 			if (!StringUtils.isEmpty(basePath)) {
 				name += "-" + basePath.replace('/', '_');
@@ -122,15 +150,31 @@
 
 			String contentType = "application/octet-stream";
 			response.setContentType(contentType + "; charset=" + response.getCharacterEncoding());
-			response.setHeader("Content-Disposition", "attachment; filename=\"" + name + ".zip"
-					+ "\"");
+			response.setHeader("Content-Disposition", "attachment; filename=\"" + name + format.extension + "\"");
 			response.setDateHeader("Last-Modified", date.getTime());
 			response.setHeader("Cache-Control", "no-cache");
 			response.setHeader("Pragma", "no-cache");
 			response.setDateHeader("Expires", 0);
 
 			try {
-				JGitUtils.zip(r, basePath, objectId, response.getOutputStream());
+				switch (format) {
+				case zip:
+					CompressionUtils.zip(r, basePath, objectId, response.getOutputStream());
+					break;
+				case tar:
+					CompressionUtils.tar(r, basePath, objectId, response.getOutputStream());
+					break;
+				case gz:
+					CompressionUtils.gz(r, basePath, objectId, response.getOutputStream());
+					break;
+				case xz:
+					CompressionUtils.xz(r, basePath, objectId, response.getOutputStream());
+					break;
+				case bzip2:
+					CompressionUtils.bzip2(r, basePath, objectId, response.getOutputStream());
+					break;
+				}
+				
 				response.flushBuffer();
 			} catch (Throwable t) {
 				logger.error("Failed to write attachment to client", t);
diff --git a/src/com/gitblit/utils/CompressionUtils.java b/src/com/gitblit/utils/CompressionUtils.java
new file mode 100644
index 0000000..7b0d047
--- /dev/null
+++ b/src/com/gitblit/utils/CompressionUtils.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.compressors.CompressorStreamFactory;
+import org.apache.commons.compress.utils.IOUtils;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Collection of static methods for retrieving information from a repository.
+ * 
+ * @author James Moger
+ * 
+ */
+public class CompressionUtils {
+
+	static final Logger LOGGER = LoggerFactory.getLogger(CompressionUtils.class);
+
+	/**
+	 * Log an error message and exception.
+	 * 
+	 * @param t
+	 * @param repository
+	 *            if repository is not null it MUST be the {0} parameter in the
+	 *            pattern.
+	 * @param pattern
+	 * @param objects
+	 */
+	private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
+		List<Object> parameters = new ArrayList<Object>();
+		if (objects != null && objects.length > 0) {
+			for (Object o : objects) {
+				parameters.add(o);
+			}
+		}
+		if (repository != null) {
+			parameters.add(0, repository.getDirectory().getAbsolutePath());
+		}
+		LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
+	}
+
+	/**
+	 * Zips the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean zip(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		RevCommit commit = JGitUtils.getCommit(repository, objectId);
+		if (commit == null) {
+			return false;
+		}
+		boolean success = false;
+		RevWalk rw = new RevWalk(repository);
+		TreeWalk tw = new TreeWalk(repository);
+		try {
+			tw.addTree(commit.getTree());
+			ZipOutputStream zos = new ZipOutputStream(os);
+			zos.setComment("Generated by Gitblit");
+			if (!StringUtils.isEmpty(basePath)) {
+				PathFilter f = PathFilter.create(basePath);
+				tw.setFilter(f);
+			}
+			tw.setRecursive(true);
+			while (tw.next()) {
+				if (tw.getFileMode(0) == FileMode.GITLINK) {
+					continue;
+				}
+				ZipEntry entry = new ZipEntry(tw.getPathString());
+				entry.setSize(tw.getObjectReader().getObjectSize(tw.getObjectId(0),
+						Constants.OBJ_BLOB));
+				entry.setComment(commit.getName());
+				zos.putNextEntry(entry);
+
+				ObjectId entid = tw.getObjectId(0);
+				FileMode entmode = tw.getFileMode(0);
+				RevBlob blob = (RevBlob) rw.lookupAny(entid, entmode.getObjectType());
+				rw.parseBody(blob);
+
+				ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
+				byte[] tmp = new byte[4096];
+				InputStream in = ldr.openStream();
+				int n;
+				while ((n = in.read(tmp)) > 0) {
+					zos.write(tmp, 0, n);
+				}
+				in.close();
+			}
+			zos.finish();
+			success = true;
+		} catch (IOException e) {
+			error(e, repository, "{0} failed to zip files from commit {1}", commit.getName());
+		} finally {
+			tw.release();
+			rw.dispose();
+		}
+		return success;
+	}
+	
+	/**
+	 * tar the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean tar(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		return tar(null, repository, basePath, objectId, os);
+	}
+	
+	/**
+	 * tar.gz the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean gz(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		return tar(CompressorStreamFactory.GZIP, repository, basePath, objectId, os);
+	}
+	
+	/**
+	 * tar.xz the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean xz(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		return tar(CompressorStreamFactory.XZ, repository, basePath, objectId, os);
+	}
+	
+	/**
+	 * tar.bzip2 the contents of the tree at the (optionally) specified revision and
+	 * the (optionally) specified basepath to the supplied outputstream.
+	 * 
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	public static boolean bzip2(Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		
+		return tar(CompressorStreamFactory.BZIP2, repository, basePath, objectId, os);
+	}
+	
+	/**
+	 * Compresses/archives the contents of the tree at the (optionally)
+	 * specified revision and the (optionally) specified basepath to the
+	 * supplied outputstream.
+	 * 
+	 * @param algorithm
+	 *            compression algorithm for tar (optional)
+	 * @param repository
+	 * @param basePath
+	 *            if unspecified, entire repository is assumed.
+	 * @param objectId
+	 *            if unspecified, HEAD is assumed.
+	 * @param os
+	 * @return true if repository was successfully zipped to supplied output
+	 *         stream
+	 */
+	private static boolean tar(String algorithm, Repository repository, String basePath, String objectId,
+			OutputStream os) {
+		RevCommit commit = JGitUtils.getCommit(repository, objectId);
+		if (commit == null) {
+			return false;
+		}
+		
+		OutputStream cos = os;
+		if (!StringUtils.isEmpty(algorithm)) {
+			try {
+				cos = new CompressorStreamFactory().createCompressorOutputStream(algorithm, os);
+			} catch (CompressorException e1) {
+				error(e1, repository, "{0} failed to open {1} stream", algorithm);
+			}
+		}
+		boolean success = false;
+		RevWalk rw = new RevWalk(repository);
+		TreeWalk tw = new TreeWalk(repository);
+		try {
+			tw.addTree(commit.getTree());
+			TarArchiveOutputStream tos = new TarArchiveOutputStream(cos);
+			tos.setAddPaxHeadersForNonAsciiNames(true);
+			tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
+			if (!StringUtils.isEmpty(basePath)) {
+				PathFilter f = PathFilter.create(basePath);
+				tw.setFilter(f);
+			}
+			tw.setRecursive(true);
+			while (tw.next()) {
+				FileMode mode = tw.getFileMode(0);
+				if (mode == FileMode.GITLINK) {
+					continue;
+				}
+				ObjectId id = tw.getObjectId(0);
+				
+				// new entry
+				TarArchiveEntry entry = new TarArchiveEntry(tw.getPathString());
+				entry.setSize(tw.getObjectReader().getObjectSize(id, Constants.OBJ_BLOB));
+				
+				if (FileMode.SYMLINK.equals(mode)) {
+					// symlink
+					entry.setMode(mode.getBits());
+					
+					// read the symlink target
+					ByteArrayOutputStream bs = new ByteArrayOutputStream();
+					RevBlob blob = (RevBlob) rw.lookupAny(id, mode.getObjectType());
+					rw.parseBody(blob);				
+					ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
+					IOUtils.copy(ldr.openStream(), bs);
+					entry.setLinkName(bs.toString("UTF-8"));
+				} else {
+					// regular file or executable file
+					entry.setMode(mode.getBits());
+				}				
+				entry.setModTime(commit.getAuthorIdent().getWhen());
+
+				tos.putArchiveEntry(entry);
+				
+				if (!FileMode.SYMLINK.equals(mode)) {
+					// write the blob
+					RevBlob blob = (RevBlob) rw.lookupAny(id, mode.getObjectType());
+					rw.parseBody(blob);				
+					ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
+					IOUtils.copy(ldr.openStream(), tos);
+				}
+				
+				// close entry
+				tos.closeArchiveEntry();
+			}
+			tos.finish();
+			tos.close();
+			cos.close();
+			success = true;
+		} catch (IOException e) {
+			error(e, repository, "{0} failed to {1} stream files from commit {2}", algorithm, commit.getName());
+		} finally {
+			tw.release();
+			rw.dispose();
+		}
+		return success;
+	}
+}
diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java
index bc44f00..9cfb37f 100644
--- a/src/com/gitblit/utils/JGitUtils.java
+++ b/src/com/gitblit/utils/JGitUtils.java
@@ -19,7 +19,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -30,8 +29,6 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.regex.Pattern;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
 
 import org.eclipse.jgit.api.CloneCommand;
 import org.eclipse.jgit.api.FetchCommand;
@@ -1720,72 +1717,6 @@
 			}
 		} catch (Throwable t) {
 			error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName);
-		}
-		return success;
-	}
-
-	/**
-	 * Zips the contents of the tree at the (optionally) specified revision and
-	 * the (optionally) specified basepath to the supplied outputstream.
-	 * 
-	 * @param repository
-	 * @param basePath
-	 *            if unspecified, entire repository is assumed.
-	 * @param objectId
-	 *            if unspecified, HEAD is assumed.
-	 * @param os
-	 * @return true if repository was successfully zipped to supplied output
-	 *         stream
-	 */
-	public static boolean zip(Repository repository, String basePath, String objectId,
-			OutputStream os) {
-		RevCommit commit = getCommit(repository, objectId);
-		if (commit == null) {
-			return false;
-		}
-		boolean success = false;
-		RevWalk rw = new RevWalk(repository);
-		TreeWalk tw = new TreeWalk(repository);
-		try {
-			tw.addTree(commit.getTree());
-			ZipOutputStream zos = new ZipOutputStream(os);
-			zos.setComment("Generated by Gitblit");
-			if (!StringUtils.isEmpty(basePath)) {
-				PathFilter f = PathFilter.create(basePath);
-				tw.setFilter(f);
-			}
-			tw.setRecursive(true);
-			while (tw.next()) {
-				if (tw.getFileMode(0) == FileMode.GITLINK) {
-					continue;
-				}
-				ZipEntry entry = new ZipEntry(tw.getPathString());
-				entry.setSize(tw.getObjectReader().getObjectSize(tw.getObjectId(0),
-						Constants.OBJ_BLOB));
-				entry.setComment(commit.getName());
-				zos.putNextEntry(entry);
-
-				ObjectId entid = tw.getObjectId(0);
-				FileMode entmode = tw.getFileMode(0);
-				RevBlob blob = (RevBlob) rw.lookupAny(entid, entmode.getObjectType());
-				rw.parseBody(blob);
-
-				ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
-				byte[] tmp = new byte[4096];
-				InputStream in = ldr.openStream();
-				int n;
-				while ((n = in.read(tmp)) > 0) {
-					zos.write(tmp, 0, n);
-				}
-				in.close();
-			}
-			zos.finish();
-			success = true;
-		} catch (IOException e) {
-			error(e, repository, "{0} failed to zip files from commit {1}", commit.getName());
-		} finally {
-			tw.release();
-			rw.dispose();
 		}
 		return success;
 	}
diff --git a/src/com/gitblit/wicket/pages/CommitPage.html b/src/com/gitblit/wicket/pages/CommitPage.html
index 20e6b60..79a038c 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.html
+++ b/src/com/gitblit/wicket/pages/CommitPage.html
@@ -30,7 +30,7 @@
 		<tr class="hidden-phone"><th><wicket:message key="gb.tree">tree</wicket:message></th>
 			<td><span class="sha1" wicket:id="commitTree">[commit tree]</span>
 				<span class="link">
-					<a wicket:id="treeLink"><wicket:message key="gb.tree"></wicket:message></a> | <a wicket:id="zipLink"><wicket:message key="gb.zip"></wicket:message></a>
+					<a wicket:id="treeLink"><wicket:message key="gb.tree"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
 				</span>
 			</td></tr>
 		<tr class="hidden-phone"><th valign="top"><wicket:message key="gb.parent">parent</wicket:message></th>
diff --git a/src/com/gitblit/wicket/pages/CommitPage.java b/src/com/gitblit/wicket/pages/CommitPage.java
index 7bc6b41..b2a8112 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/com/gitblit/wicket/pages/CommitPage.java
@@ -22,7 +22,6 @@
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
-import org.apache.wicket.markup.html.link.ExternalLink;
 import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.markup.repeater.data.DataView;
 import org.apache.wicket.markup.repeater.data.ListDataProvider;
@@ -32,16 +31,15 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import com.gitblit.Constants;
-import com.gitblit.DownloadZipServlet;
 import com.gitblit.GitBlit;
-import com.gitblit.Keys;
 import com.gitblit.models.GitNote;
-import com.gitblit.models.SubmoduleModel;
 import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.SubmoduleModel;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.CommitHeaderPanel;
 import com.gitblit.wicket.panels.CommitLegendPanel;
+import com.gitblit.wicket.panels.CompressedDownloadsPanel;
 import com.gitblit.wicket.panels.GravatarImage;
 import com.gitblit.wicket.panels.LinkPanel;
 import com.gitblit.wicket.panels.RefsPanel;
@@ -95,8 +93,8 @@
 				newCommitParameter()));
 		add(new BookmarkablePageLink<Void>("treeLink", TreePage.class, newCommitParameter()));
 		final String baseUrl = WicketUtils.getGitblitURL(getRequest());
-		add(new ExternalLink("zipLink", DownloadZipServlet.asLink(baseUrl, repositoryName,
-				objectId, null)).setVisible(GitBlit.getBoolean(Keys.web.allowZipDownloads, true)));
+		
+		add(new CompressedDownloadsPanel("compressedLinks", baseUrl, repositoryName, objectId, null));
 
 		// Parent Commits
 		ListDataProvider<String> parentsDp = new ListDataProvider<String>(parents);
diff --git a/src/com/gitblit/wicket/pages/TreePage.html b/src/com/gitblit/wicket/pages/TreePage.html
index 0047ff0..b7e55ed 100644
--- a/src/com/gitblit/wicket/pages/TreePage.html
+++ b/src/com/gitblit/wicket/pages/TreePage.html
@@ -9,7 +9,7 @@
 
 	<!-- blob nav links -->	
 	<div class="page_nav2">
-		<a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="headLink"><wicket:message key="gb.head"></wicket:message></a> | <a wicket:id="zipLink"><wicket:message key="gb.zip"></wicket:message></a>
+		<a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="headLink"><wicket:message key="gb.head"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
 	</div>	
 	
 	<!-- commit header -->
@@ -32,14 +32,14 @@
 	<!--  submodule links -->
 	<wicket:fragment wicket:id="submoduleLinks">
 		<span class="link">
-			<a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="zip"><wicket:message key="gb.zip"></wicket:message></a>
+			<a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
 		</span>
 	</wicket:fragment>
 
 	<!--  tree links -->
 	<wicket:fragment wicket:id="treeLinks">
 		<span class="link">
-			<span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="zip"><wicket:message key="gb.zip"></wicket:message></a>
+			<span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
 		</span>
 	</wicket:fragment>
 	
diff --git a/src/com/gitblit/wicket/pages/TreePage.java b/src/com/gitblit/wicket/pages/TreePage.java
index 973634b..345814f 100644
--- a/src/com/gitblit/wicket/pages/TreePage.java
+++ b/src/com/gitblit/wicket/pages/TreePage.java
@@ -20,7 +20,6 @@
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
-import org.apache.wicket.markup.html.link.ExternalLink;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.markup.repeater.data.DataView;
@@ -30,15 +29,13 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 
-import com.gitblit.DownloadZipServlet;
-import com.gitblit.GitBlit;
-import com.gitblit.Keys;
 import com.gitblit.models.PathModel;
 import com.gitblit.models.SubmoduleModel;
 import com.gitblit.utils.ByteFormat;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.CommitHeaderPanel;
+import com.gitblit.wicket.panels.CompressedDownloadsPanel;
 import com.gitblit.wicket.panels.LinkPanel;
 import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
 
@@ -58,9 +55,8 @@
 				WicketUtils.newPathParameter(repositoryName, objectId, path)));
 		add(new BookmarkablePageLink<Void>("headLink", TreePage.class,
 				WicketUtils.newPathParameter(repositoryName, Constants.HEAD, path)));
-		add(new ExternalLink("zipLink", DownloadZipServlet.asLink(getRequest()
-				.getRelativePathPrefixToContextRoot(), repositoryName, objectId, path))
-				.setVisible(GitBlit.getBoolean(Keys.web.allowZipDownloads, true)));
+		add(new CompressedDownloadsPanel("compressedLinks", getRequest()
+				.getRelativePathPrefixToContextRoot(), repositoryName, objectId, path));
 
 		add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
 
@@ -114,10 +110,10 @@
 										entry.path)));
 						links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
 								WicketUtils.newPathParameter(repositoryName, entry.commitId,
-										entry.path)));
-						links.add(new ExternalLink("zip", DownloadZipServlet.asLink(baseUrl,
-								repositoryName, objectId, entry.path)).setVisible(GitBlit
-								.getBoolean(Keys.web.allowZipDownloads, true)));
+										entry.path)));						
+						links.add(new CompressedDownloadsPanel("compressedLinks", baseUrl,
+								repositoryName, objectId, entry.path));
+
 						item.add(links);
 					} else if (entry.isSubmodule()) {
 						// submodule
@@ -143,9 +139,8 @@
 						links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
 								WicketUtils.newPathParameter(submodulePath, submoduleId,
 										"")).setEnabled(hasSubmodule));
-						links.add(new ExternalLink("zip", DownloadZipServlet.asLink(baseUrl,
-								submodulePath, submoduleId, "")).setVisible(GitBlit
-								.getBoolean(Keys.web.allowZipDownloads, true)).setEnabled(hasSubmodule));
+						links.add(new CompressedDownloadsPanel("compressedLinks", baseUrl,
+								submodulePath, submoduleId, "").setEnabled(hasSubmodule));
 						item.add(links);						
 					} else {
 						// blob link
diff --git a/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html b/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html
new file mode 100644
index 0000000..7123d0a
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<wicket:panel>
+	<span wicket:id="compressedLinks">
+		<span wicket:id="linkSep">|</span><span wicket:id="compressedLink">ref</span>
+	</span>	
+</wicket:panel>
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java b/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java
new file mode 100644
index 0000000..b22c758
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/CompressedDownloadsPanel.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.DownloadZipServlet;
+import com.gitblit.DownloadZipServlet.Format;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+
+public class CompressedDownloadsPanel extends Panel {
+
+	private static final long serialVersionUID = 1L;
+
+	public CompressedDownloadsPanel(String id, final String baseUrl, final String repositoryName, final String objectId, final String path) {
+		super(id);
+		
+		List<String> types = GitBlit.getStrings(Keys.web.compressedDownloads);
+		if (types.isEmpty()) {
+			types.add(Format.zip.name());
+			types.add(Format.gz.name());
+		}
+		
+		ListDataProvider<String> refsDp = new ListDataProvider<String>(types);
+		DataView<String> refsView = new DataView<String>("compressedLinks", refsDp) {
+			private static final long serialVersionUID = 1L;
+			int counter;
+
+			@Override
+			protected void onBeforeRender() {
+				super.onBeforeRender();
+				counter = 0;
+			}
+			
+			@Override
+			public void populateItem(final Item<String> item) {
+				String compressionType = item.getModelObject();
+				Format format = Format.fromName(compressionType);
+				
+				String href = DownloadZipServlet.asLink(baseUrl, repositoryName,
+						objectId, path, format);
+				Component c = new LinkPanel("compressedLink", null, format.name(), href);
+				item.add(c);
+				Label lb = new Label("linkSep", "|");
+				lb.setVisible(counter > 0);
+				lb.setRenderBodyOnly(true);
+				item.add(lb.setEscapeModelStrings(false));
+				item.setRenderBodyOnly(true);
+				counter++;
+			}
+		};
+		add(refsView);
+		
+		setVisible(GitBlit.getBoolean(Keys.web.allowZipDownloads, true));
+	}
+}
\ No newline at end of file
diff --git a/tests/com/gitblit/tests/JGitUtilsTest.java b/tests/com/gitblit/tests/JGitUtilsTest.java
index 7e4d630..ce72a46 100644
--- a/tests/com/gitblit/tests/JGitUtilsTest.java
+++ b/tests/com/gitblit/tests/JGitUtilsTest.java
@@ -50,6 +50,7 @@
 import com.gitblit.models.PathModel;
 import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.models.RefModel;
+import com.gitblit.utils.CompressionUtils;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.StringUtils;
 
@@ -446,16 +447,16 @@
 
 	@Test
 	public void testZip() throws Exception {
-		assertFalse(JGitUtils.zip(null, null, null, null));
+		assertFalse(CompressionUtils.zip(null, null, null, null));
 		Repository repository = GitBlitSuite.getHelloworldRepository();
 		File zipFileA = new File(GitBlitSuite.REPOSITORIES, "helloworld.zip");
 		FileOutputStream fosA = new FileOutputStream(zipFileA);
-		boolean successA = JGitUtils.zip(repository, null, Constants.HEAD, fosA);
+		boolean successA = CompressionUtils.zip(repository, null, Constants.HEAD, fosA);
 		fosA.close();
 
 		File zipFileB = new File(GitBlitSuite.REPOSITORIES, "helloworld-java.zip");
 		FileOutputStream fosB = new FileOutputStream(zipFileB);
-		boolean successB = JGitUtils.zip(repository, "java.java", Constants.HEAD, fosB);
+		boolean successB = CompressionUtils.zip(repository, "java.java", Constants.HEAD, fosB);
 		fosB.close();
 
 		repository.close();

--
Gitblit v1.9.1