From 252dc07d7f85cc344b5919bb7c6166ef84b2102e Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gmail.com>
Date: Mon, 25 Jan 2016 11:55:58 -0500
Subject: [PATCH] Merge pull request #988 from gitblit/976-raw-download-filestore-item

---
 src/main/java/com/gitblit/wicket/pages/TreePage.java           |   44 +++-
 src/main/java/com/gitblit/models/PathModel.java                |   56 ++++
 src/main/java/com/gitblit/Constants.java                       |    4 
 src/test/java/com/gitblit/tests/JGitUtilsTest.java             |    6 
 src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java      |    5 
 src/main/java/com/gitblit/wicket/panels/HistoryPanel.java      |    2 
 src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java |    7 
 src/main/java/com/gitblit/wicket/pages/BlamePage.java          |   50 +++-
 src/main/java/com/gitblit/utils/CompressionUtils.java          |   92 +++++++-
 src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java     |   31 ++
 src/main/java/com/gitblit/servlet/GitFilter.java               |   14 
 src/main/java/com/gitblit/wicket/pages/CommitPage.html         |    3 
 src/main/java/com/gitblit/wicket/pages/TreePage.html           |    3 
 src/main/java/com/gitblit/utils/JGitUtils.java                 |  130 ++++++++++--
 src/main/java/com/gitblit/wicket/pages/FilestorePage.java      |    1 
 src/main/java/com/gitblit/models/FilestoreModel.java           |   67 ++++++
 src/main/java/com/gitblit/servlet/FilestoreServlet.java        |    5 
 src/main/java/com/gitblit/utils/DiffStatFormatter.java         |    5 
 src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html     |    1 
 src/main/java/com/gitblit/servlet/RawServlet.java              |    2 
 src/main/resources/gitblit.css                                 |    6 
 src/main/java/com/gitblit/wicket/pages/CommitPage.java         |   36 ++
 src/main/java/com/gitblit/utils/DiffUtils.java                 |   11 
 src/main/java/com/gitblit/servlet/DownloadZipServlet.java      |   18 +
 24 files changed, 468 insertions(+), 131 deletions(-)

diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 0a99953..6232552 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -94,6 +94,10 @@
 	public static final int LEN_SHORTLOG = 78;
 
 	public static final int LEN_SHORTLOG_REFS = 60;
+	
+	public static final int LEN_FILESTORE_META_MIN = 125;
+	
+	public static final int LEN_FILESTORE_META_MAX = 146;
 
 	public static final String DEFAULT_BRANCH = "default";
 
diff --git a/src/main/java/com/gitblit/models/FilestoreModel.java b/src/main/java/com/gitblit/models/FilestoreModel.java
index 4144df6..2ed1ede 100644
--- a/src/main/java/com/gitblit/models/FilestoreModel.java
+++ b/src/main/java/com/gitblit/models/FilestoreModel.java
@@ -20,6 +20,10 @@
 import java.util.Date;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.gitblit.Constants;
 
 /**
  * A FilestoreModel represents a file stored outside a repository but referenced by the repository using a unique objectID
@@ -31,6 +35,18 @@
 
 	private static final long serialVersionUID = 1L;
 
+	private static final String metaRegexText = new StringBuilder()
+			.append("version\\shttps://git-lfs.github.com/spec/v1\\s+")
+			.append("oid\\ssha256:(" + Constants.REGEX_SHA256 + ")\\s+")
+			.append("size\\s([0-9]+)")
+			.toString();
+	
+	private static final Pattern metaRegex = Pattern.compile(metaRegexText);
+	
+	private static final int metaRegexIndexSHA = 1;
+	
+	private static final int metaRegexIndexSize = 2;
+	
 	public final String oid;
 	
 	private Long size;
@@ -43,6 +59,12 @@
 	//Access Control
 	private List<String> repositories;
 	
+	public FilestoreModel(String id, long definedSize) {
+		oid = id;
+		size = definedSize;
+		status = Status.ReferenceOnly;
+	}
+	
 	public FilestoreModel(String id, long expectedSize, UserModel user, String repo) {
 		oid = id;
 		size = expectedSize;
@@ -51,6 +73,29 @@
 		stateChangedOn = new Date();
 		repositories = new ArrayList<String>();
 		repositories.add(repo);
+	}
+	
+	/*
+	 * Attempts to create a FilestoreModel from the given meta string
+	 * 
+	 * @return A valid FilestoreModel if successful, otherwise null
+	 */
+	public static FilestoreModel fromMetaString(String meta) {
+		
+		Matcher m = metaRegex.matcher(meta);
+		
+		if (m.find()) {
+			try
+			{
+				final Long size = Long.parseLong(m.group(metaRegexIndexSize));
+				final String sha = m.group(metaRegexIndexSHA);
+				return new FilestoreModel(sha, size);			
+			} catch (Exception e) {
+				//Fail silent - it is not a valid filestore item
+			}
+		}
+
+		return null;
 	}
 	
 	public synchronized long getSize() {
@@ -102,19 +147,25 @@
 	}
 	
 	public synchronized void addRepository(String repo) {
-		if (!repositories.contains(repo)) {
-			repositories.add(repo);
-		}	
+		if (status != Status.ReferenceOnly) {
+			if (!repositories.contains(repo)) {
+				repositories.add(repo);
+			}
+		}
 	}
 	
 	public synchronized void removeRepository(String repo) {
-		repositories.remove(repo);
+		if (status != Status.ReferenceOnly) {
+			repositories.remove(repo);
+		}
 	}
 	
 	public synchronized boolean isInRepositoryList(List<String> repoList) {
-		for (String name : repositories) {
-			if (repoList.contains(name)) {
-				return true;
+		if (status != Status.ReferenceOnly) {
+			for (String name : repositories) {
+				if (repoList.contains(name)) {
+					return true;
+				}
 			}
 		}
 		return false;
@@ -122,6 +173,8 @@
 	
 	public static enum Status {
 
+		ReferenceOnly(-42),
+		
 		Deleted(-30),
 		AuthenticationRequired(-20),
 		
diff --git a/src/main/java/com/gitblit/models/PathModel.java b/src/main/java/com/gitblit/models/PathModel.java
index bf58542..3c280eb 100644
--- a/src/main/java/com/gitblit/models/PathModel.java
+++ b/src/main/java/com/gitblit/models/PathModel.java
@@ -15,11 +15,20 @@
  */
 package com.gitblit.models;
 
+import java.io.IOException;
 import java.io.Serializable;
 
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import com.gitblit.manager.FilestoreManager;
+import com.gitblit.utils.JGitUtils;
 
 /**
  * PathModel is a serializable model class that represents a file or a folder,
@@ -34,16 +43,18 @@
 
 	public final String name;
 	public final String path;
+	private final FilestoreModel filestoreItem;
 	public final long size;
 	public final int mode;
 	public final String objectId;
 	public final String commitId;
 	public boolean isParentPath;
-
-	public PathModel(String name, String path, long size, int mode, String objectId, String commitId) {
+	
+	public PathModel(String name, String path, FilestoreModel filestoreItem, long size, int mode, String objectId, String commitId) {
 		this.name = name;
 		this.path = path;
-		this.size = size;
+		this.filestoreItem = filestoreItem;
+		this.size = (filestoreItem == null) ? size : filestoreItem.getSize();
 		this.mode = mode;
 		this.objectId = objectId;
 		this.commitId = commitId;
@@ -65,6 +76,18 @@
 		return FileMode.REGULAR_FILE.equals(mode)
 				|| FileMode.EXECUTABLE_FILE.equals(mode)
 				|| (FileMode.MISSING.equals(mode) && !isSymlink() && !isSubmodule() && !isTree());
+	}
+	
+	public boolean isFilestoreItem() {
+		return filestoreItem != null;
+	}
+	
+	public String getFilestoreOid() {
+		if (filestoreItem != null) {
+			return filestoreItem.oid;
+		}
+		
+		return null;
 	}
 
 	@Override
@@ -119,9 +142,9 @@
 
 		public int deletions;
 
-		public PathChangeModel(String name, String path, long size, int mode, String objectId,
+		public PathChangeModel(String name, String path, FilestoreModel filestoreItem, long size, int mode, String objectId,
 				String commitId, ChangeType type) {
-			super(name, path, size, mode, objectId, commitId);
+			super(name, path, filestoreItem, size, mode, objectId, commitId);
 			this.changeType = type;
 		}
 
@@ -148,18 +171,33 @@
 			return super.equals(o);
 		}
 
-		public static PathChangeModel from(DiffEntry diff, String commitId) {
+		public static PathChangeModel from(DiffEntry diff, String commitId, Repository repository) {
 			PathChangeModel pcm;
+			FilestoreModel filestoreItem = null;
+			long size = 0;
+
+			if (repository != null) {
+				try (RevWalk revWalk = new RevWalk(repository)) {
+					size = revWalk.getObjectReader().getObjectSize(diff.getNewId().toObjectId(), Constants.OBJ_BLOB);
+	
+					if (JGitUtils.isPossibleFilestoreItem(size)) {
+						filestoreItem = JGitUtils.getFilestoreItem(revWalk.getObjectReader().open(diff.getNewId().toObjectId()));
+					}
+				} catch (Exception e) {
+						e.printStackTrace();
+				}
+			}
+			
 			if (diff.getChangeType().equals(ChangeType.DELETE)) {
-				pcm = new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff
+				pcm = new PathChangeModel(diff.getOldPath(), diff.getOldPath(), filestoreItem, size, diff
 						.getNewMode().getBits(), diff.getOldId().name(), commitId, diff
 						.getChangeType());
 			} else if (diff.getChangeType().equals(ChangeType.RENAME)) {
-				pcm = new PathChangeModel(diff.getOldPath(), diff.getNewPath(), 0, diff
+				pcm = new PathChangeModel(diff.getOldPath(), diff.getNewPath(), filestoreItem, size, diff
 						.getNewMode().getBits(), diff.getNewId().name(), commitId, diff
 						.getChangeType());
 			} else {
-				pcm = new PathChangeModel(diff.getNewPath(), diff.getNewPath(), 0, diff
+				pcm = new PathChangeModel(diff.getNewPath(), diff.getNewPath(), filestoreItem, size, diff
 						.getNewMode().getBits(), diff.getNewId().name(), commitId, diff
 						.getChangeType());
 			}
diff --git a/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java b/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
index b9cb088..e1d76db 100644
--- a/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
+++ b/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
@@ -133,10 +133,11 @@
 	/**
 	 * Allows authentication header to be altered based on the action requested
 	 * Default is WWW-Authenticate
+	 * @param httpRequest
 	 * @param action
 	 * @return authentication type header
 	 */
-	protected String getAuthenticationHeader(String action) {
+	protected String getAuthenticationHeader(HttpServletRequest httpRequest, String action) {
 		return "WWW-Authenticate";
 	}
 	
@@ -192,7 +193,7 @@
 						logger.info(MessageFormat.format("ARF: CREATE CHALLENGE {0}", fullUrl));
 					}
 					
-					httpResponse.setHeader(getAuthenticationHeader(urlRequestType), CHALLENGE);
+					httpResponse.setHeader(getAuthenticationHeader(httpRequest, urlRequestType), CHALLENGE);
 					httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
 					return;
 				} else {
@@ -239,7 +240,7 @@
 				if (runtimeManager.isDebugMode()) {
 					logger.info(MessageFormat.format("ARF: CHALLENGE {0}", fullUrl));
 				}
-				httpResponse.setHeader(getAuthenticationHeader(urlRequestType), CHALLENGE);
+				httpResponse.setHeader(getAuthenticationHeader(httpRequest, urlRequestType), CHALLENGE);
 				httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
 				return;
 			} else {
diff --git a/src/main/java/com/gitblit/servlet/DownloadZipServlet.java b/src/main/java/com/gitblit/servlet/DownloadZipServlet.java
index 0756256..319c4f9 100644
--- a/src/main/java/com/gitblit/servlet/DownloadZipServlet.java
+++ b/src/main/java/com/gitblit/servlet/DownloadZipServlet.java
@@ -22,6 +22,7 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletResponse;
@@ -34,6 +35,7 @@
 import com.gitblit.Constants;
 import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
+import com.gitblit.manager.IFilestoreManager;
 import com.gitblit.manager.IRepositoryManager;
 import com.gitblit.utils.CompressionUtils;
 import com.gitblit.utils.JGitUtils;
@@ -57,6 +59,8 @@
 	private IStoredSettings settings;
 
 	private IRepositoryManager repositoryManager;
+	
+	private IFilestoreManager filestoreManager;
 
 	public static enum Format {
 		zip(".zip"), tar(".tar"), gz(".tar.gz"), xz(".tar.xz"), bzip2(".tar.bzip2");
@@ -78,9 +82,10 @@
 	}
 
 	@Inject
-	public DownloadZipServlet(IStoredSettings settings, IRepositoryManager repositoryManager) {
+	public DownloadZipServlet(IStoredSettings settings, IRepositoryManager repositoryManager, IFilestoreManager filestoreManager) {
 		this.settings = settings;
 		this.repositoryManager = repositoryManager;
+		this.filestoreManager = filestoreManager;
 	}
 
 	/**
@@ -169,22 +174,23 @@
 			response.setHeader("Pragma", "no-cache");
 			response.setDateHeader("Expires", 0);
 
+			
 			try {
 				switch (format) {
 				case zip:
-					CompressionUtils.zip(r, basePath, objectId, response.getOutputStream());
+					CompressionUtils.zip(r, filestoreManager, basePath, objectId, response.getOutputStream());
 					break;
 				case tar:
-					CompressionUtils.tar(r, basePath, objectId, response.getOutputStream());
+					CompressionUtils.tar(r, filestoreManager, basePath, objectId, response.getOutputStream());
 					break;
 				case gz:
-					CompressionUtils.gz(r, basePath, objectId, response.getOutputStream());
+					CompressionUtils.gz(r, filestoreManager, basePath, objectId, response.getOutputStream());
 					break;
 				case xz:
-					CompressionUtils.xz(r, basePath, objectId, response.getOutputStream());
+					CompressionUtils.xz(r, filestoreManager, basePath, objectId, response.getOutputStream());
 					break;
 				case bzip2:
-					CompressionUtils.bzip2(r, basePath, objectId, response.getOutputStream());
+					CompressionUtils.bzip2(r, filestoreManager, basePath, objectId, response.getOutputStream());
 					break;
 				}
 
diff --git a/src/main/java/com/gitblit/servlet/FilestoreServlet.java b/src/main/java/com/gitblit/servlet/FilestoreServlet.java
index 8f303fc..4af9084 100644
--- a/src/main/java/com/gitblit/servlet/FilestoreServlet.java
+++ b/src/main/java/com/gitblit/servlet/FilestoreServlet.java
@@ -238,7 +238,10 @@
 			}
 		} else {
 			response.setStatus(responseObject.error.code);
-			serialize(response, responseObject.error);
+			
+			if (isMetaRequest) {
+				serialize(response, responseObject.error);
+			}
 		}
 	};
 	
diff --git a/src/main/java/com/gitblit/servlet/GitFilter.java b/src/main/java/com/gitblit/servlet/GitFilter.java
index 27408f0..9522893 100644
--- a/src/main/java/com/gitblit/servlet/GitFilter.java
+++ b/src/main/java/com/gitblit/servlet/GitFilter.java
@@ -102,8 +102,8 @@
 	}
 
 	/**
-	 * Analyze the url and returns the action of the request. Return values are
-	 * either "/git-receive-pack" or "/git-upload-pack".
+	 * Analyze the url and returns the action of the request. Return values are:
+	 * "/git-receive-pack", "/git-upload-pack" or "/info/lfs".
 	 *
 	 * @param serverUrl
 	 * @return action of the request
@@ -316,18 +316,22 @@
 	
 	/**
 	 * Git lfs action uses an alternative authentication header, 
+	 * dependent on the viewing method.
 	 * 
+	 * @param httpRequest
 	 * @param action
 	 * @return
 	 */
 	@Override
-	protected String getAuthenticationHeader(String action) {
+	protected String getAuthenticationHeader(HttpServletRequest httpRequest, String action) {
 
 		if (action.equals(gitLfs)) {
-			return "LFS-Authenticate";
+			if (hasContentInRequestHeader(httpRequest, "Accept", FilestoreServlet.GIT_LFS_META_MIME)) {
+				return "LFS-Authenticate";
+			}
 		}
 		
-		return super.getAuthenticationHeader(action);
+		return super.getAuthenticationHeader(httpRequest, action);
 	}
 	
 	/**
diff --git a/src/main/java/com/gitblit/servlet/RawServlet.java b/src/main/java/com/gitblit/servlet/RawServlet.java
index 897047d..dca5773 100644
--- a/src/main/java/com/gitblit/servlet/RawServlet.java
+++ b/src/main/java/com/gitblit/servlet/RawServlet.java
@@ -364,7 +364,7 @@
 					if (pathEntries.get(0).path.indexOf('/') > -1) {
 						// we are in a subdirectory, add parent directory link
 						String pp = URLEncoder.encode(requestedPath, Constants.ENCODING);
-						pathEntries.add(0, new PathModel("..", pp + "/..", 0, FileMode.TREE.getBits(), null, null));
+						pathEntries.add(0, new PathModel("..", pp + "/..", null, 0, FileMode.TREE.getBits(), null, null));
 					}
 				}
 
diff --git a/src/main/java/com/gitblit/utils/CompressionUtils.java b/src/main/java/com/gitblit/utils/CompressionUtils.java
index b06edd2..1b8e6fc 100644
--- a/src/main/java/com/gitblit/utils/CompressionUtils.java
+++ b/src/main/java/com/gitblit/utils/CompressionUtils.java
@@ -16,6 +16,9 @@
 package com.gitblit.utils;
 
 import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.text.MessageFormat;
@@ -28,9 +31,11 @@
 import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
 import org.apache.commons.compress.compressors.CompressorException;
 import org.apache.commons.compress.compressors.CompressorStreamFactory;
+import org.apache.commons.io.IOUtils;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
@@ -40,6 +45,11 @@
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+
+import com.gitblit.GitBlit;
+import com.gitblit.manager.IFilestoreManager;
+import com.gitblit.models.FilestoreModel;
+import com.gitblit.models.FilestoreModel.Status;
 
 /**
  * Collection of static methods for retrieving information from a repository.
@@ -87,7 +97,7 @@
 	 * @return true if repository was successfully zipped to supplied output
 	 *         stream
 	 */
-	public static boolean zip(Repository repository, String basePath, String objectId,
+	public static boolean zip(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
 			OutputStream os) {
 		RevCommit commit = JGitUtils.getCommit(repository, objectId);
 		if (commit == null) {
@@ -115,16 +125,40 @@
 					continue;
 				}
 				tw.getObjectId(id, 0);
-
+				
+				ObjectLoader loader = repository.open(id);
+				
 				ZipArchiveEntry entry = new ZipArchiveEntry(tw.getPathString());
-				entry.setSize(reader.getObjectSize(id, Constants.OBJ_BLOB));
+
+				FilestoreModel filestoreItem = null;
+				
+				if (JGitUtils.isPossibleFilestoreItem(loader.getSize())) {
+					filestoreItem = JGitUtils.getFilestoreItem(tw.getObjectReader().open(id));
+				}
+
+				final long size = (filestoreItem == null) ? loader.getSize() : filestoreItem.getSize();  
+
+				entry.setSize(size);
 				entry.setComment(commit.getName());
 				entry.setUnixMode(mode.getBits());
 				entry.setTime(modified);
 				zos.putArchiveEntry(entry);
+				
+				if (filestoreItem == null) {
+					//Copy repository stored file
+					loader.copyTo(zos);
+				} else {
+					//Copy filestore file
+					try (FileInputStream streamIn = new FileInputStream(filestoreManager.getStoragePath(filestoreItem.oid))) {
+						IOUtils.copyLarge(streamIn, zos);
+					} catch (Throwable e) {
+						LOGGER.error(MessageFormat.format("Failed to archive filestore item {0}", filestoreItem.oid), e);
 
-				ObjectLoader ldr = repository.open(id);
-				ldr.copyTo(zos);
+						//Handle as per other errors 
+						throw e; 
+					}
+				}
+
 				zos.closeArchiveEntry();
 			}
 			zos.finish();
@@ -151,9 +185,9 @@
 	 * @return true if repository was successfully zipped to supplied output
 	 *         stream
 	 */
-	public static boolean tar(Repository repository, String basePath, String objectId,
+	public static boolean tar(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
 			OutputStream os) {
-		return tar(null, repository, basePath, objectId, os);
+		return tar(null, repository, filestoreManager, basePath, objectId, os);
 	}
 
 	/**
@@ -169,9 +203,9 @@
 	 * @return true if repository was successfully zipped to supplied output
 	 *         stream
 	 */
-	public static boolean gz(Repository repository, String basePath, String objectId,
+	public static boolean gz(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
 			OutputStream os) {
-		return tar(CompressorStreamFactory.GZIP, repository, basePath, objectId, os);
+		return tar(CompressorStreamFactory.GZIP, repository, filestoreManager, basePath, objectId, os);
 	}
 
 	/**
@@ -187,9 +221,9 @@
 	 * @return true if repository was successfully zipped to supplied output
 	 *         stream
 	 */
-	public static boolean xz(Repository repository, String basePath, String objectId,
+	public static boolean xz(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
 			OutputStream os) {
-		return tar(CompressorStreamFactory.XZ, repository, basePath, objectId, os);
+		return tar(CompressorStreamFactory.XZ, repository, filestoreManager, basePath, objectId, os);
 	}
 
 	/**
@@ -205,10 +239,10 @@
 	 * @return true if repository was successfully zipped to supplied output
 	 *         stream
 	 */
-	public static boolean bzip2(Repository repository, String basePath, String objectId,
+	public static boolean bzip2(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
 			OutputStream os) {
 
-		return tar(CompressorStreamFactory.BZIP2, repository, basePath, objectId, os);
+		return tar(CompressorStreamFactory.BZIP2, repository, filestoreManager, basePath, objectId, os);
 	}
 
 	/**
@@ -227,7 +261,7 @@
 	 * @return true if repository was successfully zipped to supplied output
 	 *         stream
 	 */
-	private static boolean tar(String algorithm, Repository repository, String basePath, String objectId,
+	private static boolean tar(String algorithm, Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
 			OutputStream os) {
 		RevCommit commit = JGitUtils.getCommit(repository, objectId);
 		if (commit == null) {
@@ -263,6 +297,7 @@
 				if (mode == FileMode.GITLINK || mode == FileMode.TREE) {
 					continue;
 				}
+				
 				tw.getObjectId(id, 0);
 
 				ObjectLoader loader = repository.open(id);
@@ -278,9 +313,34 @@
 					TarArchiveEntry entry = new TarArchiveEntry(tw.getPathString());
 					entry.setMode(mode.getBits());
 					entry.setModTime(modified);
-					entry.setSize(loader.getSize());
+
+					FilestoreModel filestoreItem = null;
+					
+					if (JGitUtils.isPossibleFilestoreItem(loader.getSize())) {
+						filestoreItem = JGitUtils.getFilestoreItem(tw.getObjectReader().open(id));
+					}
+
+					final long size = (filestoreItem == null) ? loader.getSize() : filestoreItem.getSize();  
+
+					entry.setSize(size);
 					tos.putArchiveEntry(entry);
-					loader.copyTo(tos);
+					
+					if (filestoreItem == null) {
+						//Copy repository stored file
+						loader.copyTo(tos);
+					} else {
+						//Copy filestore file
+						try (FileInputStream streamIn = new FileInputStream(filestoreManager.getStoragePath(filestoreItem.oid))) {
+
+							IOUtils.copyLarge(streamIn, tos);
+						} catch (Throwable e) {
+							LOGGER.error(MessageFormat.format("Failed to archive filestore item {0}", filestoreItem.oid), e);
+
+							//Handle as per other errors 
+							throw e; 
+						}
+					}
+					
 					tos.closeArchiveEntry();
 				}
 			}
diff --git a/src/main/java/com/gitblit/utils/DiffStatFormatter.java b/src/main/java/com/gitblit/utils/DiffStatFormatter.java
index 572046e..a6c3ae2 100644
--- a/src/main/java/com/gitblit/utils/DiffStatFormatter.java
+++ b/src/main/java/com/gitblit/utils/DiffStatFormatter.java
@@ -20,6 +20,7 @@
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.io.NullOutputStream;
 
 import com.gitblit.models.PathModel.PathChangeModel;
@@ -37,9 +38,9 @@
 
 	private PathChangeModel path;
 
-	public DiffStatFormatter(String commitId) {
+	public DiffStatFormatter(String commitId, Repository repository) {
 		super(NullOutputStream.INSTANCE);
-		diffStat = new DiffStat(commitId);
+		diffStat = new DiffStat(commitId, repository);
 	}
 
 	@Override
diff --git a/src/main/java/com/gitblit/utils/DiffUtils.java b/src/main/java/com/gitblit/utils/DiffUtils.java
index cdebec1..41aab4c 100644
--- a/src/main/java/com/gitblit/utils/DiffUtils.java
+++ b/src/main/java/com/gitblit/utils/DiffUtils.java
@@ -157,13 +157,16 @@
 		public final List<PathChangeModel> paths = new ArrayList<PathChangeModel>();
 
 		private final String commitId;
+		
+		private final Repository repository;
 
-		public DiffStat(String commitId) {
+		public DiffStat(String commitId, Repository repository) {
 			this.commitId = commitId;
+			this.repository = repository;
 		}
 
 		public PathChangeModel addPath(DiffEntry entry) {
-			PathChangeModel pcm = PathChangeModel.from(entry, commitId);
+			PathChangeModel pcm = PathChangeModel.from(entry, commitId, repository);
 			paths.add(pcm);
 			return pcm;
 		}
@@ -379,7 +382,7 @@
 			DiffFormatter df;
 			switch (outputType) {
 			case HTML:
-				df = new GitBlitDiffFormatter(commit.getName(), path, handler, tabLength);
+				df = new GitBlitDiffFormatter(commit.getName(), repository, path, handler, tabLength);
 				break;
 			case PLAIN:
 			default:
@@ -548,7 +551,7 @@
 		DiffStat stat = null;
 		try {
 			RawTextComparator cmp = RawTextComparator.DEFAULT;
-			DiffStatFormatter df = new DiffStatFormatter(commit.getName());
+			DiffStatFormatter df = new DiffStatFormatter(commit.getName(), repository);
 			df.setRepository(repository);
 			df.setDiffComparator(cmp);
 			df.setDetectRenames(true);
diff --git a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
index 86b7ca2..8ebadbf 100644
--- a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
+++ b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
@@ -33,6 +33,7 @@
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.RawParseUtils;
 
 import com.gitblit.models.PathModel.PathChangeModel;
@@ -164,11 +165,11 @@
 
 	}
 
-	public GitBlitDiffFormatter(String commitId, String path, BinaryDiffHandler handler, int tabLength) {
+	public GitBlitDiffFormatter(String commitId, Repository repository, String path, BinaryDiffHandler handler, int tabLength) {
 		super(new DiffOutputStream());
 		this.os = (DiffOutputStream) getOutputStream();
 		this.os.setFormatter(this, handler);
-		this.diffStat = new DiffStat(commitId);
+		this.diffStat = new DiffStat(commitId, repository);
 		this.tabLength = tabLength;
 		// If we have a full commitdiff, install maxima to avoid generating a super-long diff listing that
 		// will only tax the browser too much.
diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java
index c3d0207..7a00813 100644
--- a/src/main/java/com/gitblit/utils/JGitUtils.java
+++ b/src/main/java/com/gitblit/utils/JGitUtils.java
@@ -28,6 +28,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.commons.io.filefilter.TrueFileFilter;
@@ -42,6 +43,7 @@
 import org.eclipse.jgit.diff.RawTextComparator;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.StopWalkException;
 import org.eclipse.jgit.lib.BlobBasedConfig;
@@ -84,12 +86,16 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.GitBlit;
 import com.gitblit.GitBlitException;
+import com.gitblit.manager.GitblitManager;
+import com.gitblit.models.FilestoreModel;
 import com.gitblit.models.GitNote;
 import com.gitblit.models.PathModel;
 import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.models.RefModel;
 import com.gitblit.models.SubmoduleModel;
+import com.gitblit.servlet.FilestoreServlet;
 import com.google.common.base.Strings;
 
 /**
@@ -993,23 +999,40 @@
 				tw.setRecursive(true);
 				tw.addTree(commit.getTree());
 				while (tw.next()) {
-					list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw
-							.getRawMode(0), tw.getObjectId(0).getName(), commit.getId().getName(),
+					long size = 0;
+					FilestoreModel filestoreItem = null;
+					ObjectId objectId = tw.getObjectId(0);
+					
+					try {
+						if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
+
+							size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB);
+
+							if (isPossibleFilestoreItem(size)) {
+								filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId));
+							}
+						}
+					} catch (Throwable t) {
+						error(t, null, "failed to retrieve blob size for " + tw.getPathString());
+					}
+					
+					list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(),filestoreItem, size, tw
+							.getRawMode(0), objectId.getName(), commit.getId().getName(),
 							ChangeType.ADD));
 				}
 				tw.close();
 			} else {
 				RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
-				DiffStatFormatter df = new DiffStatFormatter(commit.getName());
+				DiffStatFormatter df = new DiffStatFormatter(commit.getName(), repository);
 				df.setRepository(repository);
 				df.setDiffComparator(RawTextComparator.DEFAULT);
 				df.setDetectRenames(true);
 				List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
 				for (DiffEntry diff : diffs) {
 					// create the path change model
-					PathChangeModel pcm = PathChangeModel.from(diff, commit.getName());
-
-					if (calculateDiffStat) {
+					PathChangeModel pcm = PathChangeModel.from(diff, commit.getName(), repository);
+						
+						if (calculateDiffStat) {
 						// update file diffstats
 						df.format(diff);
 						PathChangeModel pathStat = df.getDiffStat().getPath(pcm.path);
@@ -1083,7 +1106,7 @@
 
 			List<DiffEntry> diffEntries = df.scan(startCommit.getTree(), endCommit.getTree());
 			for (DiffEntry diff : diffEntries) {
-				PathChangeModel pcm = PathChangeModel.from(diff,  endCommit.getName());
+				PathChangeModel pcm = PathChangeModel.from(diff,  endCommit.getName(), repository);
 				list.add(pcm);
 			}
 			Collections.sort(list);
@@ -1167,21 +1190,54 @@
 	private static PathModel getPathModel(TreeWalk tw, String basePath, RevCommit commit) {
 		String name;
 		long size = 0;
+		
 		if (StringUtils.isEmpty(basePath)) {
 			name = tw.getPathString();
 		} else {
 			name = tw.getPathString().substring(basePath.length() + 1);
 		}
 		ObjectId objectId = tw.getObjectId(0);
+		FilestoreModel filestoreItem = null;
+		
 		try {
 			if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
+
 				size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB);
+
+				if (isPossibleFilestoreItem(size)) {
+					filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId));
+				}
 			}
 		} catch (Throwable t) {
 			error(t, null, "failed to retrieve blob size for " + tw.getPathString());
 		}
-		return new PathModel(name, tw.getPathString(), size, tw.getFileMode(0).getBits(),
+		return new PathModel(name, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(),
 				objectId.getName(), commit.getName());
+	}
+	
+	public static boolean isPossibleFilestoreItem(long size) {
+		return (   (size >= com.gitblit.Constants.LEN_FILESTORE_META_MIN) 
+				&& (size <= com.gitblit.Constants.LEN_FILESTORE_META_MAX));
+	}
+	
+	/**
+	 * 
+	 * @return Representative FilestoreModel if valid, otherwise null
+	 */
+	public static FilestoreModel getFilestoreItem(ObjectLoader obj){
+		try {
+			final byte[] blob = obj.getCachedBytes(com.gitblit.Constants.LEN_FILESTORE_META_MAX);
+			final String meta = new String(blob, "UTF-8");
+		
+			return FilestoreModel.fromMetaString(meta);
+
+		} catch (LargeObjectException e) {
+			//Intentionally failing silent
+		} catch (Exception e) {
+			error(e, null, "failed to retrieve filestoreItem " + obj.toString());
+		}
+		
+		return null;
 	}
 
 	/**
@@ -1197,29 +1253,34 @@
 			throws IOException {
 
 		long size = 0;
+		FilestoreModel filestoreItem = null;
 		TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree());
 		String pathString = path;
 
-			if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
-				size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB);
-				pathString = PathUtils.getLastPathComponent(pathString);
+		if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
 
-			} else if (tw.isSubtree()) {
+			pathString = PathUtils.getLastPathComponent(pathString);
+			
+			size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB);
+			
+			if (isPossibleFilestoreItem(size)) {
+				filestoreItem = getFilestoreItem(tw.getObjectReader().open(tw.getObjectId(0)));
+			}
+		} else if (tw.isSubtree()) {
 
-				// do not display dirs that are behind in the path
-				if (!Strings.isNullOrEmpty(filter)) {
-					pathString = path.replaceFirst(filter + "/", "");
-				}
-
-				// remove the last slash from path in displayed link
-				if (pathString != null && pathString.charAt(pathString.length()-1) == '/') {
-					pathString = pathString.substring(0, pathString.length()-1);
-				}
+			// do not display dirs that are behind in the path
+			if (!Strings.isNullOrEmpty(filter)) {
+				pathString = path.replaceFirst(filter + "/", "");
 			}
 
-			return new PathModel(pathString, tw.getPathString(), size, tw.getFileMode(0).getBits(),
-					tw.getObjectId(0).getName(), commit.getName());
+			// remove the last slash from path in displayed link
+			if (pathString != null && pathString.charAt(pathString.length()-1) == '/') {
+				pathString = pathString.substring(0, pathString.length()-1);
+			}
+		}
 
+		return new PathModel(pathString, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(),
+				tw.getObjectId(0).getName(), commit.getName());
 
 	}
 
@@ -2513,4 +2574,27 @@
 		}
 		return new MergeResult(MergeStatus.FAILED, null);
 	}
+	
+	
+	/**
+	 * Returns the LFS URL for the given oid 
+	 * Currently assumes that the Gitblit Filestore is used 
+	 *
+	 * @param baseURL
+	 * @param repository name
+	 * @param oid of lfs item
+	 * @return the lfs item URL
+	 */
+	public static String getLfsRepositoryUrl(String baseURL, String repositoryName, String oid) {
+		
+		if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
+			baseURL = baseURL.substring(0, baseURL.length() - 1);
+		}
+		
+		return baseURL + com.gitblit.Constants.R_PATH 
+					   + repositoryName + "/" 
+					   + com.gitblit.Constants.R_LFS 
+					   + "objects/" + oid;
+		
+	}
 }
diff --git a/src/main/java/com/gitblit/wicket/pages/BlamePage.java b/src/main/java/com/gitblit/wicket/pages/BlamePage.java
index e45bbbc..2fcca0a 100644
--- a/src/main/java/com/gitblit/wicket/pages/BlamePage.java
+++ b/src/main/java/com/gitblit/wicket/pages/BlamePage.java
@@ -32,6 +32,7 @@
 import org.apache.wicket.behavior.SimpleAttributeModifier;
 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;
@@ -93,9 +94,34 @@
 		final BlameType activeBlameType = BlameType.get(blameTypeParam);
 
 		RevCommit commit = getCommit();
-
-		add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class,
-				WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+		
+		PathModel pathModel = null;
+		
+		List<PathModel> paths = JGitUtils.getFilesInPath(getRepository(), StringUtils.getRootPath(blobPath), commit);
+		for (PathModel path : paths) {
+			if (path.path.equals(blobPath)) {
+				pathModel = path;
+				break;
+			}
+		}
+		
+		if (pathModel == null) {
+			final String notFound = MessageFormat.format("Blame page failed to find {0} in {1} @ {2}",
+					blobPath, repositoryName, objectId);
+			logger.error(notFound);
+			add(new Label("annotation").setVisible(false));
+			add(new Label("missingBlob", missingBlob(blobPath, commit)).setEscapeModelStrings(false));
+			return;
+		}
+		
+		if (pathModel.isFilestoreItem()) {
+			String rawUrl = JGitUtils.getLfsRepositoryUrl(getContextUrl(), repositoryName, pathModel.getFilestoreOid());
+			add(new ExternalLink("blobLink", rawUrl));
+		} else {
+			add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class,
+					WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));	
+		}
+		
 		add(new BookmarkablePageLink<Void>("commitLink", CommitPage.class,
 				WicketUtils.newObjectParameter(repositoryName, objectId)));
 		add(new BookmarkablePageLink<Void>("commitDiffLink", CommitDiffPage.class,
@@ -134,23 +160,9 @@
 		final DateFormat df = new SimpleDateFormat(format);
 		df.setTimeZone(getTimeZone());
 
-		PathModel pathModel = null;
-		List<PathModel> paths = JGitUtils.getFilesInPath(getRepository(), StringUtils.getRootPath(blobPath), commit);
-		for (PathModel path : paths) {
-			if (path.path.equals(blobPath)) {
-				pathModel = path;
-				break;
-			}
-		}
+		
 
-		if (pathModel == null) {
-			final String notFound = MessageFormat.format("Blame page failed to find {0} in {1} @ {2}",
-					blobPath, repositoryName, objectId);
-			logger.error(notFound);
-			add(new Label("annotation").setVisible(false));
-			add(new Label("missingBlob", missingBlob(blobPath, commit)).setEscapeModelStrings(false));
-			return;
-		}
+		
 
 		add(new Label("missingBlob").setVisible(false));
 
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html
index 2e0d57c..254d7d0 100644
--- a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html
@@ -45,6 +45,7 @@
 			<td class="changeType"><span wicket:id="changeType">[change type]</span></td>		
 			<td class="path"><span wicket:id="pathName">[commit path]</span></td>			
 			<td class="hidden-phone rightAlign">
+				<span wicket:id="filestore" style="margin-right:20px;" class="aui-lozenge aui-lozenge-moved"></span>
 				<span class="hidden-tablet" style="padding-right:20px;" wicket:id="diffStat"></span>
 				<span class="link">
 					<span class="hidden-tablet"><a wicket:id="patch"><wicket:message key="gb.patch"></wicket:message></a> | </span><a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a><span class="hidden-tablet"> | <a wicket:id="raw"><wicket:message key="gb.raw"></wicket:message></a></span> | <a wicket:id="blame"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
index b56d721..9bc1570 100644
--- a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -135,6 +135,8 @@
 			@Override
 			public void populateItem(final Item<PathChangeModel> item) {
 				final PathChangeModel entry = item.getModelObject();
+				final String filestoreItemUrl = entry.isFilestoreItem() ? JGitUtils.getLfsRepositoryUrl(getContextUrl(), repositoryName, entry.getFilestoreOid()) : null; 
+				
 				Label changeType = new Label("changeType", "");
 				WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);
 				setChangeTypeTooltip(changeType, entry.changeType);
@@ -143,6 +145,7 @@
 
 				boolean hasSubmodule = false;
 				String submodulePath = null;
+				
 				if (entry.isTree()) {
 					// tree
 					item.add(new LinkPanel("pathName", null, entry.path, TreePage.class,
@@ -159,11 +162,13 @@
 					item.add(new LinkPanel("pathName", "list", entry.path + " @ " + getShortObjectId(submoduleId), "#n" + entry.objectId));
 				} else {
 					// add relative link
-					item.add(new LinkPanel("pathName", "list", entry.path, "#n" + entry.objectId));
+					item.add(new LinkPanel("pathName", "list", entry.path, entry.isFilestoreItem() ? filestoreItemUrl : "#n" + entry.objectId));
 				}
 
 				// quick links
 				if (entry.isSubmodule()) {
+					item.add(new Label("filestore", getString("gb.filestore")).setVisible(false));
+					
 					item.add(new ExternalLink("raw", "").setEnabled(false));
 					// submodule
 					item.add(new ExternalLink("patch", "").setEnabled(false));
@@ -179,12 +184,24 @@
 							.newPathParameter(repositoryName, entry.commitId, entry.path))
 							.setEnabled(!entry.changeType.equals(ChangeType.ADD)
 									&& !entry.changeType.equals(ChangeType.DELETE)));
-					item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
-							.newPathParameter(repositoryName, entry.commitId, entry.path))
-							.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
-					String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path);
-					item.add(new ExternalLink("raw", rawUrl)
-							.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
+					
+					if (entry.isFilestoreItem()) {
+						item.add(new Label("filestore", getString("gb.filestore")).setVisible(true));
+						
+						item.add(new ExternalLink("view", filestoreItemUrl));
+						item.add(new ExternalLink("raw", filestoreItemUrl));
+					} else {
+						
+						item.add(new Label("filestore", getString("gb.filestore")).setVisible(false));
+						
+						item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
+								.newPathParameter(repositoryName, entry.commitId, entry.path))
+								.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
+						
+						item.add(new ExternalLink("raw", RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path))
+								.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
+					}
+					
 					item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
 							.newPathParameter(repositoryName, entry.commitId, entry.path))
 							.setEnabled(!entry.changeType.equals(ChangeType.ADD)
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.html b/src/main/java/com/gitblit/wicket/pages/CommitPage.html
index 2aa10f2..23e9438 100644
--- a/src/main/java/com/gitblit/wicket/pages/CommitPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.html
@@ -77,8 +77,9 @@
 	<table class="pretty">
 		<tr wicket:id="changedPath">
 			<td class="changeType"><span wicket:id="changeType">[change type]</span></td>
-			<td class="path"><span wicket:id="pathName">[commit path]</span></td>			
+			<td class="path"><span wicket:id="pathName">[commit path]</span></td>	
 			<td class="hidden-phone rightAlign">
+				<span wicket:id="filestore" style="margin-right:20px;" class="aui-lozenge aui-lozenge-moved"></span>
 				<span class="hidden-tablet" style="padding-right:20px;" wicket:id="diffStat"></span>
 				<span class="link">
 					<a wicket:id="diff"><wicket:message key="gb.diff"></wicket:message></a> | <span class="hidden-tablet"><a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="raw"><wicket:message key="gb.raw"></wicket:message></a> | </span><a wicket:id="blame"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.java b/src/main/java/com/gitblit/wicket/pages/CommitPage.java
index 0a1a68d..c841173 100644
--- a/src/main/java/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.java
@@ -163,6 +163,8 @@
 			@Override
 			public void populateItem(final Item<PathChangeModel> item) {
 				final PathChangeModel entry = item.getModelObject();
+				final String filestoreItemUrl = entry.isFilestoreItem() ? JGitUtils.getLfsRepositoryUrl(getContextUrl(), repositoryName, entry.getFilestoreOid()) : null;
+				
 				Label changeType = new Label("changeType", "");
 				WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);
 				setChangeTypeTooltip(changeType, entry.changeType);
@@ -194,9 +196,13 @@
 						path = JGitUtils.getStringContent(getRepository(), getCommit().getTree(), path);
 						displayPath = entry.path + " -> " + path;
 					}
-					item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
-							WicketUtils
-									.newPathParameter(repositoryName, entry.commitId, path)));
+					
+					if (entry.isFilestoreItem()) {
+						item.add(new LinkPanel("pathName", "list", entry.path, filestoreItemUrl));
+					} else {
+						item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
+							WicketUtils.newPathParameter(repositoryName, entry.commitId, path)));
+					}
 				}
 
 
@@ -204,6 +210,8 @@
 				if (entry.isSubmodule()) {
 					item.add(new ExternalLink("raw", "").setEnabled(false));
 
+					item.add(new Label("filestore", getString("gb.filestore")).setVisible(false));
+					
 					// submodule
 					item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
 							.newPathParameter(repositoryName, entry.commitId, entry.path))
@@ -220,12 +228,22 @@
 							.newPathParameter(repositoryName, entry.commitId, entry.path))
 							.setEnabled(!entry.changeType.equals(ChangeType.ADD)
 									&& !entry.changeType.equals(ChangeType.DELETE)));
-					item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
-							.newPathParameter(repositoryName, entry.commitId, entry.path))
-							.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
-					String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path);
-					item.add(new ExternalLink("raw", rawUrl)
-							.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
+					
+					if (entry.isFilestoreItem()) {
+						item.add(new Label("filestore", getString("gb.filestore")).setVisible(true));
+						
+						item.add(new ExternalLink("view", filestoreItemUrl));
+						item.add(new ExternalLink("raw", filestoreItemUrl));
+					} else {
+						item.add(new Label("filestore", getString("gb.filestore")).setVisible(false));
+						
+						item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
+								.newPathParameter(repositoryName, entry.commitId, entry.path))
+								.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
+						String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path);
+						item.add(new ExternalLink("raw", rawUrl)
+								.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
+					}
 					item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
 							.newPathParameter(repositoryName, entry.commitId, entry.path))
 							.setEnabled(!entry.changeType.equals(ChangeType.ADD)
diff --git a/src/main/java/com/gitblit/wicket/pages/FilestorePage.java b/src/main/java/com/gitblit/wicket/pages/FilestorePage.java
index be0181b..7c3bb9d 100644
--- a/src/main/java/com/gitblit/wicket/pages/FilestorePage.java
+++ b/src/main/java/com/gitblit/wicket/pages/FilestorePage.java
@@ -35,7 +35,6 @@
 import com.gitblit.wicket.CacheControl;
 import com.gitblit.wicket.FilestoreUI;
 import com.gitblit.wicket.GitBlitWebSession;
-import com.gitblit.wicket.RequiresAdminRole;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.CacheControl.LastModified;
 
diff --git a/src/main/java/com/gitblit/wicket/pages/TreePage.html b/src/main/java/com/gitblit/wicket/pages/TreePage.html
index 51a996f..c07f9c5 100644
--- a/src/main/java/com/gitblit/wicket/pages/TreePage.html
+++ b/src/main/java/com/gitblit/wicket/pages/TreePage.html
@@ -22,7 +22,8 @@
 	<table style="width:100%" class="pretty">
 		<tr wicket:id="changedPath">
 			<td class="hidden-phone icon"><img wicket:id="pathIcon" /></td>
-			<td><span wicket:id="pathName"></span></td>			
+			<td><span wicket:id="pathName"></span></td>
+			<td class="hidden-phone filestore"><span wicket:id="filestore" class="aui-lozenge aui-lozenge-moved"></span></td>
 			<td class="hidden-phone size"><span wicket:id="pathSize">[path size]</span></td>
 			<td class="hidden-phone mode"><span wicket:id="pathPermissions">[path permissions]</span></td>
 			<td class="treeLinks"><span wicket:id="pathLinks">[path links]</span></td>
diff --git a/src/main/java/com/gitblit/wicket/pages/TreePage.java b/src/main/java/com/gitblit/wicket/pages/TreePage.java
index d7899dc..f138214 100644
--- a/src/main/java/com/gitblit/wicket/pages/TreePage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TreePage.java
@@ -70,12 +70,13 @@
 			if (path.lastIndexOf('/') > -1) {
 				parentPath = path.substring(0, path.lastIndexOf('/'));
 			}
-			PathModel model = new PathModel("..", parentPath, 0, FileMode.TREE.getBits(), null, objectId);
+			PathModel model = new PathModel("..", parentPath, null, 0, FileMode.TREE.getBits(), null, objectId);
 			model.isParentPath = true;
 			paths.add(0, model);
 		}
 
 		final String id = getBestCommitId(commit);
+		
 		final ByteFormat byteFormat = new ByteFormat();
 		final String baseUrl = WicketUtils.getGitblitURL(getRequest());
 
@@ -88,7 +89,9 @@
 			@Override
 			public void populateItem(final Item<PathModel> item) {
 				PathModel entry = item.getModelObject();
+				
 				item.add(new Label("pathPermissions", JGitUtils.getPermissionsFromMode(entry.mode)));
+				
 				if (entry.isParentPath) {
 					// parent .. path
 					item.add(WicketUtils.newBlankImage("pathIcon"));
@@ -96,6 +99,7 @@
 					item.add(new LinkPanel("pathName", null, entry.name, TreePage.class,
 							WicketUtils
 									.newPathParameter(repositoryName, id, entry.path)));
+					item.add(new Label("filestore", getString("gb.filestore")).setVisible(false));
 					item.add(new Label("pathLinks", ""));
 				} else {
 					if (entry.isTree()) {
@@ -105,6 +109,8 @@
 						item.add(new LinkPanel("pathName", "list", entry.name, TreePage.class,
 								WicketUtils.newPathParameter(repositoryName, id,
 										entry.path)));
+
+						item.add(new Label("filestore", getString("gb.filestore")).setVisible(false));
 
 						// links
 						Fragment links = new Fragment("pathLinks", "treeLinks", this);
@@ -133,6 +139,8 @@
 								getShortObjectId(submoduleId), TreePage.class,
 								WicketUtils.newPathParameter(submodulePath, submoduleId, "")).setEnabled(hasSubmodule));
 
+						item.add(new Label("filestore", getString("gb.filestore")).setVisible(false));
+						
 						Fragment links = new Fragment("pathLinks", "submoduleLinks", this);
 						links.add(new BookmarkablePageLink<Void>("view", SummaryPage.class,
 								WicketUtils.newRepositoryParameter(submodulePath)).setEnabled(hasSubmodule));
@@ -155,17 +163,33 @@
 						}
 						item.add(WicketUtils.getFileImage("pathIcon", entry.name));
 						item.add(new Label("pathSize", byteFormat.format(entry.size)));
-						item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
-								WicketUtils.newPathParameter(repositoryName, id,
-										path)));
-
+						
 						// links
 						Fragment links = new Fragment("pathLinks", "blobLinks", this);
-						links.add(new BookmarkablePageLink<Void>("view", BlobPage.class,
-								WicketUtils.newPathParameter(repositoryName, id,
-										path)));
-						String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, id, path);
-						links.add(new ExternalLink("raw", rawUrl));
+						
+						if (entry.isFilestoreItem()) {
+							item.add(new Label("filestore", getString("gb.filestore")).setVisible(true));
+							
+							final String filestoreItemUrl = JGitUtils.getLfsRepositoryUrl(getContextUrl(), repositoryName, entry.getFilestoreOid());
+							
+							item.add(new LinkPanel("pathName", "list", displayPath, filestoreItemUrl));
+							links.add(new ExternalLink("view", filestoreItemUrl));
+							links.add(new ExternalLink("raw", filestoreItemUrl));
+							
+						} else {
+							item.add(new Label("filestore", getString("gb.filestore")).setVisible(false));
+							
+							item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
+									WicketUtils.newPathParameter(repositoryName, id,
+											path)));
+							
+							links.add(new BookmarkablePageLink<Void>("view", BlobPage.class,
+									WicketUtils.newPathParameter(repositoryName, id,
+											path)));
+							String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, id, path);
+							links.add(new ExternalLink("raw", rawUrl));
+						}
+						
 						links.add(new BookmarkablePageLink<Void>("blame", BlamePage.class,
 								WicketUtils.newPathParameter(repositoryName, id,
 										path)));
diff --git a/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java b/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java
index a3f127b..75fd70e 100644
--- a/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java
@@ -109,7 +109,7 @@
 					tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
 					while (tw.next()) {
 						if (tw.getPathString().equals(path)) {
-							matchingPath = new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw
+							matchingPath = new PathChangeModel(tw.getPathString(), tw.getPathString(), null, 0, tw
 								.getRawMode(0), tw.getObjectId(0).getName(), commit.getId().getName(),
 								ChangeType.MODIFY);
 						}
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index 0cc8fd0..0d36da7 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -1944,6 +1944,12 @@
 	padding-right:15px;
 }
 
+td.filestore {
+	text-align: right;
+	width:1em;
+	padding-right:15px;
+}
+
 td.size {
 	text-align: right;
 	width: 8em;	
diff --git a/src/test/java/com/gitblit/tests/JGitUtilsTest.java b/src/test/java/com/gitblit/tests/JGitUtilsTest.java
index 2cf4a5a..c273e86 100644
--- a/src/test/java/com/gitblit/tests/JGitUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/JGitUtilsTest.java
@@ -585,16 +585,16 @@
 
 	@Test
 	public void testZip() throws Exception {
-		assertFalse(CompressionUtils.zip(null, null, null, null));
+		assertFalse(CompressionUtils.zip(null, null, null, null, null));
 		Repository repository = GitBlitSuite.getHelloworldRepository();
 		File zipFileA = new File(GitBlitSuite.REPOSITORIES, "helloworld.zip");
 		FileOutputStream fosA = new FileOutputStream(zipFileA);
-		boolean successA = CompressionUtils.zip(repository, null, Constants.HEAD, fosA);
+		boolean successA = CompressionUtils.zip(repository, null, null, Constants.HEAD, fosA);
 		fosA.close();
 
 		File zipFileB = new File(GitBlitSuite.REPOSITORIES, "helloworld-java.zip");
 		FileOutputStream fosB = new FileOutputStream(zipFileB);
-		boolean successB = CompressionUtils.zip(repository, "java.java", Constants.HEAD, fosB);
+		boolean successB = CompressionUtils.zip(repository, null, "java.java", Constants.HEAD, fosB);
 		fosB.close();
 
 		repository.close();

--
Gitblit v1.9.1