James Moger
2012-09-29 1e1b85270f93b3bca624c99b478f3a9a23be2395
Preliminary implementation of server-side forking (issue-137)

The fork mechanism clones the repository , access restrictions, and
other config options. The app has been updated throughout to handle
personal repositories and to properly display origin/fork links.

In order to fork a repository the user account must have the #fork role,
the origin repository must permit forking, and the user account must
have standard clone permissions to the repository.

Because forking introduces a new user role no existing user accounts can
automatically begin forking a repository. This is both a pro and a con.

Since the fork has the same access restrictions as the origin repository,
those who can access the origin may also access the fork. This is intentional
to facilitate integration-manager workflow. The fork owner does have the
power to completely change the access restrictions of his/her fork.
6 files added
38 files modified
1542 ■■■■ changed files
.project 46 ●●●● patch | view | raw | blame | history
docs/04_releases.mkd 13 ●●●●● patch | view | raw | blame | history
docs/05_roadmap.mkd 1 ●●●● patch | view | raw | blame | history
resources/gitblit.css 46 ●●●●● patch | view | raw | blame | history
src/com/gitblit/ConfigUserService.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 2 ●●●●● patch | view | raw | blame | history
src/com/gitblit/FileUserService.java 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 187 ●●●● patch | view | raw | blame | history
src/com/gitblit/SyndicationServlet.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/models/ProjectModel.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/models/RepositoryModel.java 49 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/UserModel.java 29 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/ActivityUtils.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/utils/JGitUtils.java 14 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/StringUtils.java 29 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 16 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/BasePage.java 9 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.html 4 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.java 1 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.html 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.java 1 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ForksPage.html 24 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ForksPage.java 133 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/GitSearchPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/HistoryPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/LogPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ProjectPage.html 70 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ProjectPage.java 152 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ProjectsPage.java 8 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.html 34 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.java 123 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RootPage.java 9 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/UserPage.html 44 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/UserPage.java 146 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/GravatarImage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/HistoryPanel.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/LogPanel.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.html 79 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java 199 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoriesPanel.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoriesPanel.java 23 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/SearchPanel.java 5 ●●●●● patch | view | raw | blame | history
.project
@@ -1,23 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
    <name>gitblit</name>
    <comment></comment>
    <projects>
    </projects>
    <buildSpec>
        <buildCommand>
            <name>org.eclipse.jdt.core.javabuilder</name>
            <arguments>
            </arguments>
        </buildCommand>
        <buildCommand>
            <name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
            <arguments>
            </arguments>
        </buildCommand>
    </buildSpec>
    <natures>
        <nature>org.eclipse.jdt.core.javanature</nature>
        <nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
    </natures>
</projectDescription>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
    <name>Gitblit</name>
    <comment></comment>
    <projects>
    </projects>
    <buildSpec>
        <buildCommand>
            <name>org.eclipse.jdt.core.javabuilder</name>
            <arguments>
            </arguments>
        </buildCommand>
        <buildCommand>
            <name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
            <arguments>
            </arguments>
        </buildCommand>
    </buildSpec>
    <natures>
        <nature>org.eclipse.jdt.core.javanature</nature>
        <nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
    </natures>
</projectDescription>
docs/04_releases.mkd
@@ -17,10 +17,15 @@
#### additions
- added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)
- delete branch feature (issue 121, Github/ajermakovics)
- added line links to blob view at the expense of zebra striping (issue 130)
- added RedmineUserService (github/mallowlabs)
- Added simple project pages.  A project is a subfolder off the *git.repositoriesFolder*.
- Added support for personal repositories.  This builds on the simple project pages.
Personal repositories are stored in *git.repositoriesFolder*/*~username*.  Each user with personal repositories will have a user page, something like the GitHub profile page.  Personal repositories have all the same features as common repositories.
- Added support for server-side forking of a repository to a personal repository (issue 137)
In order to fork a repository to a personal clone, the user account must have the *fork* permission **and** the repository must *allow forks*.  The clone inherits the access restrictions of its origin.  i.e. if Team A has access to the origin repository, then by default Team A also has access to the fork.  This is to facilitate collaboration.  However, the fork owner may change access to the fork and add/remove users/teams, etc as required.
- Added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)
- Delete branch feature (issue 121, Github/ajermakovics)
- Added line links to blob view at the expense of zebra striping (issue 130)
- Added RedmineUserService (github/mallowlabs)
#### changes
docs/05_roadmap.mkd
@@ -26,7 +26,6 @@
### IDEAS
* Gitblit: Re-use the EGit branch visualization table cell renderer as some sort of servlet
* Gitblit: Support personal repositories (~username/repo)
* Gitblit: diff should highlight inserted/removed fragment compared to original line
* Gitblit: implement branch permission controls as Groovy pre-receive script.  
*Maintain permissions text file similar to a gitolite configuration file or svn authz file.*
resources/gitblit.css
@@ -20,6 +20,11 @@
    outline: none;
}
[class^="icon-"], [class*=" icon-"] a i {
    /* override for a links that look like bootstrap buttons */
    vertical-align: text-bottom;
}
hr {
    margin-top: 10px;
    margin-bottom: 10px;
@@ -127,6 +132,47 @@
    font-weight: bold;
}
.pageTitle {
    color: #888;
    font-size: 18px;
    line-height: 27px;
}
.pageTitle .project, .pageTitle .repository {
    font-family: Helvetica, arial, freesans, clean, sans-serif;
    font-size: 22px;
}
.pageTitle .controls {
    font-size: 12px;
}
.pageTitle .repository {
    font-weight: bold;
}
.originRepository {
    font-family: Helvetica, arial, freesans, clean, sans-serif;
    color: #888;
    font-size: 12px;
    line-height: 14px;
    margin: 0px;
}
.forkSource, .forkEntry {
    color: #888;
}
.forkSource {
    font-size: 18px;
    line-height: 20px;
    padding: 5px 0px;
}
.forkEntry {
    font-size: 14px;
    padding: 2px 0px;
}
div.page_footer {
    clear: both;
    height: 17px;
src/com/gitblit/ConfigUserService.java
@@ -750,6 +750,9 @@
            if (model.canAdmin) {
                roles.add(Constants.ADMIN_ROLE);
            }
            if (model.canFork) {
                roles.add(Constants.FORK_ROLE);
            }
            if (model.excludeFromFederation) {
                roles.add(Constants.NOT_FEDERATED_ROLE);
            }
@@ -858,6 +861,7 @@
                    Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
                            USER, username, ROLE)));
                    user.canAdmin = roles.contains(Constants.ADMIN_ROLE);
                    user.canFork = roles.contains(Constants.FORK_ROLE);
                    user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE);
                    // repository memberships
src/com/gitblit/Constants.java
@@ -41,6 +41,8 @@
    public static final String JGIT_VERSION = "JGit 2.1.0 (201209190230-r)";
    public static final String ADMIN_ROLE = "#admin";
    public static final String FORK_ROLE = "#fork";
    public static final String NOT_FEDERATED_ROLE = "#notfederated";
    
src/com/gitblit/FileUserService.java
@@ -234,6 +234,8 @@
                // Permissions
                if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
                    model.canAdmin = true;
                } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
                    model.canFork = true;
                } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
                    model.excludeFromFederation = true;
                }
@@ -283,6 +285,9 @@
            if (model.canAdmin) {
                roles.add(Constants.ADMIN_ROLE);
            }
            if (model.canFork) {
                roles.add(Constants.FORK_ROLE);
            }
            if (model.excludeFromFederation) {
                roles.add(Constants.NOT_FEDERATED_ROLE);
            }
src/com/gitblit/GitBlit.java
@@ -22,6 +22,8 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -747,6 +749,14 @@
    private void addToCachedRepositoryList(String name, RepositoryModel model) {
        if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
            repositoryListCache.put(name, model);
            // update the fork origin repository with this repository clone
            if (!StringUtils.isEmpty(model.originRepository)) {
                if (repositoryListCache.containsKey(model.originRepository)) {
                    RepositoryModel origin = repositoryListCache.get(model.originRepository);
                    origin.addFork(name);
                }
            }
        }
    }
    
@@ -754,12 +764,13 @@
     * Removes the repository from the list of cached repositories.
     * 
     * @param name
     * @return the model being removed
     */
    private void removeFromCachedRepositoryList(String name) {
    private RepositoryModel removeFromCachedRepositoryList(String name) {
        if (StringUtils.isEmpty(name)) {
            return;
            return null;
        }
        repositoryListCache.remove(name);
        return repositoryListCache.remove(name);
    }
    /**
@@ -988,7 +999,7 @@
            if (model == null) {
                return null;
            }
            addToCachedRepositoryList(repositoryName, model);
            addToCachedRepositoryList(repositoryName, model);
            return model;
        }
        
@@ -1034,7 +1045,7 @@
     * @return project config map
     */
    private Map<String, ProjectModel> getProjectConfigs() {
        if (projectConfigs.isOutdated()) {
        if (projectCache.isEmpty() || projectConfigs.isOutdated()) {
            
            try {
                projectConfigs.load();
@@ -1077,9 +1088,10 @@
     * Returns a list of project models for the user.
     * 
     * @param user
     * @param includeUsers
     * @return list of projects that are accessible to the user
     */
    public List<ProjectModel> getProjectModels(UserModel user) {
    public List<ProjectModel> getProjectModels(UserModel user, boolean includeUsers) {
        Map<String, ProjectModel> configs = getProjectConfigs();
        // per-user project lists, this accounts for security and visibility
@@ -1104,10 +1116,25 @@
        }
        
        // sort projects, root project first
        List<ProjectModel> projects = new ArrayList<ProjectModel>(map.values());
        Collections.sort(projects);
        projects.remove(map.get(""));
        projects.add(0, map.get(""));
        List<ProjectModel> projects;
        if (includeUsers) {
            // all projects
            projects = new ArrayList<ProjectModel>(map.values());
            Collections.sort(projects);
            projects.remove(map.get(""));
            projects.add(0, map.get(""));
        } else {
            // all non-user projects
            projects = new ArrayList<ProjectModel>();
            ProjectModel root = map.remove("");
            for (ProjectModel model : map.values()) {
                if (!model.isUserProject()) {
                    projects.add(model);
                }
            }
            Collections.sort(projects);
            projects.add(0, root);
        }
        return projects;
    }
    
@@ -1119,7 +1146,7 @@
     * @return a project model, or null if it does not exist
     */
    public ProjectModel getProjectModel(String name, UserModel user) {
        for (ProjectModel project : getProjectModels(user)) {
        for (ProjectModel project : getProjectModels(user, true)) {
            if (project.name.equalsIgnoreCase(name)) {
                return project;
            }
@@ -1137,15 +1164,37 @@
        Map<String, ProjectModel> configs = getProjectConfigs();
        ProjectModel project = configs.get(name.toLowerCase());
        if (project == null) {
            return null;
        }
        // clone the object
        project = DeepCopier.copy(project);
        String folder = name.toLowerCase() + "/";
        for (String repository : getRepositoryList()) {
            if (repository.toLowerCase().startsWith(folder)) {
                project.addRepository(repository);
            project = new ProjectModel(name);
            if (name.length() > 0 && name.charAt(0) == '~') {
                UserModel user = getUserModel(name.substring(1));
                if (user != null) {
                    project.title = user.getDisplayName();
                    project.description = "personal repositories";
                }
            }
        } else {
            // clone the object
            project = DeepCopier.copy(project);
        }
        if (StringUtils.isEmpty(name)) {
            // get root repositories
            for (String repository : getRepositoryList()) {
                if (repository.indexOf('/') == -1) {
                    project.addRepository(repository);
                }
            }
        } else {
            // get repositories in subfolder
            String folder = name.toLowerCase() + "/";
            for (String repository : getRepositoryList()) {
                if (repository.toLowerCase().startsWith(folder)) {
                    project.addRepository(repository);
                }
            }
        }
        if (project.repositories.size() == 0) {
            // no repositories == no project
            return null;
        }
        return project;
    }
@@ -1189,18 +1238,26 @@
        model.hasCommits = JGitUtils.hasCommits(r);
        model.lastChange = JGitUtils.getLastChange(r);
        model.isBare = r.isBare();
        if (repositoryName.indexOf('/') == -1) {
            model.projectPath = "";
        } else {
            model.projectPath = repositoryName.substring(0, repositoryName.indexOf('/'));
        }
        
        StoredConfig config = r.getConfig();
        boolean hasOrigin = !StringUtils.isEmpty(config.getString("remote", "origin", "url"));
        if (config != null) {
            model.description = getConfig(config, "description", "");
            model.owner = getConfig(config, "owner", "");
            model.useTickets = getConfig(config, "useTickets", false);
            model.useDocs = getConfig(config, "useDocs", false);
            model.allowForks = getConfig(config, "allowForks", true);
            model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
                    "accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, null)));
            model.authorizationControl = AuthorizationControl.fromName(getConfig(config,
                    "authorizationControl", settings.getString(Keys.git.defaultAuthorizationControl, null)));
            model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
            model.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin);
            model.isFrozen = getConfig(config, "isFrozen", false);
            model.showReadme = getConfig(config, "showReadme", false);
            model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
@@ -1229,6 +1286,23 @@
        model.HEAD = JGitUtils.getHEADRef(r);
        model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
        r.close();
        if (model.origin != null && model.origin.startsWith("file://")) {
            // repository was cloned locally... perhaps as a fork
            try {
                File folder = new File(new URI(model.origin));
                String originRepo = com.gitblit.utils.FileUtils.getRelativePath(getRepositoriesFolder(), folder);
                if (!StringUtils.isEmpty(originRepo)) {
                    // ensure origin still exists
                    File repoFolder = new File(getRepositoriesFolder(), originRepo);
                    if (repoFolder.exists()) {
                        model.originRepository = originRepo;
                    }
                }
            } catch (URISyntaxException e) {
                logger.error("Failed to determine fork for " + model, e);
            }
        }
        return model;
    }
    
@@ -1425,9 +1499,27 @@
                            "Failed to rename repository permissions ''{0}'' to ''{1}''.",
                            repositoryName, repository.name));
                }
                // rename fork origins in their configs
                if (!ArrayUtils.isEmpty(repository.forks)) {
                    for (String fork : repository.forks) {
                        Repository rf = getRepository(fork);
                        try {
                            StoredConfig config = rf.getConfig();
                            String origin = config.getString("remote", "origin", "url");
                            origin = origin.replace(repositoryName, repository.name);
                            config.setString("remote", "origin", "url", origin);
                            config.save();
                        } catch (Exception e) {
                            logger.error("Failed to update repository fork config for " + fork, e);
                        }
                        rf.close();
                    }
                }
                // clear the cache
                clearRepositoryMetadataCache(repositoryName);
                repository.resetDisplayName();
            }
            // load repository
@@ -1483,6 +1575,7 @@
        config.setString(Constants.CONFIG_GITBLIT, null, "owner", repository.owner);
        config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets);
        config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs);
        config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);
        config.setString(Constants.CONFIG_GITBLIT, null, "accessRestriction", repository.accessRestriction.name());
        config.setString(Constants.CONFIG_GITBLIT, null, "authorizationControl", repository.authorizationControl.name());
        config.setBoolean(Constants.CONFIG_GITBLIT, null, "showRemoteBranches", repository.showRemoteBranches);
@@ -1558,7 +1651,11 @@
            closeRepository(repositoryName);
            // clear the repository cache
            clearRepositoryMetadataCache(repositoryName);
            removeFromCachedRepositoryList(repositoryName);
            RepositoryModel model = removeFromCachedRepositoryList(repositoryName);
            if (!ArrayUtils.isEmpty(model.forks)) {
                resetRepositoryListCache();
            }
            File folder = new File(repositoriesFolder, repositoryName);
            if (folder.exists() && folder.isDirectory()) {
@@ -2423,4 +2520,52 @@
        scheduledExecutor.shutdownNow();
        luceneExecutor.close();
    }
    /**
     * Creates a personal fork of the specified repository. The clone is view
     * restricted by default and the owner of the source repository is given
     * access to the clone.
     *
     * @param repository
     * @param user
     * @return true, if successful
     */
    public boolean fork(RepositoryModel repository, UserModel user) {
        String cloneName = MessageFormat.format("~{0}/{1}.git", user.username, StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name)));
        String fromUrl = MessageFormat.format("file://{0}/{1}", repositoriesFolder.getAbsolutePath(), repository.name);
        try {
            // clone the repository
            JGitUtils.cloneRepository(repositoriesFolder, cloneName, fromUrl, true, null);
            // create a Gitblit repository model for the clone
            RepositoryModel cloneModel = repository.cloneAs(cloneName);
            cloneModel.owner = user.username;
            updateRepositoryModel(cloneName, cloneModel, false);
            if (AuthorizationControl.NAMED.equals(cloneModel.authorizationControl)) {
                // add the owner of the source repository to the clone's access list
                if (!StringUtils.isEmpty(repository.owner)) {
                    UserModel owner = getUserModel(repository.owner);
                    if (owner != null) {
                        owner.repositories.add(cloneName);
                        updateUserModel(owner.username, owner, false);
                    }
                }
                // inherit origin's access lists
                List<String> users = getRepositoryUsers(repository);
                setRepositoryUsers(cloneModel, users);
                List<String> teams = getRepositoryTeams(repository);
                setRepositoryTeams(cloneModel, teams);
            }
            // add this clone to the cached model
            addToCachedRepositoryList(cloneModel.name, cloneModel);
            return true;
        } catch (Exception e) {
            logger.error("failed to fork", e);
        }
        return false;
    }
}
src/com/gitblit/SyndicationServlet.java
@@ -227,7 +227,7 @@
                commits = JGitUtils.searchRevlogs(repository, objectId, searchString, searchType,
                        offset, length);
            }
            Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository);
            Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository, model.showRemoteBranches);
            // convert RevCommit to SyndicatedEntryModel
            for (RevCommit commit : commits) {
src/com/gitblit/models/ProjectModel.java
@@ -53,6 +53,10 @@
        this.title = "";
        this.description = "";
    }
    public boolean isUserProject() {
        return name.charAt(0) == '~';
    }
    public boolean hasRepository(String name) {
        return repositories.contains(name.toLowerCase());
src/com/gitblit/models/RepositoryModel.java
@@ -20,6 +20,8 @@
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
@@ -68,7 +70,11 @@
    public List<String> postReceiveScripts;
    public List<String> mailingLists;
    public Map<String, String> customFields;
    public String projectPath;
    private String displayName;
    public boolean allowForks;
    public Set<String> forks;
    public String originRepository;
    
    public RepositoryModel() {
        this("", "", "", new Date(0));
@@ -97,6 +103,24 @@
        }
        return localBranches;
    }
    public void addFork(String repository) {
        if (forks == null) {
            forks = new TreeSet<String>();
        }
        forks.add(repository);
    }
    public void removeFork(String repository) {
        if (forks == null) {
            return;
        }
        forks.remove(repository);
    }
    public void resetDisplayName() {
        displayName = null;
    }
    @Override
    public String toString() {
@@ -110,4 +134,29 @@
    public int compareTo(RepositoryModel o) {
        return StringUtils.compareRepositoryNames(name, o.name);
    }
    public boolean isPersonalRepository() {
        return !StringUtils.isEmpty(projectPath) && projectPath.charAt(0) == '~';
    }
    public boolean isUsersPersonalRepository(String username) {
        return !StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase("~" + username);
    }
    public RepositoryModel cloneAs(String cloneName) {
        RepositoryModel clone = new RepositoryModel();
        clone.name = cloneName;
        clone.description = description;
        clone.accessRestriction = accessRestriction;
        clone.authorizationControl = authorizationControl;
        clone.federationStrategy = federationStrategy;
        clone.showReadme = showReadme;
        clone.showRemoteBranches = false;
        clone.allowForks = false;
        clone.useDocs = useDocs;
        clone.useTickets = useTickets;
        clone.skipSizeCalculation = skipSizeCalculation;
        clone.skipSummaryMetrics = skipSummaryMetrics;
        return clone;
    }
}
src/com/gitblit/models/UserModel.java
@@ -20,6 +20,7 @@
import java.util.HashSet;
import java.util.Set;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.utils.StringUtils;
@@ -42,6 +43,7 @@
    public String displayName;
    public String emailAddress;
    public boolean canAdmin;
    public boolean canFork;
    public boolean excludeFromFederation;
    public final Set<String> repositories = new HashSet<String>();
    public final Set<TeamModel> teams = new HashSet<TeamModel>();
@@ -83,6 +85,33 @@
        }
        return false;
    }
    public boolean canForkRepository(RepositoryModel repository) {
        if (canAdmin) {
            return true;
        }
        if (!canFork) {
            // user has been prohibited from forking
            return false;
        }
        if (!isAuthenticated) {
            // unauthenticated user model
            return false;
        }
        if (("~" + username).equalsIgnoreCase(repository.projectPath)) {
            // this repository is already a personal repository
            return false;
        }
        if (!repository.allowForks) {
            // repository prohibits forks
            return false;
        }
        if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) {
            return canAccessRepository(repository);
        }
        // repository is not clone-restricted
        return true;
    }
    public boolean hasRepository(String name) {
        return repositories.contains(name.toLowerCase());
src/com/gitblit/utils/ActivityUtils.java
@@ -94,7 +94,7 @@
                    branches.add(objectId);
                }
                Map<ObjectId, List<RefModel>> allRefs = JGitUtils
                        .getAllRefs(repository);
                        .getAllRefs(repository, model.showRemoteBranches);
                for (String branch : branches) {
                    String shortName = branch;
src/com/gitblit/utils/JGitUtils.java
@@ -1369,9 +1369,23 @@
     * @return all refs grouped by their referenced object id
     */
    public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository) {
        return getAllRefs(repository, true);
    }
    /**
     * Returns all refs grouped by their associated object id.
     *
     * @param repository
     * @param includeRemoteRefs
     * @return all refs grouped by their referenced object id
     */
    public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository, boolean includeRemoteRefs) {
        List<RefModel> list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1);
        Map<ObjectId, List<RefModel>> refs = new HashMap<ObjectId, List<RefModel>>();
        for (RefModel ref : list) {
            if (!includeRemoteRefs && ref.getName().startsWith(Constants.R_REMOTES)) {
                continue;
            }
            ObjectId objectid = ref.getReferencedObjectId();
            if (!refs.containsKey(objectid)) {
                refs.put(objectid, new ArrayList<RefModel>());
src/com/gitblit/utils/StringUtils.java
@@ -367,7 +367,7 @@
     * @return the first invalid character found or null if string is acceptable
     */
    public static Character findInvalidCharacter(String name) {
        char[] validChars = { '/', '.', '_', '-' };
        char[] validChars = { '/', '.', '_', '-', '~' };
        for (char c : name.toCharArray()) {
            if (!Character.isLetterOrDigit(c)) {
                boolean ok = false;
@@ -660,4 +660,31 @@
        }
        return input;
    }
    /**
     * Returns the first path element of a path string.  If no path separator is
     * found in the path, an empty string is returned.
     *
     * @param path
     * @return the first element in the path
     */
    public static String getFirstPathElement(String path) {
        if (path.indexOf('/') > -1) {
            return path.substring(0, path.indexOf('/')).trim();
        }
        return "";
    }
    /**
     * Returns the last path element of a path string
     *
     * @param path
     * @return the last element in the path
     */
    public static String getLastPathElement(String path) {
        if (path.indexOf('/') > -1) {
            return path.substring(path.lastIndexOf('/') + 1);
        }
        return path;
    }
}
src/com/gitblit/wicket/GitBlitWebApp.java
@@ -34,6 +34,7 @@
import com.gitblit.wicket.pages.CommitPage;
import com.gitblit.wicket.pages.DocsPage;
import com.gitblit.wicket.pages.FederationRegistrationPage;
import com.gitblit.wicket.pages.ForksPage;
import com.gitblit.wicket.pages.GitSearchPage;
import com.gitblit.wicket.pages.GravatarProfilePage;
import com.gitblit.wicket.pages.HistoryPage;
@@ -53,6 +54,7 @@
import com.gitblit.wicket.pages.TicketPage;
import com.gitblit.wicket.pages.TicketsPage;
import com.gitblit.wicket.pages.TreePage;
import com.gitblit.wicket.pages.UserPage;
import com.gitblit.wicket.pages.UsersPage;
public class GitBlitWebApp extends WebApplication {
@@ -116,6 +118,8 @@
        mount("/lucene", LuceneSearchPage.class);
        mount("/project", ProjectPage.class, "p");
        mount("/projects", ProjectsPage.class);
        mount("/user", UserPage.class, "user");
        mount("/forks", ForksPage.class, "r");
    }
    private void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -318,4 +318,18 @@
gb.projects = projects
gb.project = project
gb.allProjects = all projects
gb.copyToClipboard = copy to clipboard
gb.copyToClipboard = copy to clipboard
gb.fork = fork
gb.forks = forks
gb.forkRepository = fork {0}?
gb.repositoryForked = {0} has been forked
gb.repositoryForkFailed= failed to fork {1}
gb.personalRepositories = personal repositories
gb.allowForks = allow forks
gb.allowForksDescription = allow authorized users to fork this repository
gb.forkedFrom = forked from
gb.canFork = can fork
gb.canForkDescription = user is permitted to fork authorized repositories
gb.myFork = view my fork
gb.forksProhibited = forks prohibited
gb.forksProhibitedWarning = this repository forbids forks
src/com/gitblit/wicket/pages/BasePage.java
@@ -18,7 +18,6 @@
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -36,6 +35,7 @@
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RedirectToUrlException;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.CSSPackageResource;
import org.apache.wicket.markup.html.WebPage;
@@ -63,7 +63,6 @@
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.LinkPanel;
@@ -235,9 +234,9 @@
        return req.getServerName();
    }
    
    protected String getRepositoryUrl(RepositoryModel repository) {
    public static String getRepositoryUrl(RepositoryModel repository) {
        StringBuilder sb = new StringBuilder();
        sb.append(WicketUtils.getGitblitURL(getRequestCycle().getRequest()));
        sb.append(WicketUtils.getGitblitURL(RequestCycle.get().getRequest()));
        sb.append(Constants.GIT_PATH);
        sb.append(repository.name);
        
@@ -252,7 +251,7 @@
    
    protected List<ProjectModel> getProjectModels() {
        final UserModel user = GitBlitWebSession.get().getUser();
        List<ProjectModel> projects = GitBlit.self().getProjectModels(user);
        List<ProjectModel> projects = GitBlit.self().getProjectModels(user, true);
        return projects;
    }
    
src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -34,10 +34,12 @@
                    </wicket:container>
                </td></tr>
                <tr><th colspan="2"><hr/></th></tr>
                <tr><th><wicket:message key="gb.allowForks"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="allowForks" tabindex="17" /> &nbsp;<span class="help-inline"><wicket:message key="gb.allowForksDescription"></wicket:message></span></label></td></tr>
                <tr><th colspan="2"><hr/></th></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.permittedTeams"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
                <tr><td colspan="2"><h3><wicket:message key="gb.federation"></wicket:message> &nbsp;<small><wicket:message key="gb.federationRepositoryDescription"></wicket:message></small></h3></td></tr>    
                <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="17" /></td></tr>
                <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="18" /></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>
                <tr><td colspan="2"><h3><wicket:message key="gb.search"></wicket:message> &nbsp;<small><wicket:message key="gb.indexedBranchesDescription"></wicket:message></small></h3></td></tr>    
                <tr><th style="vertical-align: top;"><wicket:message key="gb.indexedBranches"></wicket:message></th><td style="padding:2px;"><span wicket:id="indexedBranches"></span></td></tr>
src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -343,6 +343,7 @@
        form.add(new TextField<String>("description"));
        form.add(new DropDownChoice<String>("owner", GitBlit.self().getAllUsernames())
                .setEnabled(GitBlitWebSession.get().canAdmin()));
        form.add(new CheckBox("allowForks"));
        form.add(new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays
                .asList(AccessRestrictionType.values()), new AccessRestrictionRenderer()));
        form.add(new CheckBox("isFrozen"));
src/com/gitblit/wicket/pages/EditUserPage.html
@@ -17,12 +17,13 @@
                <tr><th><wicket:message key="gb.displayName"></wicket:message></th><td class="edit"><input type="text" wicket:id="displayName" size="30" tabindex="4" /></td></tr>
                <tr><th><wicket:message key="gb.emailAddress"></wicket:message></th><td class="edit"><input type="text" wicket:id="emailAddress" size="30" tabindex="5" /></td></tr>
                <tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canAdminDescription"></wicket:message></span></label></td></tr>                
                <tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></span></label></td></tr>
                <tr><th><wicket:message key="gb.canFork"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canFork" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canForkDescription"></wicket:message></span></label></td></tr>
                <tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="8" /> &nbsp;<span class="help-inline"><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></span></label></td></tr>
                <tr><td colspan="2" style="padding-top:15px;"><h3><wicket:message key="gb.accessPermissions"></wicket:message> &nbsp;<small><wicket:message key="gb.accessPermissionsForUserDescription"></wicket:message></small></h3></td></tr>    
                <tr><th style="vertical-align: top;"><wicket:message key="gb.teamMemberships"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
                <tr><td colspan="2"><hr></hr></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
                <tr><td colspan='2'><div class="form-actions"><input class="btn btn-primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="8" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="9" /></div></td></tr>
                <tr><td colspan='2'><div class="form-actions"><input class="btn btn-primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="10" /></div></td></tr>
            </tbody>
        </table>
    </form>    
src/com/gitblit/wicket/pages/EditUserPage.java
@@ -231,6 +231,7 @@
        form.add(new TextField<String>("displayName").setEnabled(editDisplayName));
        form.add(new TextField<String>("emailAddress").setEnabled(editEmailAddress));
        form.add(new CheckBox("canAdmin"));
        form.add(new CheckBox("canFork"));
        form.add(new CheckBox("excludeFromFederation"));
        form.add(repositories);
        form.add(teams.setEnabled(editTeams));
src/com/gitblit/wicket/pages/ForksPage.html
New file
@@ -0,0 +1,24 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="en"
      lang="en">
<body>
<wicket:extend>
    <div class="forkSource">
        <b><span class="repositorySwatch" wicket:id="forkSourceSwatch"></span></b>
        <span wicket:id="forkSourceAvatar" style="vertical-align: baseline;"></span>
        <span wicket:id="forkSourceProject">[a project]</span> / <span wicket:id="forkSource">[a fork]</span>
    </div>
    <div wicket:id="fork">
        <div class="forkEntry">
            <span wicket:id="anAvatar" style="vertical-align: baseline;"></span>
            <span wicket:id="aProject">[a project]</span> / <span wicket:id="aFork">[a fork]</span>
        </div>
    </div>
</wicket:extend>
</body>
</html>
src/com/gitblit/wicket/pages/ForksPage.java
New file
@@ -0,0 +1,133 @@
/*
 * 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.wicket.pages;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.eclipse.jgit.lib.PersonIdent;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
import com.gitblit.wicket.panels.LinkPanel;
public class ForksPage extends RepositoryPage {
    public ForksPage(PageParameters params) {
        super(params);
        RepositoryModel model = getRepositoryModel();
        RepositoryModel origin;
        List<String> list;
        if (ArrayUtils.isEmpty(model.forks)) {
            // origin repository has forks
            origin = GitBlit.self().getRepositoryModel(model.originRepository);
            list = new ArrayList<String>(origin.forks);
        } else {
            // this repository has forks
            origin = model;
            list = new ArrayList<String>(model.forks);
        }
        if (origin.isPersonalRepository()) {
            // personal repository
            UserModel user = GitBlit.self().getUserModel(origin.projectPath.substring(1));
            PersonIdent ident = new PersonIdent(user.getDisplayName(), user.emailAddress);
            add(new GravatarImage("forkSourceAvatar", ident, 20));
            add(new Label("forkSourceSwatch").setVisible(false));
            add(new LinkPanel("forkSourceProject", null, user.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(user.username)));
        } else {
            // standard repository
            add(new GravatarImage("forkSourceAvatar", new PersonIdent("", ""), 20).setVisible(false));
            Component swatch;
            if (origin.isBare){
                swatch = new Label("forkSourceSwatch", "&nbsp;").setEscapeModelStrings(false);
            } else {
                swatch = new Label("forkSourceSwatch", "!");
                WicketUtils.setHtmlTooltip(swatch, getString("gb.workingCopyWarning"));
            }
            WicketUtils.setCssBackground(swatch, origin.toString());
            add(swatch);
            final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
            swatch.setVisible(showSwatch);
            String projectName = origin.projectPath;
            if (StringUtils.isEmpty(projectName)) {
                projectName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
            }
            add(new LinkPanel("forkSourceProject", null, projectName, ProjectPage.class, WicketUtils.newProjectParameter(origin.projectPath)));
        }
        String source = StringUtils.getLastPathElement(origin.name);
        add(new LinkPanel("forkSource", null, StringUtils.stripDotGit(source), SummaryPage.class, WicketUtils.newRepositoryParameter(origin.name)));
        // only display user-accessible forks
        UserModel user = GitBlitWebSession.get().getUser();
        List<RepositoryModel> forks = new ArrayList<RepositoryModel>();
        for (String aFork : list) {
            RepositoryModel fork = GitBlit.self().getRepositoryModel(user, aFork);
            if (fork != null) {
                forks.add(fork);
            }
        }
        ListDataProvider<RepositoryModel> forksDp = new ListDataProvider<RepositoryModel>(forks);
        DataView<RepositoryModel> forksList = new DataView<RepositoryModel>("fork", forksDp) {
            private static final long serialVersionUID = 1L;
            public void populateItem(final Item<RepositoryModel> item) {
                RepositoryModel fork = item.getModelObject();
                if (fork.isPersonalRepository()) {
                    UserModel user = GitBlit.self().getUserModel(fork.projectPath.substring(1));
                    PersonIdent ident = new PersonIdent(user.getDisplayName(), user.emailAddress);
                    item.add(new GravatarImage("anAvatar", ident, 20));
                    item.add(new LinkPanel("aProject", null, user.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(user.username)));
                } else {
                    PersonIdent ident = new PersonIdent(fork.name, fork.name);
                    item.add(new GravatarImage("anAvatar", ident, 20));
                    item.add(new LinkPanel("aProject", null, fork.projectPath, ProjectPage.class, WicketUtils.newProjectParameter(fork.projectPath)));
                }
                String repo = StringUtils.getLastPathElement(fork.name);
                item.add(new LinkPanel("aFork", null, StringUtils.stripDotGit(repo), SummaryPage.class, WicketUtils.newRepositoryParameter(fork.name)));
                WicketUtils.setCssStyle(item, "margin-left:25px;");
            }
        };
        add(forksList);
    }
    @Override
    protected String getPageName() {
        return getString("gb.forks");
    }
}
src/com/gitblit/wicket/pages/GitSearchPage.java
@@ -36,7 +36,7 @@
        int nextPage = pageNumber + 1;
        SearchPanel search = new SearchPanel("searchPanel", repositoryName, objectId, value,
                searchType, getRepository(), -1, pageNumber - 1);
                searchType, getRepository(), -1, pageNumber - 1, getRepositoryModel().showRemoteBranches);
        boolean hasMore = search.hasMore();
        add(search);
src/com/gitblit/wicket/pages/HistoryPage.java
@@ -32,7 +32,7 @@
        int nextPage = pageNumber + 1;
        HistoryPanel history = new HistoryPanel("historyPanel", repositoryName, objectId, path,
                getRepository(), -1, pageNumber - 1);
                getRepository(), -1, pageNumber - 1, getRepositoryModel().showRemoteBranches);
        boolean hasMore = history.hasMore();
        add(history);
src/com/gitblit/wicket/pages/LogPage.java
@@ -37,7 +37,7 @@
            refid = getRepositoryModel().HEAD;
        }
        LogPanel logPanel = new LogPanel("logPanel", repositoryName, refid, getRepository(), -1,
                pageNumber - 1);
                pageNumber - 1, getRepositoryModel().showRemoteBranches);
        boolean hasMore = logPanel.hasMore();
        add(logPanel);
src/com/gitblit/wicket/pages/ProjectPage.html
@@ -7,29 +7,6 @@
<body>
<wicket:extend>
    <wicket:fragment wicket:id="repositoryAdminLinks">
        <span class="link">
            <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
            | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
            | <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
            | <a wicket:id="deleteRepository"><wicket:message key="gb.delete">[delete]</wicket:message></a>
        </span>
    </wicket:fragment>
    <wicket:fragment wicket:id="repositoryOwnerLinks">
        <span class="link">
            <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
            | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
            | <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
        </span>
    </wicket:fragment>
    <wicket:fragment wicket:id="repositoryUserLinks">
        <span class="link">
            <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
            | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
        </span>
    </wicket:fragment>
    <div class="row">
        <div class="span12">
@@ -60,50 +37,11 @@
                        <div class="markdown" wicket:id="repositoriesMessage">[repositories message]</div>
                    </div>
                </div>
                <!-- repositories -->
                <div class="row">
                    <div class="span6" style="border-top:1px solid #eee;" wicket:id="repository">
                        <div style="padding-top:15px;padding-bottom:15px;margin-right:15px;">
                            <div class="pull-right" style="text-align:right;padding-right:15px;">
                                <span wicket:id="repositoryLinks"></span>
                                <div>
                                    <img class="inlineIcon" wicket:id="frozenIcon" />
                                    <img class="inlineIcon" wicket:id="federatedIcon" />
                                    <a style="text-decoration: none;" wicket:id="tickets" wicket:message="title:gb.tickets">
                                        <img style="border:0px;vertical-align:middle;" src="bug_16x16.png"></img>
                                    </a>
                                    <a style="text-decoration: none;" wicket:id="docs" wicket:message="title:gb.docs">
                                        <img style="border:0px;vertical-align:middle;" src="book_16x16.png"></img>
                                    </a>
                                    <a style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
                                        <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
                                    </a>
                                </div>
                                <span style="color: #999;font-style:italic;font-size:0.8em;" wicket:id="repositoryOwner">[owner]</span>
                            </div>
                            <h3><span class="repositorySwatch" wicket:id="repositorySwatch"></span>
                                <span style="padding-left:3px;" wicket:id="repositoryName">[repository name]</span>
                                <img class="inlineIcon" wicket:id="accessRestrictionIcon" />
                            </h3>
                            <div style="padding-left:20px;">
                                <div style="padding-bottom:10px" wicket:id="repositoryDescription">[repository description]</div>
                                <div style="color: #999;">
                                    <wicket:message key="gb.lastChange">[last change]</wicket:message> <span wicket:id="repositoryLastChange">[last change]</span>,
                                    <span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span>
                                </div>
                                <div class="hidden-phone hidden-tablet" wicket:id="repositoryCloneUrl">[repository clone url]</div>
                            </div>
                        </div>
                    </div>
                </div>
                    <div class="span6" style="border-bottom:1px solid #eee;" wicket:id="repositoryList">
                        <span wicket:id="repository"></span>
                    </div>
                </div>
            </div>
            
            <!-- activity tab -->
src/com/gitblit/wicket/pages/ProjectPage.java
@@ -34,10 +34,7 @@
import org.apache.wicket.RedirectException;
import org.apache.wicket.behavior.HeaderContributor;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
@@ -52,7 +49,6 @@
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ActivityUtils;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebApp;
@@ -66,9 +62,7 @@
import com.gitblit.wicket.charting.GoogleLineChart;
import com.gitblit.wicket.charting.GooglePieChart;
import com.gitblit.wicket.panels.ActivityPanel;
import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.RepositoryUrlPanel;
import com.gitblit.wicket.panels.ProjectRepositoryPanel;
public class ProjectPage extends RootPage {
    
@@ -148,143 +142,16 @@
            }
        });
        final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
        final boolean gitServlet = GitBlit.getBoolean(Keys.git.enableGitServlet, true);
        final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
        final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories);
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repository", dp) {
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repositoryList", dp) {
            private static final long serialVersionUID = 1L;
            public void populateItem(final Item<RepositoryModel> item) {
                final RepositoryModel entry = item.getModelObject();
                // repository swatch
                Component swatch;
                if (entry.isBare){
                    swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
                } else {
                    swatch = new Label("repositorySwatch", "!");
                    WicketUtils.setHtmlTooltip(swatch, getString("gb.workingCopyWarning"));
                }
                WicketUtils.setCssBackground(swatch, entry.toString());
                item.add(swatch);
                swatch.setVisible(showSwatch);
                
                PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
                item.add(new LinkPanel("repositoryName", "list", StringUtils.getRelativePath(projectPath, StringUtils.stripDotGit(entry.name)), SummaryPage.class, pp));
                item.add(new Label("repositoryDescription", entry.description).setVisible(!StringUtils.isEmpty(entry.description)));
                item.add(new BookmarkablePageLink<Void>("tickets", TicketsPage.class, pp).setVisible(entry.useTickets));
                item.add(new BookmarkablePageLink<Void>("docs", DocsPage.class, pp).setVisible(entry.useDocs));
                if (entry.isFrozen) {
                    item.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png",
                            getString("gb.isFrozen")));
                } else {
                    item.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
                }
                if (entry.isFederated) {
                    item.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png",
                            getString("gb.isFederated")));
                } else {
                    item.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
                }
                switch (entry.accessRestriction) {
                case NONE:
                    item.add(WicketUtils.newBlankImage("accessRestrictionIcon").setVisible(false));
                    break;
                case PUSH:
                    item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",
                            getAccessRestrictions().get(entry.accessRestriction)));
                    break;
                case CLONE:
                    item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",
                            getAccessRestrictions().get(entry.accessRestriction)));
                    break;
                case VIEW:
                    item.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",
                            getAccessRestrictions().get(entry.accessRestriction)));
                    break;
                default:
                    item.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
                }
                item.add(new Label("repositoryOwner", StringUtils.isEmpty(entry.owner) ? "" : (entry.owner + " (" + getString("gb.owner") + ")")));
                UserModel user = GitBlitWebSession.get().getUser();
                Fragment repositoryLinks;
                boolean showOwner = user != null && user.username.equalsIgnoreCase(entry.owner);
                if (showAdmin || showOwner) {
                    repositoryLinks = new Fragment("repositoryLinks",
                            showAdmin ? "repositoryAdminLinks" : "repositoryOwnerLinks", this);
                    repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository",
                            EditRepositoryPage.class, WicketUtils
                                    .newRepositoryParameter(entry.name)));
                    if (showAdmin) {
                        Link<Void> deleteLink = new Link<Void>("deleteRepository") {
                            private static final long serialVersionUID = 1L;
                            @Override
                            public void onClick() {
                                if (GitBlit.self().deleteRepositoryModel(entry)) {
                                    info(MessageFormat.format(getString("gb.repositoryDeleted"), entry));
                                    // TODO dp.remove(entry);
                                } else {
                                    error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), entry));
                                }
                            }
                        };
                        deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
                                getString("gb.deleteRepository"), entry)));
                        repositoryLinks.add(deleteLink);
                    }
                } else {
                    repositoryLinks = new Fragment("repositoryLinks", "repositoryUserLinks", this);
                }
                repositoryLinks.add(new BookmarkablePageLink<Void>("tree", TreePage.class,
                        WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
                repositoryLinks.add(new BookmarkablePageLink<Void>("log", LogPage.class,
                        WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
                item.add(repositoryLinks);
                String lastChange;
                if (entry.lastChange.getTime() == 0) {
                    lastChange = "--";
                } else {
                    lastChange = getTimeUtils().timeAgo(entry.lastChange);
                }
                Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
                item.add(lastChangeLabel);
                WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
                if (entry.hasCommits) {
                    // Existing repository
                    item.add(new Label("repositorySize", entry.size).setVisible(showSize));
                } else {
                    // New repository
                    item.add(new Label("repositorySize", getString("gb.empty"))
                            .setEscapeModelStrings(false));
                }
                item.add(new ExternalLink("syndication", SyndicationServlet.asLink("",
                        entry.name, null, 0)));
                List<String> repositoryUrls = new ArrayList<String>();
                if (gitServlet) {
                    // add the Gitblit repository url
                    repositoryUrls.add(getRepositoryUrl(entry));
                }
                repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(entry.name));
                String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.remove(0);
                item.add(new RepositoryUrlPanel("repositoryCloneUrl", primaryUrl));
                ProjectRepositoryPanel row = new ProjectRepositoryPanel("repository",
                        getLocalizer(), this, showAdmin, entry, getAccessRestrictions());
                item.add(row);
            }
        };
        add(dataView);
@@ -434,7 +301,7 @@
    protected List<ProjectModel> getProjectModels() {
        if (projectModels.isEmpty()) {
            final UserModel user = GitBlitWebSession.get().getUser();
            List<ProjectModel> projects = GitBlit.self().getProjectModels(user);
            List<ProjectModel> projects = GitBlit.self().getProjectModels(user, false);
            projectModels.addAll(projects);
        }
        return projectModels;
@@ -451,7 +318,12 @@
    
    protected List<DropDownMenuItem> getProjectsMenu() {
        List<DropDownMenuItem> menu = new ArrayList<DropDownMenuItem>();
        List<ProjectModel> projects = getProjectModels();
        List<ProjectModel> projects = new ArrayList<ProjectModel>();
        for (ProjectModel model : getProjectModels()) {
            if (!model.isUserProject()) {
                projects.add(model);
            }
        }
        int maxProjects = 15;
        boolean showAllProjects = projects.size() > maxProjects;
        if (showAllProjects) {
src/com/gitblit/wicket/pages/ProjectsPage.java
@@ -36,6 +36,7 @@
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
@@ -63,6 +64,13 @@
    protected boolean reusePageParameters() {
        return true;
    }
    @Override
    protected List<ProjectModel> getProjectModels() {
        final UserModel user = GitBlitWebSession.get().getUser();
        List<ProjectModel> projects = GitBlit.self().getProjectModels(user, false);
        return projects;
    }
    private void setup(PageParameters params) {
        setupPage("", "");
src/com/gitblit/wicket/pages/RepositoryPage.html
@@ -42,9 +42,18 @@
            <!-- page header -->
            <div class="pageTitle">
                <div class="row">
                    <div wicket:id="workingCopy"></div>
                    <div class="span9">
                        <h2><span wicket:id="repositoryName">[repository name]</span> <small><span wicket:id="pageName">[page name]</span></small></h2>
                    <div class="controls">
                        <span wicket:id="workingCopyIndicator"></span>
                        <span wicket:id="forksProhibitedIndicator"></span>
                        <div class="hidden-phone btn-group pull-right">
                            <!-- future spot for other repo buttons -->
                            <a class="btn" wicket:id="myForkLink"><i class="icon-random"></i> <wicket:message key="gb.myFork"></wicket:message></a>
                            <a class="btn" wicket:id="forkLink"><i class="icon-random"></i> <wicket:message key="gb.fork"></wicket:message></a>
                        </div>
                    </div>
                    <div class="span7">
                        <div><span class="project" wicket:id="projectTitle">[project title]</span>/<span class="repository" wicket:id="repositoryName">[repository name]</span> <span wicket:id="pageName">[page name]</span></div>
                        <span wicket:id="originRepository">[origin repository]</span>
                    </div>
                </div>
            </div>
@@ -52,11 +61,22 @@
            <wicket:child />
        </div>
        
        <wicket:fragment wicket:id="workingCopyFragment">
            <p class="pull-right" style="padding-top:5px;">
                <span class="alert alert-info" style="padding: 8px 14px 8px 14px;vertical-align: middle;"><i class="icon-exclamation-sign" style="vertical-align: middle;"></i>&nbsp;<span class="hidden-phone" wicket:id="workingCopy" style="font-weight:bold;">[working copy]</span></span>
            </p>
        <wicket:fragment wicket:id="originFragment">
            <p class="originRepository"><wicket:message key="gb.forkedFrom">[forked from]</wicket:message> <span wicket:id="originRepository">[origin repository]</span></p>
        </wicket:fragment>
        <wicket:fragment wicket:id="workingCopyFragment">
            <div class="pull-right" style="padding-top:0px;margin-bottom:0px;padding-left:5px">
                <span class="alert alert-info" style="padding: 6px 14px 6px 14px;vertical-align: middle;"><i class="icon-exclamation-sign"></i>&nbsp;<span class="hidden-phone" wicket:id="workingCopy" style="font-weight:bold;">[working copy]</span></span>
            </div>
        </wicket:fragment>
        <wicket:fragment wicket:id="forksProhibitedFragment">
            <div class="pull-right" style="padding-top:0px;margin-bottom:0px;padding-left:5px">
                <span class="alert alert-error" style="padding: 6px 14px 6px 14px;vertical-align: middle;"><i class="icon-ban-circle"></i>&nbsp;<span class="hidden-phone" wicket:id="forksProhibited" style="font-weight:bold;">[forks prohibited]</span></span>
            </div>
        </wicket:fragment>
    </wicket:extend>
</body>
</html>
src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -28,10 +28,12 @@
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RedirectException;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
@@ -47,8 +49,10 @@
import com.gitblit.Keys;
import com.gitblit.PagesServlet;
import com.gitblit.SyndicationServlet;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
@@ -58,6 +62,7 @@
import com.gitblit.wicket.PageRegistration.OtherPageLink;
import com.gitblit.wicket.SessionlessForm;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.NavigationPanel;
import com.gitblit.wicket.panels.RefsPanel;
@@ -81,10 +86,11 @@
    public RepositoryPage(PageParameters params) {
        super(params);
        repositoryName = WicketUtils.getRepositoryName(params);
        if (repositoryName.indexOf('/') > -1) {
            projectName = repositoryName.substring(0, repositoryName.indexOf('/'));
        } else {
        String root =StringUtils.getFirstPathElement(repositoryName);
        if (StringUtils.isEmpty(root)) {
            projectName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
        } else {
            projectName = root;
        }
        objectId = WicketUtils.getObject(params);
        
@@ -125,7 +131,6 @@
        // standard links
        pages.put("repositories", new PageRegistration("gb.repositories", RepositoriesPage.class));
        pages.put("project", new PageRegistration("gb.project", ProjectPage.class, WicketUtils.newProjectParameter(projectName)));
        pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
        pages.put("log", new PageRegistration("gb.log", LogPage.class, params));
        pages.put("branches", new PageRegistration("gb.branches", BranchesPage.class, params));
@@ -136,6 +141,17 @@
        Repository r = getRepository();
        RepositoryModel model = getRepositoryModel();
        // forks list button
        if (StringUtils.isEmpty(model.originRepository)) {
            if (!ArrayUtils.isEmpty(model.forks)) {
                // this origin repository has forks
                pages.put("forks", new PageRegistration("gb.forks", ForksPage.class, params));
            }
        } else {
            // this is a fork of another repository
            pages.put("forks", new PageRegistration("gb.forks", ForksPage.class, params));
        }
        // per-repository extra page links
        if (model.useTickets && TicgitUtils.getTicketsBranch(r) != null) {
            pages.put("tickets", new PageRegistration("gb.tickets", TicketsPage.class, params));
@@ -168,19 +184,100 @@
    @Override
    protected void setupPage(String repositoryName, String pageName) {
        add(new LinkPanel("repositoryName", null, StringUtils.stripDotGit(repositoryName),
                SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryName)));
        add(new Label("pageName", pageName).setRenderBodyOnly(true));
        if (getRepositoryModel().isBare) {
            add(new Label("workingCopy").setVisible(false));
        String projectName = StringUtils.getFirstPathElement(repositoryName);
        ProjectModel project = GitBlit.self().getProjectModel(projectName);
        if (project.isUserProject()) {
            // user-as-project
            add(new LinkPanel("projectTitle", null, project.getDisplayName(),
                    UserPage.class, WicketUtils.newUsernameParameter(project.name.substring(1))));
        } else {
            Fragment fragment = new Fragment("workingCopy", "workingCopyFragment", this);
            // project
            add(new LinkPanel("projectTitle", null, project.name,
                    ProjectPage.class, WicketUtils.newProjectParameter(project.name)));
        }
        String name = StringUtils.stripDotGit(repositoryName);
        if (!StringUtils.isEmpty(projectName) && name.startsWith(projectName)) {
            name = name.substring(projectName.length() + 1);
        }
        add(new LinkPanel("repositoryName", null, name, SummaryPage.class,
                WicketUtils.newRepositoryParameter(repositoryName)));
        add(new Label("pageName", pageName).setRenderBodyOnly(true));
        // indicate origin repository
        RepositoryModel model = getRepositoryModel();
        if (StringUtils.isEmpty(model.originRepository)) {
            add(new Label("originRepository").setVisible(false));
        } else {
            Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
            forkFrag.add(new LinkPanel("originRepository", null, StringUtils.stripDotGit(model.originRepository),
                    SummaryPage.class, WicketUtils.newRepositoryParameter(model.originRepository)));
            add(forkFrag);
        }
        if (getRepositoryModel().isBare) {
            add(new Label("workingCopyIndicator").setVisible(false));
        } else {
            Fragment wc = new Fragment("workingCopyIndicator", "workingCopyFragment", this);
            Label lbl = new Label("workingCopy", getString("gb.workingCopy"));
            WicketUtils.setHtmlTooltip(lbl,  getString("gb.workingCopyWarning"));
            fragment.add(lbl);
            add(fragment);
            wc.add(lbl);
            add(wc);
        }
        if (getRepositoryModel().allowForks) {
            add(new Label("forksProhibitedIndicator").setVisible(false));
        } else {
            Fragment wc = new Fragment("forksProhibitedIndicator", "forksProhibitedFragment", this);
            Label lbl = new Label("forksProhibited", getString("gb.forksProhibited"));
            WicketUtils.setHtmlTooltip(lbl,  getString("gb.forksProhibitedWarning"));
            wc.add(lbl);
            add(wc);
        }
        UserModel user = GitBlitWebSession.get().getUser();
        // fork button
        if (user != null) {
            final String clonedRepo = MessageFormat.format("~{0}/{1}.git", user.username, StringUtils.stripDotGit(StringUtils.getLastPathElement(model.name)));
            boolean hasClone = GitBlit.self().hasRepository(clonedRepo) && !getRepositoryModel().name.equals(clonedRepo);
            if (user.canForkRepository(model) && !hasClone) {
                Link<Void> forkLink = new Link<Void>("forkLink") {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void onClick() {
                        RepositoryModel model = getRepositoryModel();
                        if (GitBlit.self().fork(model, GitBlitWebSession.get().getUser())) {
                            throw new RedirectException(SummaryPage.class, WicketUtils.newRepositoryParameter(clonedRepo));
                        } else {
                            error(MessageFormat.format(getString("gb.repositoryForkFailed"), model));
                        }
                    }
                };
                forkLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
                        getString("gb.forkRepository"), getRepositoryModel())));
                add(forkLink);
            } else {
                // user not allowed to fork or fork already exists or repo forbids forking
                add(new ExternalLink("forkLink", "").setVisible(false));
            }
            if (hasClone) {
                // user has clone
                String url = getRequestCycle().urlFor(SummaryPage.class, WicketUtils.newRepositoryParameter(clonedRepo)).toString();
                add(new ExternalLink("myForkLink", url));
            } else {
                // user does not have clone
                add(new ExternalLink("myForkLink", "").setVisible(false));
            }
        } else {
            // server prohibits forking
            add(new ExternalLink("forkLink", "").setVisible(false));
            add(new ExternalLink("myForkLink", "").setVisible(false));
        }
        super.setupPage(repositoryName, pageName);
    }
@@ -312,7 +409,7 @@
    }
    protected void addRefs(Repository r, RevCommit c) {
        add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r)));
        add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r, getRepositoryModel().showRemoteBranches)));
    }
    protected void addFullText(String wicketId, String text, boolean substituteRegex) {
src/com/gitblit/wicket/pages/RootPage.java
@@ -184,6 +184,9 @@
                // remove named repository parameter
                params.remove("r");
                // remove named user parameter
                params.remove("user");
                // remove days back parameter if it is the default value
                if (params.containsKey("db")
                        && params.getInt("db") == GitBlit.getInteger(Keys.web.activityDuration, 14)) {
@@ -327,6 +330,12 @@
        boolean hasParameter = false;
        String projectName = WicketUtils.getProjectName(params);
        String userName = WicketUtils.getUsername(params);
        if (StringUtils.isEmpty(projectName)) {
            if (!StringUtils.isEmpty(userName)) {
                projectName = "~" + userName;
            }
        }
        String repositoryName = WicketUtils.getRepositoryName(params);
        String set = WicketUtils.getSet(params);
        String regex = WicketUtils.getRegEx(params);
src/com/gitblit/wicket/pages/SummaryPage.java
@@ -130,7 +130,7 @@
        add(new Label("otherUrls", StringUtils.flattenStrings(repositoryUrls, "<br/>"))
        .setEscapeModelStrings(false));
        add(new LogPanel("commitsPanel", repositoryName, getRepositoryModel().HEAD, r, numberCommits, 0));
        add(new LogPanel("commitsPanel", repositoryName, getRepositoryModel().HEAD, r, numberCommits, 0, getRepositoryModel().showRemoteBranches));
        add(new TagsPanel("tagsPanel", repositoryName, r, numberRefs).hideIfEmpty());
        add(new BranchesPanel("branchesPanel", getRepositoryModel(), r, numberRefs, false).hideIfEmpty());
src/com/gitblit/wicket/pages/UserPage.html
New file
@@ -0,0 +1,44 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="en"
      lang="en">
<body>
<wicket:extend>
    <div class="row">
        <div class="span4">
            <div wicket:id="gravatar"></div>
            <div style="text-align: left;">
                <h2><span wicket:id="userDisplayName"></span></h2>
                <div><i class="icon-user"></i> <span wicket:id="userUsername"></span></div>
                <div><i class="icon-envelope"></i><span wicket:id="userEmail"></span></div>
            </div>
        </div>
        <div class="span8">
            <div class="tabbable">
                <!-- tab titles -->
                <ul class="nav nav-tabs">
                    <li class="active"><a href="#repositories" data-toggle="tab"><wicket:message key="gb.repositories"></wicket:message></a></li>
                </ul>
                <!-- tab content -->
                <div class="tab-content">
                    <!-- repositories tab -->
                    <div class="tab-pane active" id="repositories">
                        <table width="100%">
                            <tbody>
                                <tr wicket:id="repositoryList"><td style="border-bottom:1px solid #eee;"><span wicket:id="repository"></span></td></tr>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
</wicket:extend>
</body>
</html>
src/com/gitblit/wicket/pages/UserPage.java
New file
@@ -0,0 +1,146 @@
/*
 * 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.wicket.pages;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RedirectException;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.eclipse.jgit.lib.PersonIdent;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebApp;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.ProjectRepositoryPanel;
public class UserPage extends RootPage {
    List<ProjectModel> projectModels = new ArrayList<ProjectModel>();
    public UserPage() {
        super();
        throw new RedirectException(GitBlitWebApp.get().getHomePage());
    }
    public UserPage(PageParameters params) {
        super(params);
        setup(params);
    }
    @Override
    protected boolean reusePageParameters() {
        return true;
    }
    private void setup(PageParameters params) {
        setupPage("", "");
        // check to see if we should display a login message
        boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
        if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
            authenticationError("Please login");
            return;
        }
        String userName = WicketUtils.getUsername(params);
        if (StringUtils.isEmpty(userName)) {
            throw new RedirectException(GitBlitWebApp.get().getHomePage());
        }
        UserModel user = GitBlit.self().getUserModel(userName);
        if (user == null) {
            // construct a temporary user model
            user = new UserModel(userName);
        }
        String projectName = "~" + userName;
        ProjectModel project = GitBlit.self().getProjectModel(projectName);
        if (project == null) {
            throw new RedirectException(GitBlitWebApp.get().getHomePage());
        }
        add(new Label("userDisplayName", user.getDisplayName()));
        add(new Label("userUsername", user.username));
        LinkPanel email = new LinkPanel("userEmail", null, user.emailAddress, "mailto:#");
        email.setRenderBodyOnly(true);
        add(email.setVisible(GitBlit.getBoolean(Keys.web.showEmailAddresses, true) && !StringUtils.isEmpty(user.emailAddress)));
        PersonIdent person = new PersonIdent(user.getDisplayName(), user.emailAddress);
        add(new GravatarImage("gravatar", person, 210));
        List<RepositoryModel> repositories = getRepositories(params);
        Collections.sort(repositories, new Comparator<RepositoryModel>() {
            @Override
            public int compare(RepositoryModel o1, RepositoryModel o2) {
                // reverse-chronological sort
                return o2.lastChange.compareTo(o1.lastChange);
            }
        });
        final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories);
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repositoryList", dp) {
            private static final long serialVersionUID = 1L;
            public void populateItem(final Item<RepositoryModel> item) {
                final RepositoryModel entry = item.getModelObject();
                ProjectRepositoryPanel row = new ProjectRepositoryPanel("repository",
                        getLocalizer(), this, showAdmin, entry, getAccessRestrictions());
                item.add(row);
            }
        };
        add(dataView);
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
                UserPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
        // preserve repository filter option on time choices
        menu.menuItems.addAll(getTimeFilterItems(params));
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
        }
        pages.add(menu);
    }
}
src/com/gitblit/wicket/panels/GravatarImage.java
@@ -48,7 +48,7 @@
    public GravatarImage(String id, PersonIdent person, int width) {
        super(id);
        String email = person.getEmailAddress().toLowerCase();
        String email = person.getEmailAddress() == null ? person.getName().toLowerCase() : person.getEmailAddress().toLowerCase();
        String hash = StringUtils.getMD5(email);
        Link<Void> link = new BookmarkablePageLink<Void>("link", GravatarProfilePage.class,
                WicketUtils.newObjectParameter(hash));
src/com/gitblit/wicket/panels/HistoryPanel.java
@@ -54,7 +54,7 @@
    private boolean hasMore;
    public HistoryPanel(String wicketId, final String repositoryName, final String objectId,
            final String path, Repository r, int limit, int pageOffset) {
            final String path, Repository r, int limit, int pageOffset, boolean showRemoteRefs) {
        super(wicketId);
        boolean pageResults = limit <= 0;
        int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
@@ -74,7 +74,7 @@
        }
        final boolean isTree = matchingPath == null ? true : matchingPath.isTree();
        final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r);
        final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
        List<RevCommit> commits;
        if (pageResults) {
            // Paging result set
src/com/gitblit/wicket/panels/LogPanel.java
@@ -49,7 +49,7 @@
    private boolean hasMore;
    public LogPanel(String wicketId, final String repositoryName, final String objectId,
            Repository r, int limit, int pageOffset) {
            Repository r, int limit, int pageOffset, boolean showRemoteRefs) {
        super(wicketId);
        boolean pageResults = limit <= 0;
        int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
@@ -57,7 +57,7 @@
            itemsPerPage = 50;
        }
        final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r);
        final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
        List<RevCommit> commits;
        if (pageResults) {
            // Paging result set
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
New file
@@ -0,0 +1,79 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="en"
      lang="en">
<wicket:panel>
    <wicket:fragment wicket:id="repositoryAdminLinks">
        <span class="link">
            <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
            | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
            | <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
            | <a wicket:id="deleteRepository"><wicket:message key="gb.delete">[delete]</wicket:message></a>
        </span>
    </wicket:fragment>
    <wicket:fragment wicket:id="repositoryOwnerLinks">
        <span class="link">
            <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
            | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
            | <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
        </span>
    </wicket:fragment>
    <wicket:fragment wicket:id="repositoryUserLinks">
        <span class="link">
            <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
            | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
        </span>
    </wicket:fragment>
    <wicket:fragment wicket:id="originFragment">
        <p class="originRepository" style="margin-left:20px;" ><wicket:message key="gb.forkedFrom">[forked from]</wicket:message> <span wicket:id="originRepository">[origin repository]</span></p>
    </wicket:fragment>
    <div>
        <div style="padding-top:15px;padding-bottom:15px;margin-right:15px;">
            <div class="pull-right" style="text-align:right;padding-right:15px;">
                <span wicket:id="repositoryLinks"></span>
                <div>
                    <img class="inlineIcon" wicket:id="frozenIcon" />
                    <img class="inlineIcon" wicket:id="federatedIcon" />
                    <a style="text-decoration: none;" wicket:id="tickets" wicket:message="title:gb.tickets">
                        <img style="border:0px;vertical-align:middle;" src="bug_16x16.png"></img>
                    </a>
                    <a style="text-decoration: none;" wicket:id="docs" wicket:message="title:gb.docs">
                        <img style="border:0px;vertical-align:middle;" src="book_16x16.png"></img>
                    </a>
                    <a style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
                        <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
                    </a>
                </div>
                <span style="color: #999;font-style:italic;font-size:0.8em;" wicket:id="repositoryOwner">[owner]</span>
            </div>
            <div class="pageTitle" style="border:0px;">
                <div>
                    <span class="repositorySwatch" wicket:id="repositorySwatch"></span>
                    <span class="repository" style="padding-left:3px;color:black;" wicket:id="repositoryName">[repository name]</span>
                    <img class="inlineIcon" style="vertical-align:baseline" wicket:id="accessRestrictionIcon" />
                </div>
                <span wicket:id="originRepository">[origin repository]</span>
            </div>
            <div style="padding-left:20px;">
                <div style="padding-bottom:10px" wicket:id="repositoryDescription">[repository description]</div>
                <div style="color: #999;">
                    <wicket:message key="gb.lastChange">[last change]</wicket:message> <span wicket:id="repositoryLastChange">[last change]</span>,
                    <span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span>
                </div>
                <div class="hidden-phone hidden-tablet" wicket:id="repositoryCloneUrl">[repository clone url]</div>
            </div>
        </div>
    </div>
</wicket:panel>
</html>
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
New file
@@ -0,0 +1,199 @@
/*
 * 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.wicket.panels;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.wicket.Component;
import org.apache.wicket.Localizer;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Fragment;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.SyndicationServlet;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.BasePage;
import com.gitblit.wicket.pages.DocsPage;
import com.gitblit.wicket.pages.EditRepositoryPage;
import com.gitblit.wicket.pages.LogPage;
import com.gitblit.wicket.pages.SummaryPage;
import com.gitblit.wicket.pages.TicketsPage;
import com.gitblit.wicket.pages.TreePage;
public class ProjectRepositoryPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    public ProjectRepositoryPanel(String wicketId, Localizer localizer, Component owner,
            final boolean isAdmin, final RepositoryModel entry,
            final Map<AccessRestrictionType, String> accessRestrictions) {
        super(wicketId);
        final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
        final boolean gitServlet = GitBlit.getBoolean(Keys.git.enableGitServlet, true);
        final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
        // repository swatch
        Component swatch;
        if (entry.isBare) {
            swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
        } else {
            swatch = new Label("repositorySwatch", "!");
            WicketUtils.setHtmlTooltip(swatch, localizer.getString("gb.workingCopyWarning", owner));
        }
        WicketUtils.setCssBackground(swatch, entry.toString());
        add(swatch);
        swatch.setVisible(showSwatch);
        PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
        add(new LinkPanel("repositoryName", "list", StringUtils.getRelativePath(entry.projectPath,
                StringUtils.stripDotGit(entry.name)), SummaryPage.class, pp));
        add(new Label("repositoryDescription", entry.description).setVisible(!StringUtils
                .isEmpty(entry.description)));
        if (StringUtils.isEmpty(entry.originRepository)) {
            add(new Label("originRepository").setVisible(false));
        } else {
            Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
            forkFrag.add(new LinkPanel("originRepository", null, StringUtils.stripDotGit(entry.originRepository),
                    SummaryPage.class, WicketUtils.newRepositoryParameter(entry.originRepository)));
            add(forkFrag);
        }
        add(new BookmarkablePageLink<Void>("tickets", TicketsPage.class, pp).setVisible(entry.useTickets));
        add(new BookmarkablePageLink<Void>("docs", DocsPage.class, pp).setVisible(entry.useDocs));
        if (entry.isFrozen) {
            add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", localizer.getString("gb.isFrozen", owner)));
        } else {
            add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
        }
        if (entry.isFederated) {
            add(WicketUtils.newImage("federatedIcon", "federated_16x16.png", localizer.getString("gb.isFederated", owner)));
        } else {
            add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
        }
        switch (entry.accessRestriction) {
        case NONE:
            add(WicketUtils.newBlankImage("accessRestrictionIcon").setVisible(false));
            break;
        case PUSH:
            add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",
                    accessRestrictions.get(entry.accessRestriction)));
            break;
        case CLONE:
            add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",
                    accessRestrictions.get(entry.accessRestriction)));
            break;
        case VIEW:
            add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",
                    accessRestrictions.get(entry.accessRestriction)));
            break;
        default:
            add(WicketUtils.newBlankImage("accessRestrictionIcon"));
        }
        add(new Label("repositoryOwner", StringUtils.isEmpty(entry.owner) ? "" : (entry.owner + " ("
                + localizer.getString("gb.owner", owner) + ")")));
        UserModel user = GitBlitWebSession.get().getUser();
        Fragment repositoryLinks;
        boolean showOwner = user != null && user.username.equalsIgnoreCase(entry.owner);
        // owner of personal repository gets admin powers
        boolean showAdmin = isAdmin || entry.isUsersPersonalRepository(user.username);
        if (showAdmin || showOwner) {
            repositoryLinks = new Fragment("repositoryLinks", showAdmin ? "repositoryAdminLinks"
                    : "repositoryOwnerLinks", this);
            repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository", EditRepositoryPage.class,
                    WicketUtils.newRepositoryParameter(entry.name)));
            if (showAdmin) {
                Link<Void> deleteLink = new Link<Void>("deleteRepository") {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void onClick() {
                        if (GitBlit.self().deleteRepositoryModel(entry)) {
                            info(MessageFormat.format(getString("gb.repositoryDeleted"), entry));
                            // TODO dp.remove(entry);
                        } else {
                            error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), entry));
                        }
                    }
                };
                deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
                        localizer.getString("gb.deleteRepository", owner), entry)));
                repositoryLinks.add(deleteLink);
            }
        } else {
            repositoryLinks = new Fragment("repositoryLinks", "repositoryUserLinks", this);
        }
        repositoryLinks.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
                .newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
        repositoryLinks.add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils
                .newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
        add(repositoryLinks);
        String lastChange;
        if (entry.lastChange.getTime() == 0) {
            lastChange = "--";
        } else {
            lastChange = getTimeUtils().timeAgo(entry.lastChange);
        }
        Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
        add(lastChangeLabel);
        WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
        if (entry.hasCommits) {
            // Existing repository
            add(new Label("repositorySize", entry.size).setVisible(showSize));
        } else {
            // New repository
            add(new Label("repositorySize", localizer.getString("gb.empty", owner)).setEscapeModelStrings(false));
        }
        add(new ExternalLink("syndication", SyndicationServlet.asLink("", entry.name, null, 0)));
        List<String> repositoryUrls = new ArrayList<String>();
        if (gitServlet) {
            // add the Gitblit repository url
            repositoryUrls.add(BasePage.getRepositoryUrl(entry));
        }
        repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(entry.name));
        String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.remove(0);
        add(new RepositoryUrlPanel("repositoryCloneUrl", primaryUrl));
    }
}
src/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -72,7 +72,7 @@
    
    <wicket:fragment wicket:id="groupRepositoryRow">
        <td colspan="1"><span wicket:id="groupName">[group name]</span></td>
        <td colspan="6"><span class="hidden-phone" style="font-weight:normal;color:#666;" wicket:id="groupDescription">[description]</span></td>
        <td colspan="6" style="padding: 2px;"><span class="hidden-phone" style="font-weight:normal;color:#666;" wicket:id="groupDescription">[description]</span></td>
    </wicket:fragment>
        
    <wicket:fragment wicket:id="repositoryRow">
src/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -58,6 +58,7 @@
import com.gitblit.wicket.pages.ProjectPage;
import com.gitblit.wicket.pages.RepositoriesPage;
import com.gitblit.wicket.pages.SummaryPage;
import com.gitblit.wicket.pages.UserPage;
public class RepositoriesPanel extends BasePanel {
@@ -116,7 +117,7 @@
            }
                        
            Map<String, ProjectModel> projects = new HashMap<String, ProjectModel>();
            for (ProjectModel project : GitBlit.self().getProjectModels(user)) {
            for (ProjectModel project : GitBlit.self().getProjectModels(user, true)) {
                projects.put(project.name, project);
            }
            List<RepositoryModel> groupedModels = new ArrayList<RepositoryModel>();
@@ -138,7 +139,7 @@
        final String baseUrl = WicketUtils.getGitblitURL(getRequest());
        final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {
            private static final long serialVersionUID = 1L;
            int counter;
@@ -156,8 +157,19 @@
                    currGroupName = entry.name;
                    Fragment row = new Fragment("rowContent", "groupRepositoryRow", this);
                    item.add(row);
                    row.add(new LinkPanel("groupName", null, entry.toString(), ProjectPage.class, WicketUtils.newProjectParameter(entry.name)));
                    row.add(new Label("groupDescription", entry.description == null ? "":entry.description));
                    String name = entry.toString();
                    if (name.charAt(0) == '~') {
                        // user page
                        String username = name.substring(1);
                        UserModel user = GitBlit.self().getUserModel(username);
                        row.add(new LinkPanel("groupName", null, user == null ? username : user.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(username)));
                        row.add(new Label("groupDescription", getString("gb.personalRepositories")));
                    } else {
                        // project page
                        row.add(new LinkPanel("groupName", null, name, ProjectPage.class, WicketUtils.newProjectParameter(name)));
                        row.add(new Label("groupDescription", entry.description == null ? "":entry.description));
                    }
                    WicketUtils.setCssClass(item, "group");
                    // reset counter so that first row is light background
                    counter = 0;
@@ -272,7 +284,8 @@
                WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
                boolean showOwner = user != null && user.username.equalsIgnoreCase(entry.owner);
                if (showAdmin) {
                boolean myPersonalRepository = showOwner && entry.isUsersPersonalRepository(user.username);
                if (showAdmin || myPersonalRepository) {
                    Fragment repositoryLinks = new Fragment("repositoryLinks",
                            "repositoryAdminLinks", this);
                    repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository",
src/com/gitblit/wicket/panels/SearchPanel.java
@@ -47,7 +47,8 @@
    private boolean hasMore;
    public SearchPanel(String wicketId, final String repositoryName, final String objectId,
            final String value, Constants.SearchType searchType, Repository r, int limit, int pageOffset) {
            final String value, Constants.SearchType searchType, Repository r, int limit, int pageOffset,
            boolean showRemoteRefs) {
        super(wicketId);
        boolean pageResults = limit <= 0;
        int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
@@ -57,7 +58,7 @@
        RevCommit commit = JGitUtils.getCommit(r, objectId);
        final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r);
        final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
        List<RevCommit> commits;
        if (pageResults) {
            // Paging result set