Download zip feature.
diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index b263f32..e2ddce6 100644
--- a/distrib/gitblit.properties
+++ b/distrib/gitblit.properties
@@ -55,6 +55,9 @@
 # If web.authenticate=false, any user can execute the aforementioned functions.  

 web.allowAdministration = true

 

+# Allow dyanamic zip downloads.   

+web.allowZipDownloads = true

+

 # This is the message display above the repositories table.

 # This can point to a file with Markdown content.

 # Specifying "gitblit" uses the internal welcome message.

diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index 46f3208..b84ab7d 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -17,6 +17,10 @@
 	public final static String ADMIN_ROLE = "#admin";

 

 	public final static String PROPERTIES_FILE = "gitblit.properties";

+	

+	public final static String GIT_SERVLET_PATH = "/git/";

+	

+	public final static String ZIP_SERVLET_PATH = "/zip/";

 

 	public static enum AccessRestrictionType {

 		NONE, PUSH, CLONE, VIEW;

diff --git a/src/com/gitblit/DownloadZipServlet.java b/src/com/gitblit/DownloadZipServlet.java
new file mode 100644
index 0000000..87fda90
--- /dev/null
+++ b/src/com/gitblit/DownloadZipServlet.java
@@ -0,0 +1,100 @@
+package com.gitblit;

+

+import java.util.Date;

+

+import javax.servlet.http.HttpServlet;

+import javax.servlet.http.HttpServletResponse;

+

+import org.eclipse.jgit.lib.Repository;

+import org.eclipse.jgit.revwalk.RevCommit;

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+

+import com.gitblit.Constants.AccessRestrictionType;

+import com.gitblit.utils.JGitUtils;

+import com.gitblit.utils.StringUtils;

+import com.gitblit.wicket.models.RepositoryModel;

+

+public class DownloadZipServlet extends HttpServlet {

+

+	public static String asLink(String baseURL, String repository, String objectId, String path) {

+		return baseURL + (baseURL.endsWith("/") ? "" : "/") + "zip?r=" + repository + (path == null ? "" : ("&p=" + path)) + (objectId == null ? "" : ("&h=" + objectId));

+	}

+

+	private static final long serialVersionUID = 1L;

+

+	private final static Logger logger = LoggerFactory.getLogger(DownloadZipServlet.class);

+

+	public DownloadZipServlet() {

+		super();

+	}

+

+	@Override

+	protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, java.io.IOException {

+		processRequest(request, response);

+	}

+

+	@Override

+	protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, java.io.IOException {

+		processRequest(request, response);

+	}

+

+	private void processRequest(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, java.io.IOException {

+		if (!GitBlit.self().settings().getBoolean(Keys.web.allowZipDownloads, true)) {

+			logger.warn("Zip downloads are disabled");

+			response.sendError(HttpServletResponse.SC_FORBIDDEN);

+			return;

+

+		}

+		String repository = request.getParameter("r");

+		String basePath = request.getParameter("p");

+		String objectId = request.getParameter("h");

+

+		try {

+			String name = repository;

+			if (name.indexOf('/') > -1) {

+				name = name.substring(name.lastIndexOf('/') + 1);

+			}

+

+			// check roles first

+			boolean authorized = request.isUserInRole(Constants.ADMIN_ROLE);

+			authorized |= request.isUserInRole(repository);

+

+			if (!authorized) {

+				RepositoryModel model = GitBlit.self().getRepositoryModel(repository);

+				if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {

+					logger.warn("Unauthorized access via zip servlet for " + model.name);

+					response.sendError(HttpServletResponse.SC_FORBIDDEN);

+					return;

+				}

+			}

+			if (!StringUtils.isEmpty(basePath)) {

+				name += "-" + basePath.replace('/', '_');

+			}

+			if (!StringUtils.isEmpty(objectId)) {

+				name += "-" + objectId;

+			}

+

+			Repository r = GitBlit.self().getRepository(repository);

+			RevCommit commit = JGitUtils.getCommit(r, objectId);

+			Date date = JGitUtils.getCommitDate(commit);

+			String contentType = "application/octet-stream";

+			response.setContentType(contentType + "; charset=" + response.getCharacterEncoding());

+			// response.setContentLength(attachment.getFileSize());

+			response.setHeader("Content-Disposition", "attachment; filename=\"" + name + ".zip" + "\"");

+			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());

+				response.flushBuffer();

+			} catch (Throwable t) {

+				logger.error("Failed to write attachment to client", t);

+			}

+		} catch (Throwable t) {

+			logger.error("Failed to write attachment to client", t);

+		}

+	}

+}

diff --git a/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java
index e9e4463..a7b1538 100644
--- a/src/com/gitblit/GitBlitServer.java
+++ b/src/com/gitblit/GitBlitServer.java
@@ -211,10 +211,13 @@
 		wicketFilter.setInitParameter(WicketFilter.FILTER_MAPPING_PARAM, wicketPathSpec);

 		wicketFilter.setInitParameter(WicketFilter.IGNORE_PATHS_PARAM, "git/");

 		rootContext.addFilter(wicketFilter, wicketPathSpec, FilterMapping.DEFAULT);

-

+		

+		// Zip Servlet

+		rootContext.addServlet(DownloadZipServlet.class, Constants.ZIP_SERVLET_PATH + "*");

+		

 		// Git Servlet

 		ServletHolder gitServlet = null;

-		String gitServletPathSpec = "/git/*";

+		String gitServletPathSpec = Constants.GIT_SERVLET_PATH + "*";

 		if (fileSettings.getBoolean(Keys.git.enableGitServlet, true)) {

 			gitServlet = rootContext.addServlet(GitBlitServlet.class, gitServletPathSpec);

 			gitServlet.setInitParameter("base-path", params.repositoriesFolder);

diff --git a/src/com/gitblit/tests/JGitUtilsTest.java b/src/com/gitblit/tests/JGitUtilsTest.java
index 196058c..11b7712 100644
--- a/src/com/gitblit/tests/JGitUtilsTest.java
+++ b/src/com/gitblit/tests/JGitUtilsTest.java
@@ -1,6 +1,7 @@
 package com.gitblit.tests;

 

 import java.io.File;

+import java.io.FileOutputStream;

 import java.util.Date;

 import java.util.List;

 

@@ -103,5 +104,27 @@
 		r.close();

 		System.out.println(diff);

 	}

+	

+	public void testZip() throws Exception {

+		Repository r = new FileRepository(new File(repositoriesFolder, "gitblit.git/" + Constants.DOT_GIT));

+		FileOutputStream fos = null;

+		try {

+			File zipFile = new File("c:/output.zip");

+			zipFile.delete();

+			fos = new FileOutputStream(zipFile);

+			if (JGitUtils.zip(r, "src", Constants.HEAD, fos)) {

+				System.out.println("zip = " + zipFile.length() + " bytes");

+			} else {

+				System.err.println("failed to generate zip file?!");

+			}

+		} finally {

+			if (fos != null) {

+				try {

+					fos.close();

+				} catch (Throwable t) {

+				}

+			}

+		}

+	}

 

 }

diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java
index c9c13c5..b153c0c 100644
--- a/src/com/gitblit/utils/JGitUtils.java
+++ b/src/com/gitblit/utils/JGitUtils.java
@@ -4,6 +4,7 @@
 import java.io.File;

 import java.io.IOException;

 import java.io.InputStream;

+import java.io.OutputStream;

 import java.nio.charset.Charset;

 import java.text.DateFormat;

 import java.text.ParseException;

@@ -17,6 +18,8 @@
 import java.util.Map;

 import java.util.Set;

 import java.util.concurrent.atomic.AtomicInteger;

+import java.util.zip.ZipEntry;

+import java.util.zip.ZipOutputStream;

 

 import org.eclipse.jgit.api.Git;

 import org.eclipse.jgit.diff.DiffEntry;

@@ -808,10 +811,57 @@
 		return null;

 	}

 

+	public static boolean zip(Repository r, String basePath, String objectId, OutputStream os) throws Exception {

+		RevCommit commit = getCommit(r, objectId);

+		if (commit == null) {

+			return false;

+		}

+		final RevWalk rw = new RevWalk(r);

+		final TreeWalk walk = new TreeWalk(r);

+		try {

+			walk.addTree(commit.getTree());

+			ZipOutputStream zos = new ZipOutputStream(os);

+			zos.setComment("Generated by Git:Blit");

+			if (basePath != null && basePath.length() > 0) {

+				PathFilter f = PathFilter.create(basePath);

+				walk.setFilter(f);

+			}

+			walk.setRecursive(true);

+			while (walk.next()) {

+				ZipEntry entry = new ZipEntry(walk.getPathString());

+				entry.setSize(walk.getObjectReader().getObjectSize(walk.getObjectId(0), Constants.OBJ_BLOB));

+				entry.setComment(commit.getName());

+				zos.putNextEntry(entry);

+

+				ObjectId entid = walk.getObjectId(0);

+				FileMode entmode = walk.getFileMode(0);

+				RevBlob blob = (RevBlob) rw.lookupAny(entid, entmode.getObjectType());

+				rw.parseBody(blob);

+

+				ObjectLoader ldr = r.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();

+			return true;

+		} catch (IOException e) {

+			LOGGER.error("Failed to zip files from commit " + commit.getName(), e);

+		} finally {

+			walk.release();

+			rw.dispose();

+		}

+		return false;

+	}

+

 	public static List<Metric> getDateMetrics(Repository r) {

 		Metric total = new Metric("TOTAL");

 		final Map<String, Metric> metricMap = new HashMap<String, Metric>();

-		

+

 		if (hasCommits(r)) {

 			final List<RefModel> tags = getTags(r, -1);

 			final Map<ObjectId, RefModel> tagMap = new HashMap<ObjectId, RefModel>();

diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index b6dbc11..0c4c350 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -92,4 +92,5 @@
 gb.canAdminDescription = can administer Git:Blit server

 gb.permittedUsers = permitted users

 gb.isFrozen = is frozen

-gb.isFrozenDescription = deny push operations
\ No newline at end of file
+gb.isFrozenDescription = deny push operations

+gb.zip = zip
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/CommitPage.html b/src/com/gitblit/wicket/pages/CommitPage.html
index 059d77e..1b5fffe 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.html
+++ b/src/com/gitblit/wicket/pages/CommitPage.html
@@ -23,7 +23,12 @@
 		<tr><th><wicket:message key="gb.committer">committer</wicket:message></th><td><span class="sha1" wicket:id="commitCommitter">[committer]</span></td></tr>

 		<tr><th></th><td><span class="sha1" wicket:id="commitCommitterDate">[commit date]</span></td></tr>

 		<tr><th><wicket:message key="gb.commit">commit</wicket:message></th><td><span class="sha1" wicket:id="commitId">[commit id]</span></td></tr>

-		<tr><th><wicket:message key="gb.tree">tree</wicket:message></th><td><span class="sha1" wicket:id="commitTree">[commit tree]</span></td></tr>

+		<tr><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>

+				</span>

+			</td></tr>

 		<tr><th valign="top"><wicket:message key="gb.parent">parent</wicket:message></th>

 			<td>

 				<span wicket:id="commitParents">

diff --git a/src/com/gitblit/wicket/pages/CommitPage.java b/src/com/gitblit/wicket/pages/CommitPage.java
index 5396e82..c3c3fa9 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/com/gitblit/wicket/pages/CommitPage.java
@@ -6,6 +6,7 @@
 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;

@@ -13,6 +14,9 @@
 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.utils.JGitUtils;

 import com.gitblit.utils.JGitUtils.SearchType;

 import com.gitblit.wicket.LinkPanel;

@@ -62,6 +66,8 @@
 		add(new Label("commitId", c.getName()));

 

 		add(new LinkPanel("commitTree", "list", c.getTree().getName(), TreePage.class, newCommitParameter()));

+		add(new BookmarkablePageLink<Void>("treeLink", TreePage.class, newCommitParameter()));

+		add(new ExternalLink("zipLink", DownloadZipServlet.asLink(getRequest().getRelativePathPrefixToContextRoot(), repositoryName, objectId, null)).setVisible(GitBlit.self().settings().getBoolean(Keys.web.allowZipDownloads, true)));

 

 		// 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 35eac29..8d706ee 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="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>

 	</div>	

 	

 	<!-- commit header -->

@@ -32,7 +32,7 @@
 	<!--  tree links -->

 	<wicket:fragment wicket:id="treeLinks">

 		<span class="link">

-			<a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>

+			<a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | <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>

 	</wicket:fragment>

 	

diff --git a/src/com/gitblit/wicket/pages/TreePage.java b/src/com/gitblit/wicket/pages/TreePage.java
index ea5bd53..e385fab 100644
--- a/src/com/gitblit/wicket/pages/TreePage.java
+++ b/src/com/gitblit/wicket/pages/TreePage.java
@@ -5,6 +5,7 @@
 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;

@@ -13,6 +14,9 @@
 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.utils.ByteFormat;

 import com.gitblit.utils.JGitUtils;

 import com.gitblit.wicket.LinkPanel;

@@ -36,6 +40,7 @@
 		// tree page links

 		add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class, 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.self().settings().getBoolean(Keys.web.allowZipDownloads, true)));

 

 		add(new CommitHeaderPanel("commitHeader", repositoryName, commit));

 

@@ -73,6 +78,7 @@
 						Fragment links = new Fragment("pathLinks", "treeLinks", this);

 						links.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils.newPathParameter(repositoryName, entry.commitId, entry.path)));

 						links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils.newPathParameter(repositoryName, entry.commitId, entry.path)));

+						links.add(new ExternalLink("zip", DownloadZipServlet.asLink(getRequest().getRelativePathPrefixToContextRoot(), repositoryName, objectId, entry.path)).setVisible(GitBlit.self().settings().getBoolean(Keys.web.allowZipDownloads, true)));

 						item.add(links);

 					} else {

 						// blob link

diff --git a/src/com/gitblit/wicket/resources/gitblit.css b/src/com/gitblit/wicket/resources/gitblit.css
index 4a971a6..64484d2 100644
--- a/src/com/gitblit/wicket/resources/gitblit.css
+++ b/src/com/gitblit/wicket/resources/gitblit.css
@@ -114,15 +114,19 @@
 	color: #008000;

 }

 

+span.link {

+	color: #888;

+}

+

 span.link, span.link a {

 	font-family: sans-serif;

-	font-size: 11px;

+	font-size: 11px;	

 }

 

 span.link em, div.link span em {

 	font-style: normal;

 	font-family: sans-serif;

-	font-size: 11px;

+	font-size: 11px;	

 }

 

 div.page_header {