James Moger
2016-01-25 252dc07d7f85cc344b5919bb7c6166ef84b2102e
Merge pull request #988 from gitblit/976-raw-download-filestore-item

Fix for #976 - Filestore links via browser
24 files modified
599 ■■■■ changed files
src/main/java/com/gitblit/Constants.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/FilestoreModel.java 67 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/PathModel.java 56 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/DownloadZipServlet.java 18 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/FilestoreServlet.java 5 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/GitFilter.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/RawServlet.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/CompressionUtils.java 92 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/DiffStatFormatter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/DiffUtils.java 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/JGitUtils.java 130 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/BlamePage.java 50 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java 31 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/CommitPage.html 3 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/CommitPage.java 36 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/FilestorePage.java 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TreePage.html 3 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TreePage.java 44 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/HistoryPanel.java 2 ●●● patch | view | raw | blame | history
src/main/resources/gitblit.css 6 ●●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/JGitUtilsTest.java 6 ●●●● patch | view | raw | blame | history
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";
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),
        
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());
            }
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 {
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;
                }
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);
            }
        }
    };
    
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);
    }
    
    /**
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));
                    }
                }
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();
                }
            }
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
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);
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.
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;
    }
}
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));
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>
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)
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>
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)
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;
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>
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)));
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);
                        }
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;    
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();