Milos Cubrilo
2015-01-11 a9a2ffcf9a34bd25fe2e05bfdd4cde74725bb17d
#230 - Improve empty folder navigation.

Empty folders are automatically skipped when browsing repository tree (similar to github "folder jumping" feature).
2 files added
4 files modified
685 ■■■■■ changed files
src/main/java/com/gitblit/utils/JGitUtils.java 502 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/PathUtils.java 92 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TreePage.java 2 ●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/GitBlitSuite.java 14 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/JGitUtilsTest.java 9 ●●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/PathUtilsTest.java 66 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/JGitUtils.java
@@ -30,6 +30,7 @@
import java.util.Map.Entry;
import java.util.regex.Pattern;
import com.google.common.base.Strings;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.FetchCommand;
@@ -891,6 +892,63 @@
    }
    /**
     * Returns the list of files in the specified folder at the specified
     * commit. If the repository does not exist or is empty, an empty list is
     * returned.
     *
     * This is modified version that implements path compression feature.
     *
     * @param repository
     * @param path
     *            if unspecified, root folder is assumed.
     * @param commit
     *            if null, HEAD is assumed.
     * @return list of files in specified path
     */
    public static List<PathModel> getFilesInPath2(Repository repository, String path, RevCommit commit) {
        List<PathModel> list = new ArrayList<PathModel>();
        if (!hasCommits(repository)) {
            return list;
        }
        if (commit == null) {
            commit = getCommit(repository, null);
        }
        final TreeWalk tw = new TreeWalk(repository);
        try {
            tw.addTree(commit.getTree());
            final boolean isPathEmpty = Strings.isNullOrEmpty(path);
            if (!isPathEmpty) {
                PathFilter f = PathFilter.create(path);
                tw.setFilter(f);
            }
            tw.setRecursive(true);
            List<String> paths = new ArrayList<>();
            while (tw.next()) {
                    String child = isPathEmpty ? tw.getPathString()
                            : tw.getPathString().replaceFirst(String.format("%s/", path), "");
                    paths.add(child);
            }
            for(String p: PathUtils.compressPaths(paths)) {
                String pathString = isPathEmpty ? p : String.format("%s/%s", path, p);
                list.add(getPathModel(repository, pathString, path, commit));
            }
        } catch (IOException e) {
            error(e, repository, "{0} failed to get files for commit {1}", commit.getName());
        } finally {
            tw.release();
        }
        Collections.sort(list);
        return list;
    }
    /**
     * Returns the list of files changed in a specified commit. If the
     * repository does not exist or is empty, an empty list is returned.
     *
@@ -1122,6 +1180,46 @@
        return new PathModel(name, tw.getPathString(), size, tw.getFileMode(0).getBits(),
                objectId.getName(), commit.getName());
    }
    /**
     * Returns a path model by path string
     *
     * @param repo
     * @param path
     * @param filter
     * @param commit
     * @return a path model of the specified object
     */
    private static PathModel getPathModel(Repository repo, String path, String filter, RevCommit commit)
            throws IOException {
        long size = 0;
        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);
            } 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);
                }
            }
            return new PathModel(pathString, tw.getPathString(), size, tw.getFileMode(0).getBits(),
                    tw.getObjectId(0).getName(), commit.getName());
    }
    /**
     * Returns a permissions representation of the mode bits.
@@ -2194,97 +2292,97 @@
        }
        return false;
    }
    /**
     * Returns true if the commit identified by commitId is an ancestor or the
     * the commit identified by tipId.
     *
     * @param repository
     * @param commitId
     * @param tipId
     * @return true if there is the commit is an ancestor of the tip
     */
    public static boolean isMergedInto(Repository repository, String commitId, String tipId) {
        try {
            return isMergedInto(repository, repository.resolve(commitId), repository.resolve(tipId));
        } catch (Exception e) {
            LOGGER.error("Failed to determine isMergedInto", e);
        }
        return false;
    }
    /**
     * Returns true if the commit identified by commitId is an ancestor or the
     * the commit identified by tipId.
     *
     * @param repository
     * @param commitId
     * @param tipId
     * @return true if there is the commit is an ancestor of the tip
     */
    public static boolean isMergedInto(Repository repository, ObjectId commitId, ObjectId tipCommitId) {
        // traverse the revlog looking for a commit chain between the endpoints
        RevWalk rw = new RevWalk(repository);
        try {
            // must re-lookup RevCommits to workaround undocumented RevWalk bug
            RevCommit tip = rw.lookupCommit(tipCommitId);
            RevCommit commit = rw.lookupCommit(commitId);
            return rw.isMergedInto(commit, tip);
        } catch (Exception e) {
            LOGGER.error("Failed to determine isMergedInto", e);
        } finally {
            rw.dispose();
        }
        return false;
    }
    /**
     * Returns the merge base of two commits or null if there is no common
     * ancestry.
     *
     * @param repository
     * @param commitIdA
     * @param commitIdB
     * @return the commit id of the merge base or null if there is no common base
     */
    public static String getMergeBase(Repository repository, ObjectId commitIdA, ObjectId commitIdB) {
        RevWalk rw = new RevWalk(repository);
        try {
            RevCommit a = rw.lookupCommit(commitIdA);
            RevCommit b = rw.lookupCommit(commitIdB);
            rw.setRevFilter(RevFilter.MERGE_BASE);
            rw.markStart(a);
            rw.markStart(b);
            RevCommit mergeBase = rw.next();
            if (mergeBase == null) {
                return null;
            }
            return mergeBase.getName();
        } catch (Exception e) {
            LOGGER.error("Failed to determine merge base", e);
        } finally {
            rw.dispose();
        }
        return null;
    }
    public static enum MergeStatus {
        MISSING_INTEGRATION_BRANCH, MISSING_SRC_BRANCH, NOT_MERGEABLE, FAILED, ALREADY_MERGED, MERGEABLE, MERGED;
    }
    /**
     * Determines if we can cleanly merge one branch into another.  Returns true
     * if we can merge without conflict, otherwise returns false.
     *
     * @param repository
     * @param src
     * @param toBranch
     * @return true if we can merge without conflict
     */
    public static MergeStatus canMerge(Repository repository, String src, String toBranch) {
        RevWalk revWalk = null;
        try {
    /**
     * Returns true if the commit identified by commitId is an ancestor or the
     * the commit identified by tipId.
     *
     * @param repository
     * @param commitId
     * @param tipId
     * @return true if there is the commit is an ancestor of the tip
     */
    public static boolean isMergedInto(Repository repository, String commitId, String tipId) {
        try {
            return isMergedInto(repository, repository.resolve(commitId), repository.resolve(tipId));
        } catch (Exception e) {
            LOGGER.error("Failed to determine isMergedInto", e);
        }
        return false;
    }
    /**
     * Returns true if the commit identified by commitId is an ancestor or the
     * the commit identified by tipId.
     *
     * @param repository
     * @param commitId
     * @param tipId
     * @return true if there is the commit is an ancestor of the tip
     */
    public static boolean isMergedInto(Repository repository, ObjectId commitId, ObjectId tipCommitId) {
        // traverse the revlog looking for a commit chain between the endpoints
        RevWalk rw = new RevWalk(repository);
        try {
            // must re-lookup RevCommits to workaround undocumented RevWalk bug
            RevCommit tip = rw.lookupCommit(tipCommitId);
            RevCommit commit = rw.lookupCommit(commitId);
            return rw.isMergedInto(commit, tip);
        } catch (Exception e) {
            LOGGER.error("Failed to determine isMergedInto", e);
        } finally {
            rw.dispose();
        }
        return false;
    }
    /**
     * Returns the merge base of two commits or null if there is no common
     * ancestry.
     *
     * @param repository
     * @param commitIdA
     * @param commitIdB
     * @return the commit id of the merge base or null if there is no common base
     */
    public static String getMergeBase(Repository repository, ObjectId commitIdA, ObjectId commitIdB) {
        RevWalk rw = new RevWalk(repository);
        try {
            RevCommit a = rw.lookupCommit(commitIdA);
            RevCommit b = rw.lookupCommit(commitIdB);
            rw.setRevFilter(RevFilter.MERGE_BASE);
            rw.markStart(a);
            rw.markStart(b);
            RevCommit mergeBase = rw.next();
            if (mergeBase == null) {
                return null;
            }
            return mergeBase.getName();
        } catch (Exception e) {
            LOGGER.error("Failed to determine merge base", e);
        } finally {
            rw.dispose();
        }
        return null;
    }
    public static enum MergeStatus {
        MISSING_INTEGRATION_BRANCH, MISSING_SRC_BRANCH, NOT_MERGEABLE, FAILED, ALREADY_MERGED, MERGEABLE, MERGED;
    }
    /**
     * Determines if we can cleanly merge one branch into another.  Returns true
     * if we can merge without conflict, otherwise returns false.
     *
     * @param repository
     * @param src
     * @param toBranch
     * @return true if we can merge without conflict
     */
    public static MergeStatus canMerge(Repository repository, String src, String toBranch) {
        RevWalk revWalk = null;
        try {
            revWalk = new RevWalk(repository);
            ObjectId branchId = repository.resolve(toBranch);
            if (branchId == null) {
@@ -2294,122 +2392,122 @@
            if (srcId == null) {
                return MergeStatus.MISSING_SRC_BRANCH;
            }
            RevCommit branchTip = revWalk.lookupCommit(branchId);
            RevCommit srcTip = revWalk.lookupCommit(srcId);
            if (revWalk.isMergedInto(srcTip, branchTip)) {
                // already merged
                return MergeStatus.ALREADY_MERGED;
            } else if (revWalk.isMergedInto(branchTip, srcTip)) {
                // fast-forward
                return MergeStatus.MERGEABLE;
            }
            RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
            boolean canMerge = merger.merge(branchTip, srcTip);
            if (canMerge) {
                return MergeStatus.MERGEABLE;
            }
            RevCommit branchTip = revWalk.lookupCommit(branchId);
            RevCommit srcTip = revWalk.lookupCommit(srcId);
            if (revWalk.isMergedInto(srcTip, branchTip)) {
                // already merged
                return MergeStatus.ALREADY_MERGED;
            } else if (revWalk.isMergedInto(branchTip, srcTip)) {
                // fast-forward
                return MergeStatus.MERGEABLE;
            }
            RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
            boolean canMerge = merger.merge(branchTip, srcTip);
            if (canMerge) {
                return MergeStatus.MERGEABLE;
            }
        } catch (NullPointerException e) {
            LOGGER.error("Failed to determine canMerge", e);
        } catch (IOException e) {
            LOGGER.error("Failed to determine canMerge", e);
        } catch (IOException e) {
            LOGGER.error("Failed to determine canMerge", e);
        } finally {
            if (revWalk != null) {
            if (revWalk != null) {
                revWalk.release();
            }
        }
        return MergeStatus.NOT_MERGEABLE;
    }
    public static class MergeResult {
        public final MergeStatus status;
        public final String sha;
        MergeResult(MergeStatus status, String sha) {
            this.status = status;
            this.sha = sha;
        }
    }
    /**
     * Tries to merge a commit into a branch.  If there are conflicts, the merge
     * will fail.
     *
     * @param repository
     * @param src
     * @param toBranch
     * @param committer
     * @param message
     * @return the merge result
     */
    public static MergeResult merge(Repository repository, String src, String toBranch,
            PersonIdent committer, String message) {
        if (!toBranch.startsWith(Constants.R_REFS)) {
            // branch ref doesn't start with ref, assume this is a branch head
            toBranch = Constants.R_HEADS + toBranch;
        }
        RevWalk revWalk = null;
        try {
            revWalk = new RevWalk(repository);
            RevCommit branchTip = revWalk.lookupCommit(repository.resolve(toBranch));
            RevCommit srcTip = revWalk.lookupCommit(repository.resolve(src));
            if (revWalk.isMergedInto(srcTip, branchTip)) {
                // already merged
                return new MergeResult(MergeStatus.ALREADY_MERGED, null);
            }
            RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
            boolean merged = merger.merge(branchTip, srcTip);
            if (merged) {
                // create a merge commit and a reference to track the merge commit
                ObjectId treeId = merger.getResultTreeId();
                ObjectInserter odi = repository.newObjectInserter();
                try {
                    // Create a commit object
                    CommitBuilder commitBuilder = new CommitBuilder();
                    commitBuilder.setCommitter(committer);
                    commitBuilder.setAuthor(committer);
                    commitBuilder.setEncoding(Constants.CHARSET);
                    if (StringUtils.isEmpty(message)) {
                        message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName());
                    }
                    commitBuilder.setMessage(message);
                    commitBuilder.setParentIds(branchTip.getId(), srcTip.getId());
                    commitBuilder.setTreeId(treeId);
                    // Insert the merge commit into the repository
                    ObjectId mergeCommitId = odi.insert(commitBuilder);
                    odi.flush();
                    // set the merge ref to the merge commit
                    RevCommit mergeCommit = revWalk.parseCommit(mergeCommitId);
                    RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
                    mergeRefUpdate.setNewObjectId(mergeCommitId);
                    mergeRefUpdate.setRefLogMessage("commit: " + mergeCommit.getShortMessage(), false);
                    RefUpdate.Result rc = mergeRefUpdate.update();
                    switch (rc) {
                    case FAST_FORWARD:
                        // successful, clean merge
            }
        }
        return MergeStatus.NOT_MERGEABLE;
    }
    public static class MergeResult {
        public final MergeStatus status;
        public final String sha;
        MergeResult(MergeStatus status, String sha) {
            this.status = status;
            this.sha = sha;
        }
    }
    /**
     * Tries to merge a commit into a branch.  If there are conflicts, the merge
     * will fail.
     *
     * @param repository
     * @param src
     * @param toBranch
     * @param committer
     * @param message
     * @return the merge result
     */
    public static MergeResult merge(Repository repository, String src, String toBranch,
            PersonIdent committer, String message) {
        if (!toBranch.startsWith(Constants.R_REFS)) {
            // branch ref doesn't start with ref, assume this is a branch head
            toBranch = Constants.R_HEADS + toBranch;
        }
        RevWalk revWalk = null;
        try {
            revWalk = new RevWalk(repository);
            RevCommit branchTip = revWalk.lookupCommit(repository.resolve(toBranch));
            RevCommit srcTip = revWalk.lookupCommit(repository.resolve(src));
            if (revWalk.isMergedInto(srcTip, branchTip)) {
                // already merged
                return new MergeResult(MergeStatus.ALREADY_MERGED, null);
            }
            RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
            boolean merged = merger.merge(branchTip, srcTip);
            if (merged) {
                // create a merge commit and a reference to track the merge commit
                ObjectId treeId = merger.getResultTreeId();
                ObjectInserter odi = repository.newObjectInserter();
                try {
                    // Create a commit object
                    CommitBuilder commitBuilder = new CommitBuilder();
                    commitBuilder.setCommitter(committer);
                    commitBuilder.setAuthor(committer);
                    commitBuilder.setEncoding(Constants.CHARSET);
                    if (StringUtils.isEmpty(message)) {
                        message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName());
                    }
                    commitBuilder.setMessage(message);
                    commitBuilder.setParentIds(branchTip.getId(), srcTip.getId());
                    commitBuilder.setTreeId(treeId);
                    // Insert the merge commit into the repository
                    ObjectId mergeCommitId = odi.insert(commitBuilder);
                    odi.flush();
                    // set the merge ref to the merge commit
                    RevCommit mergeCommit = revWalk.parseCommit(mergeCommitId);
                    RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
                    mergeRefUpdate.setNewObjectId(mergeCommitId);
                    mergeRefUpdate.setRefLogMessage("commit: " + mergeCommit.getShortMessage(), false);
                    RefUpdate.Result rc = mergeRefUpdate.update();
                    switch (rc) {
                    case FAST_FORWARD:
                        // successful, clean merge
                        break;
                    default:
                        throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when merging commit {1} into {2} in {3}",
                                rc.name(), srcTip.getName(), branchTip.getName(), repository.getDirectory()));
                    }
                    // return the merge commit id
                    return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
                } finally {
                    odi.release();
                }
            }
        } catch (IOException e) {
            LOGGER.error("Failed to merge", e);
                    default:
                        throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when merging commit {1} into {2} in {3}",
                                rc.name(), srcTip.getName(), branchTip.getName(), repository.getDirectory()));
                    }
                    // return the merge commit id
                    return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
                } finally {
                    odi.release();
                }
            }
        } catch (IOException e) {
            LOGGER.error("Failed to merge", e);
        } finally {
            if (revWalk != null) {
            if (revWalk != null) {
                revWalk.release();
            }
        }
        return new MergeResult(MergeStatus.FAILED, null);
    }
            }
        }
        return new MergeResult(MergeStatus.FAILED, null);
    }
}
src/main/java/com/gitblit/utils/PathUtils.java
New file
@@ -0,0 +1,92 @@
package com.gitblit.utils;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import java.util.*;
/**
 *  Utils for handling path strings
 *
 */
public class PathUtils {
    private PathUtils() {}
    /**
     *  Compress paths containing no files
     *
     * @param paths lines from `git ls-tree -r --name-only ${branch}`
     * @return compressed paths
     */
    public static List<String> compressPaths(final Iterable<String> paths)  {
        ArrayList<String> pathList = new ArrayList<>();
        Map<String, List<String[]>> folderRoots = new LinkedHashMap<>();
        for (String s: paths) {
            String[] components = s.split("/");
            // File in current directory
            if (components.length == 1) {
                pathList.add(components[0]);
                // Directory path
            } else {
                List<String[]> rootedPaths = folderRoots.get(components[0]);
                if (rootedPaths == null) {
                    rootedPaths = new ArrayList<>();
                }
                rootedPaths.add(components);
                folderRoots.put(components[0], rootedPaths);
            }
        }
        for (String folder: folderRoots.keySet()) {
            List<String[]> matchingPaths = folderRoots.get(folder);
            if (matchingPaths.size() == 1) {
                pathList.add(toStringPath(matchingPaths.get(0)));
            } else {
                pathList.add(longestCommonSequence(matchingPaths));
            }
        }
        return pathList;
    }
    /**
     *  Get last path component
     *
     *
     * @param path string path separated by slashes
     * @return rightmost entry
     */
    public static String getLastPathComponent(final String path) {
        return Iterables.getLast(Splitter.on("/").omitEmptyStrings().split(path), path);
    }
    private static String toStringPath(final String[] pathComponents) {
        List<String> tmp = Arrays.asList(pathComponents);
        return Joiner.on('/').join(tmp.subList(0,tmp.size()-1)) + '/';
    }
    private static String longestCommonSequence(final List<String[]> paths) {
        StringBuilder path = new StringBuilder();
        for (int i = 0; i < paths.get(0).length; i++) {
            String current = paths.get(0)[i];
            for (int j = 1; j < paths.size(); j++) {
                if (!current.equals(paths.get(j)[i])) {
                    return path.toString();
                }
            }
            path.append(current);
            path.append('/');
        }
        return path.toString();
    }
}
src/main/java/com/gitblit/wicket/pages/TreePage.java
@@ -52,7 +52,7 @@
        Repository r = getRepository();
        RevCommit commit = getCommit();
        List<PathModel> paths = JGitUtils.getFilesInPath(r, path, commit);
        List<PathModel> paths = JGitUtils.getFilesInPath2(r, path, commit);
        // tree page links
        add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -63,9 +63,9 @@
        GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class, GitDaemonTest.class,
        SshDaemonTest.class, GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class,
        FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdAuthenticationTest.class,
        ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class,
        ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class,
        BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class,
        SshKeysDispatcherTest.class, UITicketTest.class })
        SshKeysDispatcherTest.class, UITicketTest.class, PathUtilsTest.class })
public class GitBlitSuite {
    public static final File BASEFOLDER = new File("data");
@@ -110,11 +110,11 @@
        return getRepository("test/gitective.git");
    }
    public static Repository getTicketsTestRepository() {
        JGitUtils.createRepository(REPOSITORIES, "gb-tickets.git").close();
        return getRepository("gb-tickets.git");
    }
    public static Repository getTicketsTestRepository() {
        JGitUtils.createRepository(REPOSITORIES, "gb-tickets.git").close();
        return getRepository("gb-tickets.git");
    }
    private static Repository getRepository(String name) {
        try {
            File gitDir = FileKey.resolve(new File(REPOSITORIES, name), FS.DETECTED);
src/test/java/com/gitblit/tests/JGitUtilsTest.java
@@ -476,6 +476,15 @@
    }
    @Test
    public void testFilesInPath2() throws Exception {
        assertEquals(0, JGitUtils.getFilesInPath2(null, null, null).size());
        Repository repository = GitBlitSuite.getHelloworldRepository();
        List<PathModel> files = JGitUtils.getFilesInPath2(repository, null, null);
        repository.close();
        assertTrue(files.size() > 10);
    }
    @Test
    public void testDocuments() throws Exception {
        Repository repository = GitBlitSuite.getTicgitRepository();
        List<String> extensions = Arrays.asList(new String[] { ".mkd", ".md" });
src/test/java/com/gitblit/tests/PathUtilsTest.java
New file
@@ -0,0 +1,66 @@
/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.tests;
import com.gitblit.utils.PathUtils;
import org.junit.Test;
import java.util.Arrays;
public class PathUtilsTest extends GitblitUnitTest {
    private static final String[][][] testData = {
            {
                    // Folder contents
                    {".gitignore","src/main/java/a.java", "src/main/java/b.java", "docs/c.md"},
                    // Expected after compressing
                    {".gitignore", "src/main/java/", "docs/"}
            },
            {
                    {".gitignore","src/main/java/a.java", "src/main/b.java", "docs/c.md"},
                    {".gitignore", "src/main/", "docs/"}
            },
            {
                    {".gitignore","src/x.java","src/main/java/a.java", "src/main/java/b.java", "docs/c.md"},
                    {".gitignore", "src/", "docs/"}
            },
    };
    @Test
    public void testCompressPaths() throws Exception {
        for (String[][] test : testData ) {
            assertArrayEquals(test[1], PathUtils.compressPaths(Arrays.asList(test[0])).toArray(new String[]{}));
        }
    }
    @Test
    public void testGetLastPathComponent() {
        assertEquals(PathUtils.getLastPathComponent("/a/b/c/d/e.out"), "e.out");
        assertEquals(PathUtils.getLastPathComponent("e.out"), "e.out");
        assertEquals(PathUtils.getLastPathComponent("/a/b/c/d/"), "d");
        assertEquals(PathUtils.getLastPathComponent("/a/b/c/d"), "d");
        assertEquals(PathUtils.getLastPathComponent("/"), "/");
    }
}