James Moger
2012-08-10 eb870fc034460c2bab69039b21049d332a002ca1
Submodules support
1 files added
20 files modified
490 ■■■■ changed files
distrib/gitblit.properties 12 ●●●●● patch | view | raw | blame | history
docs/01_features.mkd 1 ●●●● patch | view | raw | blame | history
docs/04_releases.mkd 28 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 15 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/PathModel.java 10 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/SubmoduleModel.java 47 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/JGitUtils.java 80 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/MarkdownUtils.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/StringUtils.java 26 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp_es.properties 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp_ja.properties 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp_pl.properties 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/CommitDiffPage.java 50 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/CommitPage.java 61 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/MarkdownPage.java 6 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.java 87 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.java 6 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/TreePage.html 7 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/TreePage.java 31 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/StringUtilsTest.java 7 ●●●●● patch | view | raw | blame | history
distrib/gitblit.properties
@@ -37,6 +37,18 @@
# SINCE 1.0.1
git.searchExclusions =
# List of regex url patterns for extracting a repository name when locating
# submodules.
#   e.g. git.submoduleUrlPatterns = .*?://github.com/(.*) will extract
#   *gitblit/gitblit.git* from *git://github.com/gitblit/gitblit.git*
# If no matches are found then the submodule repository name is assumed to be
# whatever trails the last / character. (e.g. gitblit.git).
#
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 1.0.1
git.submoduleUrlPatterns = .*?://github.com/(.*)
# Allow push/pull over http/https with JGit servlet.
# If you do NOT want to allow Git clients to clone/push to Gitblit set this
# to false.  You might want to do this if you are only using ssh:// or git://.
docs/01_features.mkd
@@ -23,6 +23,7 @@
- LDAP authentication and optional LDAP-controlled Team memberships
- Gravatar integration
- Git-notes display support
- Submodule support
- gh-pages display support (Jekyll is not supported)
- Branch metrics (uses Google Charts)
- HEAD and Branch RSS feeds
docs/04_releases.mkd
@@ -11,12 +11,11 @@
#### fixes
- Support StartTLS in LdapUserService (Steffen Gebert, issue 122)
- Do not index blobs in submodules (issue 119)
- Do not index submodule links (issue 119)
- Restore original user or team object on failure to update (issue 118)
- Fixes to relative path determination in repository search algorithm for symlinks (issue 116)
- Fix to GitServlet to allow pushing to symlinked repositories (issue 116)
- Repository URL uses `X-Forwarded-Proto` and `X-Forwarded-Port`, if available, for reverse proxy configurations (issue 115)
- Repository URL now uses `X-Forwarded-Proto` and `X-Forwarded-Port`, if available, for reverse proxy configurations (issue 115)
- Output real RAW content, not simulated RAW content (issue 114)
- Fixed Lucene charset encoding bug when reindexing a repository (issue 112)
- Fixed search box linking to Lucene page for nested repository on Tomcat (issue 111)
@@ -25,9 +24,17 @@
#### additions
- Added a repository setting to control authorization as AUTHENTICATED or NAMED.
NAMED is the original behavior for authorizing against a list of permitted users or permitted teams.
AUTHENTICATED allows restricted access for any authenticated user.
- Preliminary bare repository submodule support
    **New:** *git.submoduleUrlPatterns=*
    - *git.submoduleUrlPatterns* is a space-delimited list of regular expressions for extracting a repository name from a submodule url.
    For example, `git.submoduleUrlPatterns = .*?://github.com/(.*)` would extract *gitblit/gitblit.git* from *git://github.git/gitblit/gitblit.git*
    **Note:** You may not need this control to work with submodules, but it is there if you do.
    - If there are no matches from *git.submoduleUrlPatterns* then the repository name is assumed to be whatever comes after the last `/` character *(e.g. gitblit.git)*
    - Gitblit will try to locate this repository relative to the current repository *(e.g. myfolder/myrepo.git, myfolder/mysubmodule.git)* and then at the root level *(mysubmodule.git)* if that fails.
    - Submodule references in a working copy will be properly identified as gitlinks, but Gitblit will not traverse into the working copy submodule repository.
- Added a repository setting to control authorization as AUTHENTICATED or NAMED. (issue 117)
NAMED is the original behavior for authorizing against a list of permitted users or permitted teams.
AUTHENTICATED allows restricted access for any authenticated user.  This is a looser authorization control.
- Added default authorization control setting (AUTHENTICATED or NAMED)  
    **New:** *git.defaultAuthorizationControl=NAMED*  
- Added setting to control how deep Gitblit will recurse into *git.repositoriesFolder* looking for repositories (issue 103)  
@@ -36,10 +43,17 @@
    **New:** *git.searchExclusions=*  
- Blob page now supports displaying images (issue 6)
- Non-image binary files can now be downloaded using the RAW link
- Support StartTLS in LdapUserService (Steffen Gebert, issue 122)
#### changes
- Line breaks inserted for readability in raw Markdown content display in the event of a parsing/transformation error.  An error message is now displayed prepended to the raw content.
- Improve UTF-8 reading for Markdown files
- Updated Polish translation
<hr/>
### Older Releases
**1.0.0** *released 2012-07-14*
@@ -103,8 +117,6 @@
- added Ivy 2.2.0
<hr/>
### Older Releases
**0.9.3** *released 2012-04-11*
src/com/gitblit/GitBlit.java
@@ -910,6 +910,21 @@
        r.close();
        return model;
    }
    /**
     * Determines if this server has the requested repository.
     *
     * @param name
     * @return true if the repository exists
     */
    public boolean hasRepository(String repositoryName) {
        Repository r = getRepository(repositoryName, false);
        if (r == null) {
            return false;
        }
        r.close();
        return true;
    }
    /**
     * Returns the size in bytes of the repository. Gitblit caches the
src/com/gitblit/models/PathModel.java
@@ -35,14 +35,16 @@
    public final String path;
    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 commitId) {
    public PathModel(String name, String path, long size, int mode, String objectId, String commitId) {
        this.name = name;
        this.path = path;
        this.size = size;
        this.mode = mode;
        this.objectId = objectId;
        this.commitId = commitId;
    }
@@ -102,9 +104,9 @@
        public final ChangeType changeType;
        public PathChangeModel(String name, String path, long size, int mode, String commitId,
                ChangeType type) {
            super(name, path, size, mode, commitId);
        public PathChangeModel(String name, String path, long size, int mode, String objectId,
                String commitId, ChangeType type) {
            super(name, path, size, mode, objectId, commitId);
            this.changeType = type;
        }
src/com/gitblit/models/SubmoduleModel.java
New file
@@ -0,0 +1,47 @@
/*
 * Copyright 2012 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.models;
import java.io.Serializable;
/**
 * SubmoduleModel is a serializable model class that represents a git submodule
 * definition.
 *
 * @author James Moger
 *
 */
public class SubmoduleModel implements Serializable {
    private static final long serialVersionUID = 1L;
    public final String name;
    public final String path;
    public final String url;
    public boolean hasSubmodule;
    public String gitblitPath;
    public SubmoduleModel(String name, String path, String url) {
        this.name = name;
        this.path = path;
        this.url = url;
    }
    public String toString() {
        return path + "=" + url;
    }
}
src/com/gitblit/utils/JGitUtils.java
@@ -45,7 +45,9 @@
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.StopWalkException;
import org.eclipse.jgit.lib.BlobBasedConfig;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
@@ -87,6 +89,7 @@
import com.gitblit.models.PathModel;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.RefModel;
import com.gitblit.models.SubmoduleModel;
/**
 * Collection of static methods for retrieving information from a repository.
@@ -732,7 +735,8 @@
                tw.addTree(commit.getTree());
                while (tw.next()) {
                    list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw
                            .getRawMode(0), commit.getId().getName(), ChangeType.ADD));
                            .getRawMode(0), tw.getObjectId(0).getName(), commit.getId().getName(),
                            ChangeType.ADD));
                }
                tw.release();
            } else {
@@ -745,15 +749,15 @@
                for (DiffEntry diff : diffs) {
                    if (diff.getChangeType().equals(ChangeType.DELETE)) {
                        list.add(new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff
                                .getNewMode().getBits(), commit.getId().getName(), diff
                                .getNewMode().getBits(), null, commit.getId().getName(), diff
                                .getChangeType()));
                    } else if (diff.getChangeType().equals(ChangeType.RENAME)) {
                        list.add(new PathChangeModel(diff.getOldPath(), diff.getNewPath(), 0, diff
                                .getNewMode().getBits(), commit.getId().getName(), diff
                                .getNewMode().getBits(), null, commit.getId().getName(), diff
                                .getChangeType()));
                    } else {
                        list.add(new PathChangeModel(diff.getNewPath(), diff.getNewPath(), 0, diff
                                .getNewMode().getBits(), commit.getId().getName(), diff
                                .getNewMode().getBits(), null, commit.getId().getName(), diff
                                .getChangeType()));
                    }
                }
@@ -846,15 +850,16 @@
        } else {
            name = tw.getPathString().substring(basePath.length() + 1);
        }
        ObjectId objectId = tw.getObjectId(0);
        try {
            if (!tw.isSubtree()) {
                size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB);
            if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
                size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB);
            }
        } 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(),
                commit.getName());
                objectId.getName(), commit.getName());
    }
    /**
@@ -871,13 +876,10 @@
        } else if (FileMode.EXECUTABLE_FILE.equals(mode)) {
            return "-rwxr-xr-x";
        } else if (FileMode.SYMLINK.equals(mode)) {
            // FIXME symlink permissions
            return "symlink";
        } else if (FileMode.GITLINK.equals(mode)) {
            // FIXME gitlink permissions
            return "gitlink";
            return "submodule";
        }
        // FIXME missing permissions
        return "missing";
    }
@@ -1533,6 +1535,62 @@
        }
        return branch;
    }
    /**
     * Returns the list of submodules for this repository.
     *
     * @param repository
     * @param commit
     * @return list of submodules
     */
    public static List<SubmoduleModel> getSubmodules(Repository repository, String commitId) {
        RevCommit commit = getCommit(repository, commitId);
        return getSubmodules(repository, commit.getTree());
    }
    /**
     * Returns the list of submodules for this repository.
     *
     * @param repository
     * @param commit
     * @return list of submodules
     */
    public static List<SubmoduleModel> getSubmodules(Repository repository, RevTree tree) {
        List<SubmoduleModel> list = new ArrayList<SubmoduleModel>();
        byte [] blob = getByteContent(repository, tree, ".gitmodules");
        if (blob == null) {
            return list;
        }
        try {
            BlobBasedConfig config = new BlobBasedConfig(repository.getConfig(), blob);
            for (String module : config.getSubsections("submodule")) {
                String path = config.getString("submodule", module, "path");
                String url = config.getString("submodule", module, "url");
                list.add(new SubmoduleModel(module, path, url));
            }
        } catch (ConfigInvalidException e) {
            LOGGER.error("Failed to load .gitmodules file for " + repository.getDirectory(), e);
        }
        return list;
    }
    /**
     * Returns the submodule definition for the specified path at the specified
     * commit.  If no module is defined for the path, null is returned.
     *
     * @param repository
     * @param commit
     * @param path
     * @return a submodule definition or null if there is no submodule
     */
    public static SubmoduleModel getSubmoduleModel(Repository repository, String commitId, String path) {
        for (SubmoduleModel model : getSubmodules(repository, commitId)) {
            if (model.path.equals(path)) {
                return model;
            }
        }
        return null;
    }
    /**
     * Returns the list of notes entered about the commit from the refs/notes
src/com/gitblit/utils/MarkdownUtils.java
@@ -20,6 +20,7 @@
import java.io.StringReader;
import java.io.StringWriter;
import org.slf4j.LoggerFactory;
import org.tautua.markdownpapers.Markdown;
import org.tautua.markdownpapers.parser.ParseException;
@@ -44,6 +45,8 @@
            String html = transformMarkdown(reader);
            reader.close();
            return html;
        } catch (IllegalArgumentException e) {
            throw new java.text.ParseException(e.getMessage(), 0);
        } catch (NullPointerException p) {
            throw new java.text.ParseException("Markdown string is null!", 0);
        }
@@ -65,6 +68,7 @@
            md.transform(markdownReader, writer);
            return writer.toString().trim();
        } catch (ParseException p) {
            LoggerFactory.getLogger(MarkdownUtils.class).error("MarkdownPapers failed to parse Markdown!", p);
            throw new java.text.ParseException(p.getMessage(), 0);
        } finally {
            try {
src/com/gitblit/utils/StringUtils.java
@@ -33,6 +33,8 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
@@ -599,4 +601,28 @@
        }
        return value;
    }
    /**
     * Attempt to extract a repository name from a given url using regular
     * expressions.  If no match is made, then return whatever trails after
     * the final / character.
     *
     * @param regexUrls
     * @return a repository path
     */
    public static String extractRepositoryPath(String url, String... urlpatterns) {
        for (String urlPattern : urlpatterns) {
            Pattern p = Pattern.compile(urlPattern);
            Matcher m = p.matcher(url);
            while (m.find()) {
                String repositoryPath = m.group(1);
                return repositoryPath;
            }
        }
        // last resort
        if (url.lastIndexOf('/') > -1) {
            return url.substring(url.lastIndexOf('/') + 1);
        }
        return url;
    }
}
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -312,4 +312,5 @@
gb.duration.years = {0} years
gb.authorizationControl = authorization control
gb.allowAuthenticatedDescription = grant restricted access to all authenticated users
gb.allowNamedDescription = grant restricted access to named users or teams
gb.allowNamedDescription = grant restricted access to named users or teams
gb.markdownFailure = Failed to parse Markdown content!
src/com/gitblit/wicket/GitBlitWebApp_es.properties
@@ -312,4 +312,5 @@
gb.duration.years = {0} a\u00F1os
gb.authorizationControl = authorization control
gb.allowAuthenticatedDescription = grant restricted access to all authenticated users
gb.allowNamedDescription = grant restricted access to named users or teams
gb.allowNamedDescription = grant restricted access to named users or teams
gb.markdownFailure = Failed to parse Markdown content!
src/com/gitblit/wicket/GitBlitWebApp_ja.properties
@@ -312,4 +312,5 @@
gb.duration.years = {0} years
gb.authorizationControl = authorization control
gb.allowAuthenticatedDescription = grant restricted access to all authenticated users
gb.allowNamedDescription = grant restricted access to named users or teams
gb.allowNamedDescription = grant restricted access to named users or teams
gb.markdownFailure = Failed to parse Markdown content!
src/com/gitblit/wicket/GitBlitWebApp_pl.properties
@@ -312,4 +312,5 @@
gb.duration.years = {0} lat
gb.authorizationControl = authorization control
gb.allowAuthenticatedDescription = grant restricted access to all authenticated users
gb.allowNamedDescription = grant restricted access to named users or teams
gb.allowNamedDescription = grant restricted access to named users or teams
gb.markdownFailure = Failed to parse Markdown content!
src/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -31,6 +31,7 @@
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.utils.DiffUtils;
import com.gitblit.utils.DiffUtils.DiffOutputType;
import com.gitblit.utils.JGitUtils;
@@ -86,26 +87,55 @@
                setChangeTypeTooltip(changeType, entry.changeType);
                item.add(changeType);
                boolean hasSubmodule = false;
                String submodulePath = null;
                if (entry.isTree()) {
                    // tree
                    item.add(new LinkPanel("pathName", null, entry.path, TreePage.class,
                            WicketUtils
                                    .newPathParameter(repositoryName, entry.commitId, entry.path)));
                } else if (entry.isSubmodule()) {
                    // submodule
                    String submoduleId = entry.objectId;
                    SubmoduleModel submodule = getSubmodule(entry.path);
                    submodulePath = submodule.gitblitPath;
                    hasSubmodule = submodule.hasSubmodule;
                    item.add(new LinkPanel("pathName", "list", entry.path + " @ " +
                            getShortObjectId(submoduleId), TreePage.class,
                            WicketUtils
                                    .newPathParameter(submodulePath, submoduleId, "")).setEnabled(hasSubmodule));
                } else {
                    // blob
                    item.add(new LinkPanel("pathName", "list", entry.path, BlobPage.class,
                            WicketUtils
                                    .newPathParameter(repositoryName, entry.commitId, entry.path)));
                }
                item.add(new BookmarkablePageLink<Void>("patch", PatchPage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path)));
                item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path)));
                item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path)));
                item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path))
                        .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
                // quick links
                if (entry.isSubmodule()) {
                    // submodule
                    item.add(new BookmarkablePageLink<Void>("patch", PatchPage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path)).setEnabled(false));
                    item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
                            .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
                    item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path)).setEnabled(false));
                    item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path))
                            .setEnabled(hasSubmodule));
                } else {
                    // tree or blob
                    item.add(new BookmarkablePageLink<Void>("patch", PatchPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path)));
                    item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path)));
                    item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path)));
                    item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                            .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
                }
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
            }
src/com/gitblit/wicket/pages/CommitPage.java
@@ -36,6 +36,7 @@
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.GitNote;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.utils.JGitUtils;
import com.gitblit.wicket.WicketUtils;
@@ -52,7 +53,7 @@
        Repository r = getRepository();
        RevCommit c = getCommit();
        List<String> parents = new ArrayList<String>();
        if (c.getParentCount() > 0) {
            for (RevCommit parent : c.getParents()) {
@@ -150,28 +151,60 @@
                WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);
                setChangeTypeTooltip(changeType, entry.changeType);
                item.add(changeType);
                boolean hasSubmodule = false;
                String submodulePath = null;
                if (entry.isTree()) {
                    // tree
                    item.add(new LinkPanel("pathName", null, entry.path, TreePage.class,
                            WicketUtils
                                    .newPathParameter(repositoryName, entry.commitId, entry.path)));
                } else if (entry.isSubmodule()) {
                    // submodule
                    String submoduleId = entry.objectId;
                    SubmoduleModel submodule = getSubmodule(entry.path);
                    submodulePath = submodule.gitblitPath;
                    hasSubmodule = submodule.hasSubmodule;
                    item.add(new LinkPanel("pathName", "list", entry.path + " @ " +
                            getShortObjectId(submoduleId), TreePage.class,
                            WicketUtils.newPathParameter(submodulePath, submoduleId, "")).setEnabled(hasSubmodule));
                } else {
                    // blob
                    item.add(new LinkPanel("pathName", "list", entry.path, BlobPage.class,
                            WicketUtils
                                    .newPathParameter(repositoryName, entry.commitId, entry.path)));
                }
                item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
                        .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)));
                item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path))
                        .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
                item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path))
                        .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
                // quick links
                if (entry.isSubmodule()) {
                    // submodule
                    item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path))
                            .setEnabled(false));
                    item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
                            .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
                    item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path))
                            .setEnabled(false));
                    item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path))
                            .setEnabled(hasSubmodule));
                } else {
                    // tree or blob
                    item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
                            .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)));
                    item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                            .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
                    item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                            .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
                }
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
src/com/gitblit/wicket/pages/MarkdownPage.java
@@ -15,6 +15,7 @@
 */
package com.gitblit.wicket.pages;
import java.text.MessageFormat;
import java.text.ParseException;
import org.apache.wicket.PageParameters;
@@ -27,6 +28,7 @@
import com.gitblit.GitBlit;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.WicketUtils;
public class MarkdownPage extends RepositoryPage {
@@ -56,8 +58,8 @@
        try {
            htmlText = MarkdownUtils.transformMarkdown(markdownText);
        } catch (ParseException p) {
            error(p.getMessage());
            htmlText = markdownText;
            markdownText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{2}", getString("gb.error"), getString("gb.markdownFailure"), markdownText);
            htmlText = StringUtils.breakLinesForHtml(markdownText);
        }
        // Add the html to the page
src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -19,9 +19,12 @@
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
@@ -45,6 +48,7 @@
import com.gitblit.PagesServlet;
import com.gitblit.SyndicationServlet;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
@@ -62,18 +66,20 @@
    protected final String repositoryName;
    protected final String objectId;
    private transient Repository r;
    private RepositoryModel m;
    private Map<String, SubmoduleModel> submodules;
    private final Map<String, PageRegistration> registeredPages;
    public RepositoryPage(PageParameters params) {
        super(params);
        repositoryName = WicketUtils.getRepositoryName(params);
        objectId = WicketUtils.getObject(params);
        if (StringUtils.isEmpty(repositoryName)) {
            error(MessageFormat.format(getString("gb.repositoryNotSpecifiedFor"), getPageName()), true);
        }
@@ -206,8 +212,85 @@
            error(MessageFormat.format(getString("gb.failedToFindCommit"),
                    objectId, repositoryName, getPageName()), true);
        }
        getSubmodules(commit);
        return commit;
    }
    private Map<String, SubmoduleModel> getSubmodules(RevCommit commit) {
        if (submodules == null) {
            submodules = new HashMap<String, SubmoduleModel>();
            for (SubmoduleModel model : JGitUtils.getSubmodules(r, commit.getTree())) {
                submodules.put(model.path, model);
            }
        }
        return submodules;
    }
    protected Map<String, SubmoduleModel> getSubmodules() {
        return submodules;
    }
    protected SubmoduleModel getSubmodule(String path) {
        SubmoduleModel model = submodules.get(path);
        if (model == null) {
            // undefined submodule?!
            model = new SubmoduleModel(path.substring(path.lastIndexOf('/') + 1), path, path);
            model.hasSubmodule = false;
            model.gitblitPath = model.name;
            return model;
        } else {
            // extract the repository name from the clone url
            List<String> patterns = GitBlit.getStrings(Keys.git.submoduleUrlPatterns);
            String submoduleName = StringUtils.extractRepositoryPath(model.url, patterns.toArray(new String[0]));
            // determine the current path for constructing paths relative
            // to the current repository
            String currentPath = "";
            if (repositoryName.indexOf('/') > -1) {
                currentPath = repositoryName.substring(0, repositoryName.lastIndexOf('/') + 1);
            }
            // try to locate the submodule repository
            // prefer bare to non-bare names
            List<String> candidates = new ArrayList<String>();
            // relative
            candidates.add(currentPath + StringUtils.stripDotGit(submoduleName));
            candidates.add(candidates.get(candidates.size() - 1) + ".git");
            // relative, no subfolder
            if (submoduleName.lastIndexOf('/') > -1) {
                String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
                candidates.add(currentPath + StringUtils.stripDotGit(name));
                candidates.add(currentPath + candidates.get(candidates.size() - 1) + ".git");
            }
            // absolute
            candidates.add(StringUtils.stripDotGit(submoduleName));
            candidates.add(candidates.get(candidates.size() - 1) + ".git");
            // absolute, no subfolder
            if (submoduleName.lastIndexOf('/') > -1) {
                String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
                candidates.add(StringUtils.stripDotGit(name));
                candidates.add(candidates.get(candidates.size() - 1) + ".git");
            }
            // create a unique, ordered set of candidate paths
            Set<String> paths = new LinkedHashSet<String>(candidates);
            for (String candidate : paths) {
                if (GitBlit.self().hasRepository(candidate)) {
                    model.hasSubmodule = true;
                    model.gitblitPath = candidate;
                    return model;
                }
            }
            // we do not have a copy of the submodule, but we need a path
            model.gitblitPath = candidates.get(0);
            return model;
        }
    }
    protected String getShortObjectId(String objectId) {
        return objectId.substring(0, 8);
src/com/gitblit/wicket/pages/SummaryPage.java
@@ -136,6 +136,7 @@
        if (getRepositoryModel().showReadme) {
            String htmlText = null;
            String markdownText = null;
            String readme = null;
            try {
                RevCommit head = JGitUtils.getCommit(r, null);
@@ -158,11 +159,12 @@
                }
                if (!StringUtils.isEmpty(readme)) {
                    String [] encodings = GitBlit.getEncodings();
                    String markdownText = JGitUtils.getStringContent(r, head.getTree(), readme, encodings);
                    markdownText = JGitUtils.getStringContent(r, head.getTree(), readme, encodings);
                    htmlText = MarkdownUtils.transformMarkdown(markdownText);
                }
            } catch (ParseException p) {
                error(p.getMessage());
                markdownText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{2}", getString("gb.error"), getString("gb.markdownFailure"), markdownText);
                htmlText = StringUtils.breakLinesForHtml(markdownText);
            }
            Fragment fragment = new Fragment("readme", "markdownPanel");
            fragment.add(new Label("readmeFile", readme));
src/com/gitblit/wicket/pages/TreePage.html
@@ -29,6 +29,13 @@
        </tr>
    </table>
    <!--  submodule links -->
    <wicket:fragment wicket:id="submoduleLinks">
        <span class="link">
            <a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="zip"><wicket:message key="gb.zip"></wicket:message></a>
        </span>
    </wicket:fragment>
    <!--  tree links -->
    <wicket:fragment wicket:id="treeLinks">
        <span class="link">
src/com/gitblit/wicket/pages/TreePage.java
@@ -34,6 +34,7 @@
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.PathModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.utils.ByteFormat;
import com.gitblit.utils.JGitUtils;
import com.gitblit.wicket.WicketUtils;
@@ -71,7 +72,7 @@
            if (path.lastIndexOf('/') > -1) {
                parentPath = path.substring(0, path.lastIndexOf('/'));
            }
            PathModel model = new PathModel("..", parentPath, 0, FileMode.TREE.getBits(), objectId);
            PathModel model = new PathModel("..", parentPath, 0, FileMode.TREE.getBits(), null, objectId);
            model.isParentPath = true;
            paths.add(0, model);
        }
@@ -118,6 +119,34 @@
                                repositoryName, objectId, entry.path)).setVisible(GitBlit
                                .getBoolean(Keys.web.allowZipDownloads, true)));
                        item.add(links);
                    } else if (entry.isSubmodule()) {
                        // submodule
                        String submoduleId = entry.objectId;
                        String submodulePath;
                        boolean hasSubmodule = false;
                        SubmoduleModel submodule = getSubmodule(entry.path);
                        submodulePath = submodule.gitblitPath;
                        hasSubmodule = submodule.hasSubmodule;
                        item.add(WicketUtils.newImage("pathIcon", "git-orange-16x16.png"));
                        item.add(new Label("pathSize", ""));
                        item.add(new LinkPanel("pathName", "list", entry.name + " @ " +
                                getShortObjectId(submoduleId), TreePage.class,
                                WicketUtils.newPathParameter(submodulePath, submoduleId, "")).setEnabled(hasSubmodule));
                        Fragment links = new Fragment("pathLinks", "submoduleLinks", this);
                        links.add(new BookmarkablePageLink<Void>("view", SummaryPage.class,
                                WicketUtils.newRepositoryParameter(submodulePath)).setEnabled(hasSubmodule));
                        links.add(new BookmarkablePageLink<Void>("tree", TreePage.class,
                                WicketUtils.newPathParameter(submodulePath, submoduleId,
                                        "")).setEnabled(hasSubmodule));
                        links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
                                WicketUtils.newPathParameter(submodulePath, submoduleId,
                                        "")).setEnabled(hasSubmodule));
                        links.add(new ExternalLink("zip", DownloadZipServlet.asLink(baseUrl,
                                submodulePath, submoduleId, "")).setVisible(GitBlit
                                .getBoolean(Keys.web.allowZipDownloads, true)).setEnabled(hasSubmodule));
                        item.add(links);
                    } else {
                        // blob link
                        item.add(WicketUtils.getFileImage("pathIcon", entry.name));
tests/com/gitblit/tests/StringUtilsTest.java
@@ -150,4 +150,11 @@
        assertFalse(StringUtils.fuzzyMatch("123", "12345"));
        assertFalse(StringUtils.fuzzyMatch("AbCdEfHIJ", "abc*hhh"));
    }
    @Test
    public void testGetRepositoryPath() throws Exception {
        assertEquals("gitblit/gitblit.git", StringUtils.extractRepositoryPath("git://github.com/gitblit/gitblit.git", new String [] { ".*?://github.com/(.*)" }));
        assertEquals("gitblit.git", StringUtils.extractRepositoryPath("git://github.com/gitblit/gitblit.git", new String [] { ".*?://github.com/[^/].*?/(.*)" }));
        assertEquals("gitblit.git", StringUtils.extractRepositoryPath("git://github.com/gitblit/gitblit.git"));
    }
}