James Moger
2011-05-19 00afd77a2182ece3d9522d41b86b4ddd7e132288
Owner editing. Frozen status. Grouped repositories. Documentation.
3 files added
21 files modified
487 ■■■■ changed files
distrib/gitblit.properties 10 ●●●●● patch | view | raw | blame | history
docs/00_overview.mkd 37 ●●●●● patch | view | raw | blame | history
docs/01_configuration.mkd 24 ●●●●● patch | view | raw | blame | history
docs/01_eclipse.mkd 17 ●●●● patch | view | raw | blame | history
docs/architecture.odg patch | view | raw | blame | history
docs/architecture.png patch | view | raw | blame | history
docs/sslverify.png patch | view | raw | blame | history
docs/sslverify2.png patch | view | raw | blame | history
src/com/gitblit/BuildSite.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 32 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlitServlet.java 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/StringUtils.java 7 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 4 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/LoginPage.html 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/RepositoryPage.java 19 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/models/RepositoryModel.java 8 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.html 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.java 66 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoriesPage.html 70 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoriesPage.java 155 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/resources/cold_16x16.png patch | view | raw | blame | history
src/com/gitblit/wicket/resources/gitblit.css 15 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/resources/welcome.mkd 4 ●●● patch | view | raw | blame | history
distrib/gitblit.properties
@@ -66,6 +66,16 @@
web.datestampShortFormat = yyyy-MM-dd
web.datetimestampLongFormat = EEEE, MMMM d, yyyy h:mm a z
# Choose how to present the repositories list.
# grouped = group nested/subfolder repositories together (no sorting)
# flat = flat list of repositories (sorting allowed)
web.repositoryListType = flat
# If using a grouped repository list and there are repositories at the
# root level of your repositories folder, you may specify the displayed
# group name with this setting.  This value is only used for web presentation.
web.repositoryRootGroupName = main
# Choose the diff presentation style: gitblt, gitweb, or plain
web.diffStyle = gitblit
docs/00_overview.mkd
@@ -1,6 +1,6 @@
## Overview
Git:Blit is an open-source, integrated pure-Java stack for managing, viewing, and serving [Git](http://git-scm.com) repositories.
Its designed primarily as a tool for small workgroups who want to host Git repositories on a Windows machine.
Its designed primarily as a tool for small workgroups who want to host [Git](http://git-scm.com) repositories on a Windows machine.
Of course, since its pure-Java it should run with any JVM on any platform, but there are already [many compelling Git solutions](https://git.wiki.kernel.org/index.php/InterfacesFrontendsAndTools) for non-Windows environments.
 
@@ -10,46 +10,53 @@
### Features
- Out-of-the-box integrated stack requiring minimal configuration
- JGit SmartHTTP Servlet
- Web and Git Servlet authentication
- JGit SmartHTTP servlet
- Browser and git client authentication
- Four repository access control configurations
    - *Anonymous View, Clone & Push*
    - *Authenticated Push*
    - *Authenticated Clone & Push*
    - *Authenticated View, Clone & Push*    
- Gitweb inspired UI (mostly plain html)
- Repository administration through web UI
- User administration through web UI
- Repositories may also be frozen (deny push) temporarily or permanently
- Gitweb inspired UI
- Administrators may create, edit, rename, or delete repositories through the web UI
- Administrators may create, edit, rename, or delete users through the web UI
- Repository Owners may edit repositories through the web UI
- Automatically generates a self-signed certificate for https communications
- Dates can optionally be displayed using browser's reported timezone
- Author and Committer email address display can be controlled
- Syntax highlighting
- Customizable regular expression handling for commit messages
- Single text file for server configuration
- Single text file for users configuration
- Simple repository stats
- Simple text file for server configuration
- Simple text file for users configuration
- Optional integrated Ticgit
- Optional integrated Markdown
- Optional read-only Docs page which enumerates all Markdown files within a repository
- Optional read-only Ticgit Ticket pages *(based on last MIT release bf57b032 2009-01-27)*
### Limitations
- HTTP/HTTPS are the only supported protocols
- Access controls are not path-based, they are repository-based
- Only admin users can create repositories
- Only Administrators can create, rename or delete repositories
- Git:Blit is a full-stack solution, its not just a webapp so at this time there is no WAR build
### Todo List
- Review spots where Git:Blit can cache data instead of abusing the disk
- Unit testing
- Ticgit activity/timeline
- Ticgit query feature with paging support
- Ticgit ticket change history
- Implement Markdown editing
- View images on Blob page
- View other binary files Blob page
- View other binary files on Blob page
### License
TBD
### Architecture
### Inspirations
- [Gitweb](http://www.git-scm.com)
- [Fossil](http://www.fossil-scm.org)
## Architecture
![block diagram](architecture.png "Git Blit Architecture")
@@ -73,8 +80,8 @@
- [JCommander](http://jcommander.org)
- [BouncyCastle](http://www.bouncycastle.org)
### Building
Eclipse is recommended for development as the project settings are preconfigured.
## Building
[Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured.
1. Clone the git repository from here.
2. Import the gitblit project into your Eclipse workspace.<br/>
docs/01_configuration.mkd
@@ -6,36 +6,39 @@
Open `gitblit.properties` in your favorite text editor and make sure to review and set:
    - *git.repositoryFolder*
    - *server.tempFolder*
    - *server.httpBindInterface* and *server.httpsBindInterface*
    - *server.httpBindInterface* and *server.httpsBindInterface*<br/>
**NOTE:** Consider using **https** exclusively because passwords for authentication are transmitted as clear text!
    - *server.storePassword*<br/>
**NOTE:**<br/>
Its recommended to use **https** wherever possible instead of http because passwords are transmitted as clear text!
**NOTE:** The certificate password AND the keystore password must match!
3. Execute `gitblit.cmd` or `java -jar gitblit.jar` from a command-line
4. Wait a minute or two while all dependencies are downloaded and your self-signed certificate is generated.
5. Open your browser to <http://localhost> or <https://localhost> depending on your chosen configuration.
6. Click the *Login* link and enter the default administrator credentials: **admin / admin**<br/>
**NOTE:**<br/>
Make sure to change the administrator username and/or password!!
**NOTE:** Make sure to change the administrator username and/or password!!
### Administering Repositories
Repositories can be created, edited, and deleted through the web UI.  They may also be created, edited, and deleted from the command-line using real Git or your favorite file manager and text editor.
Repositories can be created, edited, renamed, and deleted through the web UI.  They may also be created, edited, and deleted from the command-line using real [Git](http://git-scm.com) or your favorite file manager and text editor.
All repository settings are stored within the repository `.git/config` file under the *gitblit* section.
    [gitblit]
        description = master repository
        owner = Joe Owner
        owner = james
        useTickets = false
        useDocs = true
        showRemoteBranches = false
        accessRestriction = clone
        isFrozen = false
        
#### Repository Names
Repository names must be unique and are case-insensitive.  The name must be composed of letters, digits, or `/ _ - .`<br/>
Whitespace is illegal.
#### Repository Owner
The *Repository Owner* has the special permission of being able to edit a repository through the web UI.  The Repository Owner is not permitted to rename the repository, delete the repository, or reassign ownership to another user.
### Administering Users
In contrast, all users are stored in the `users.properties` file or in the file your specified in `gitblit.properties`.<br/>
All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`.<br/>
The format of `users.properties` follows Jetty's convention for HashRealms:
    username,password,role1,role2,role3...
@@ -48,11 +51,12 @@
User passwords are CASE-SENSITIVE and may be *plain*, *md5*, or *crypt* formatted (see `gitblit.properties` -> *realm.passwordStorage*).
#### User Roles
There is only one actual *role* in Git:Blit and that is *#admin* which grants administrative powers to that user.  Administrators automatically have access to all repositories.  All other *roles* are actually repository names.  If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction.  This is how users are granted access to a restricted repository.
There is only one actual *role* in Git:Blit and that is *#admin* which grants administrative powers to that user.  Administrators automatically have access to all repositories.  All other *roles* are repository names.  If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction.  This is how users are granted access to a restricted repository.
### Creating your own Self-Signed Certificate
Review the contents of the `makekeystore.cmd` or `makekeystore_jdk.cmd`script and execute it.  Voila.
Review the contents of the `makekeystore.cmd` or `makekeystore_jdk.cmd` script and execute it.<br/>
**NOTE:** The certificate password AND the keystore password must match!
### Running as a Service
Review the contents of the `installService.cmd` or `installService64.cmd`, as appropriate for your JVM.<br/>
docs/01_eclipse.mkd
@@ -1,5 +1,18 @@
## Eclipse Tips
verifySsl
### Do Not Verify Self-Signed Certificates
If you are using a self-signed certificate, like the one that is automatically generated by Git:Blit, you have to tell Eclipse/EGit to ignore certificate verification errors.
how to push new unshared project to new repository
![sslverify](sslverify.png "http.sslVerify setting")
![sslverify2](sslverify2.png "Adding http.sslVerify setting")
### Pushing a New Project to a New Git:Blit Repository
1. Project Root->Team->Share->Git
Create a Git repository inside the project
### Pushing a Git-Controlled Project to another Git:Blit Repository
1. Project Root->Team->Remote->Push
2. Enter the URL information of the repository
3. In the Refspec dialog click the buttons named  "All all branches spec" and "All all tags spec"
docs/architecture.odg
Binary files differ
docs/architecture.png

docs/sslverify.png
docs/sslverify2.png
src/com/gitblit/BuildSite.java
@@ -56,7 +56,7 @@
        String html_footer = readContent(new File(params.pageFooter));
        final String links = sb.toString();
        final String header = MessageFormat.format(html_header, Constants.FULL_NAME, links);
        final String date = new SimpleDateFormat("yyyy MMM dd").format(new Date());
        final String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        final String footer = MessageFormat.format(html_footer, "generated " + date);
        for (File file : markdownFiles) {
            try {
src/com/gitblit/Constants.java
@@ -4,7 +4,7 @@
    public final static String NAME = "Git:Blit";
    
    public final static String FULL_NAME = "Git:Blit - a Pure Java Git Server";
    public final static String FULL_NAME = "Git:Blit - a Pure Java Git Solution";
    // The build script extracts this exact line so be careful editing it
    // and only use A-Z a-z 0-9 .-_ in the string. 
src/com/gitblit/GitBlit.java
@@ -4,6 +4,7 @@
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.servlet.ServletContextEvent;
@@ -21,6 +22,7 @@
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.models.RepositoryModel;
import com.gitblit.wicket.models.UserModel;
@@ -97,7 +99,9 @@
    }
    
    public List<String> getAllUsernames() {
        return loginService.getAllUsernames();
        List<String> names = loginService.getAllUsernames();
        Collections.sort(names);
        return names;
    }
    public UserModel getUserModel(String username) {
@@ -169,15 +173,28 @@
        model.lastChange = JGitUtils.getLastChange(r);
        StoredConfig config = JGitUtils.readConfig(r);
        if (config != null) {
            model.description = config.getString("gitblit", null, "description");
            model.owner = config.getString("gitblit", null, "owner");
            model.useTickets = config.getBoolean("gitblit", "useTickets", false);
            model.useDocs = config.getBoolean("gitblit", "useDocs", false);
            model.accessRestriction = AccessRestrictionType.fromName(config.getString("gitblit", null, "accessRestriction"));
            model.showRemoteBranches = config.getBoolean("gitblit", "showRemoteBranches", false);
            model.description = getConfig(config, "description", "");
            model.owner = getConfig(config, "owner", "");
            model.useTickets = getConfig(config, "useTickets", false);
            model.useDocs = getConfig(config, "useDocs", false);
            model.accessRestriction = AccessRestrictionType.fromName(getConfig(config, "accessRestriction", null));
            model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
            model.isFrozen = getConfig(config, "isFrozen", false);
        }
        r.close();
        return model;
    }
    private String getConfig(StoredConfig config, String field, String defaultValue) {
        String value = config.getString("gitblit", null, field);
        if (StringUtils.isEmpty(value)) {
            return defaultValue;
        }
        return value;
    }
    private boolean getConfig(StoredConfig config, String field, boolean defaultValue) {
        return config.getBoolean("gitblit", field, defaultValue);
    }
    public void editRepositoryModel(RepositoryModel repository, boolean isCreate) throws GitBlitException {
@@ -209,6 +226,7 @@
        config.setBoolean("gitblit", null, "useDocs", repository.useDocs);
        config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.name());
        config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches);
        config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen);
        try {
            config.save();
        } catch (IOException e) {
src/com/gitblit/GitBlitServlet.java
@@ -44,12 +44,12 @@
            String function = url.substring(forwardSlash + 1);
            String query = req.getQueryString();
            RepositoryModel model = GitBlit.self().getRepositoryModel(repository);
            if (model != null) {
                if (model.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
            if (model != null) {
                if (model.isFrozen || model.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
                    boolean authorizedUser = req.isUserInRole(repository);
                    if (function.startsWith("git-receive-pack") || (query.indexOf("service=git-receive-pack") > -1)) {
                        // Push request
                        if (authorizedUser) {
                        if (!model.isFrozen && authorizedUser) {
                            // clone-restricted or push-authorized
                            super.service(req, rsp);
                            return;
src/com/gitblit/utils/StringUtils.java
@@ -107,4 +107,11 @@
            throw new RuntimeException(t);
        }
    }
    public static String getRootPath(String path) {
        if (path.indexOf('/') > -1) {
            return path.substring(0, path.indexOf('/'));
        }
        return "";
    }
}
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -90,4 +90,6 @@
gb.useDocsDescription = enumerates Markdown documentation in repository
gb.showRemoteBranchesDescription = show remote branches
gb.canAdminDescription = can administer Git:Blit server
gb.permittedUsers = permitted users
gb.permittedUsers = permitted users
gb.isFrozen = is frozen
gb.isFrozenDescription = deny push operations
src/com/gitblit/wicket/LoginPage.html
@@ -25,10 +25,10 @@
            <form style="text-align:center;" wicket:id="loginForm">
                <div>
                    <p/>
                    <wicket:message key="gb.username"></wicket:message>
                    <wicket:message key="gb.username"></wicket:message> &nbsp;
                    <input type="text" id="username" wicket:id="username" value=""/>
                    <p/>
                    <wicket:message key="gb.password"></wicket:message>
                    <wicket:message key="gb.password"></wicket:message> &nbsp;
                    <input type="password"  wicket:id="password" value=""/>
                    <p/>
                    <input type="submit" value="Login" wicket:message="value:gb.login" />
src/com/gitblit/wicket/RepositoryPage.java
@@ -36,7 +36,6 @@
import com.gitblit.wicket.pages.BranchesPage;
import com.gitblit.wicket.pages.DocsPage;
import com.gitblit.wicket.pages.LogPage;
import com.gitblit.wicket.pages.RepositoriesPage;
import com.gitblit.wicket.pages.SearchPage;
import com.gitblit.wicket.pages.SummaryPage;
import com.gitblit.wicket.pages.TagsPage;
@@ -79,10 +78,8 @@
        }
        Repository r = getRepository();
        if (r == null) {
            error(MessageFormat.format("Failed to open repository {0} for {1}!", repositoryName, getPageName()), true);
        }
        RepositoryModel model = getRepositoryModel();
        // standard page links
        add(new BookmarkablePageLink<Void>("summary", SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryName)));
        add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils.newRepositoryParameter(repositoryName)));
@@ -94,12 +91,12 @@
        List<String> extraPageLinks = new ArrayList<String>();
        // Conditionally add tickets page
        if (getRepositoryModel().useTickets && JGitUtils.getTicketsBranch(r) != null) {
        if (model.useTickets && JGitUtils.getTicketsBranch(r) != null) {
            extraPageLinks.add("tickets");
        }
        // Conditionally add docs page
        if (getRepositoryModel().useDocs) {
        if (model.useDocs) {
            extraPageLinks.add("docs");
        }
@@ -150,8 +147,7 @@
        if (r == null) {
            Repository r = GitBlit.self().getRepository(repositoryName);
            if (r == null) {
                error("Can not load repository " + repositoryName);
                redirectToInterceptPage(new RepositoriesPage());
                error("Can not load repository " + repositoryName, true);
                return null;
            }
            this.r = r;
@@ -163,9 +159,8 @@
        if (m == null) {
            RepositoryModel model = GitBlit.self().getRepositoryModel(GitBlitWebSession.get().getUser(), repositoryName);
            if (model == null) {
                error("Unauthorized access for repository " + repositoryName);
                redirectToInterceptPage(new RepositoriesPage());
                return null;
                error("Unauthorized access for repository " + repositoryName, true);
                return null;
            }
            m = model;
        }
src/com/gitblit/wicket/models/RepositoryModel.java
@@ -17,9 +17,14 @@
    public boolean useTickets;
    public boolean useDocs;
    public AccessRestrictionType accessRestriction;
    public boolean isFrozen;
    public RepositoryModel() {
        this.name = "";
        this.description = "";
        this.owner = "";
        this.lastChange = new Date(0);
        this.accessRestriction = AccessRestrictionType.NONE;
    }
    public RepositoryModel(String name, String description, String owner, Date lastchange) {
@@ -27,5 +32,6 @@
        this.description = description;
        this.owner = owner;
        this.lastChange = lastchange;
        this.accessRestriction = AccessRestrictionType.NONE;
    }    
}
src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -17,13 +17,14 @@
            <tbody>
                <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" id="name" size="40" tabindex="1" /></td></tr>
                <tr><th><wicket:message key="gb.description"></wicket:message></th><td class="edit"><input type="text" wicket:id="description" size="40" tabindex="2" /></td></tr>
                <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><input type="text" wicket:id="owner" size="40" tabindex="3" /></td></tr>
                <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select wicket:id="owner" tabindex="3" /></td></tr>
                <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useTickets" tabindex="4" /> &nbsp;<i><wicket:message key="gb.useTicketsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useDocs" tabindex="5" /> &nbsp;<i><wicket:message key="gb.useDocsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="6" /> &nbsp;<i><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select wicket:id="accessRestriction" tabindex="7" /></td></tr>                
                <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="isFrozen" tabindex="8" /> &nbsp;<i><wicket:message key="gb.isFrozenDescription"></wicket:message></i></td></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></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" tabindex="8" /></td></tr>
                <tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" tabindex="9" /></td></tr>
            </tbody>
        </table>
    </form>    
src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -3,6 +3,7 @@
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
@@ -23,13 +24,14 @@
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.GitBlit;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.AdminPage;
import com.gitblit.wicket.BasePage;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.models.RepositoryModel;
import com.gitblit.wicket.models.UserModel;
@AdminPage
public class EditRepositoryPage extends BasePage {
    private final boolean isCreate;
@@ -51,6 +53,9 @@
    }
    protected void setupPage(final RepositoryModel repositoryModel) {
        // ensure this user can create or edit this repository
        checkPermissions(repositoryModel);
        List<String> repositoryUsers = new ArrayList<String>();
        if (isCreate) {
            super.setupPage("", getString("gb.newRepository"));
@@ -58,6 +63,7 @@
            super.setupPage("", getString("gb.edit"));
            if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
                repositoryUsers.addAll(GitBlit.self().getRepositoryUsers(repositoryModel));
                Collections.sort(repositoryUsers);
            }
        }
@@ -99,16 +105,20 @@
                        error("Please select access restriction!");
                        return;
                    }
                    // save the repository
                    GitBlit.self().editRepositoryModel(repositoryModel, isCreate);
                    // save the repository access list
                    if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
                        Iterator<String> users = usersPalette.getSelectedChoices();
                        List<String> repositoryUsers = new ArrayList<String>();
                        while (users.hasNext()) {
                            repositoryUsers.add(users.next());
                        }
                        // ensure the owner is added to the user list
                        if (!repositoryUsers.contains(repositoryModel.owner)) {
                            repositoryUsers.add(repositoryModel.owner);
                        }
                        GitBlit.self().setRepositoryUsers(repositoryModel, repositoryUsers);
                    }
@@ -124,8 +134,9 @@
        // field names reflective match RepositoryModel fields
        form.add(new TextField<String>("name").setEnabled(isCreate));
        form.add(new TextField<String>("description"));
        form.add(new TextField<String>("owner"));
        form.add(new DropDownChoice<String>("owner", GitBlit.self().getAllUsernames()).setEnabled(GitBlitWebSession.get().canAdmin()));
        form.add(new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays.asList(AccessRestrictionType.values()), new AccessRestrictionRenderer()));
        form.add(new CheckBox("isFrozen"));
        form.add(new CheckBox("useTickets"));
        form.add(new CheckBox("useDocs"));
        form.add(new CheckBox("showRemoteBranches"));
@@ -133,6 +144,51 @@
        add(form);
    }
    /**
     * Unfortunately must repeat part of AuthorizaitonStrategy here because that
     * mechanism does not take PageParameters into consideration, only page
     * instantiation.
     *
     * Repository Owners should be able to edit their repository.
     */
    private void checkPermissions(RepositoryModel model) {
        boolean authenticateAdmin = GitBlit.self().settings().getBoolean(Keys.web.authenticateAdminPages, true);
        boolean allowAdmin = GitBlit.self().settings().getBoolean(Keys.web.allowAdministration, true);
        GitBlitWebSession session = GitBlitWebSession.get();
        UserModel user = session.getUser();
        if (allowAdmin) {
            if (authenticateAdmin) {
                if (user == null) {
                    // No Login Available
                    error("Administration requires a login", true);
                }
                if (isCreate) {
                    // Create Repository
                    if (!user.canAdmin()) {
                        // Only Administrators May Create
                        error("Only an administrator may create a repository", true);
                    }
                } else {
                    // Edit Repository
                    if (user.canAdmin()) {
                        // Admins can edit everything
                        return;
                    } else {
                        if (!model.owner.equalsIgnoreCase(user.getUsername())) {
                            // User is not an Admin nor Owner
                            error("Only an administrator or the owner may edit a repository", true);
                        }
                    }
                }
            }
        } else {
            // No Administration Permitted
            error("Administration is disabled", true);
        }
    }
    private class AccessRestrictionRenderer implements IChoiceRenderer<AccessRestrictionType> {
src/com/gitblit/wicket/pages/RepositoriesPage.html
@@ -10,30 +10,21 @@
</wicket:head>
<body>
<wicket:extend>
    <div style="text-align:center;padding-top:5px;" wicket:id="feedback">[Feedback Panel]</div>
<wicket:extend>
    <!-- Filler div -->
    <div style="padding-top:18px;"></div>
    <div style="text-align:center;padding-bottom:5px;" wicket:id="feedback">[Feedback Panel]</div>
    
    <div class="markdown" style="padding-top:5px;" wicket:id="repositoriesMessage">[repositories message]</div>
    <div class="markdown" style="margin-top:-0.5em;padding-bottom:5px;" wicket:id="repositoriesMessage">[repositories message]</div>
    
    <div style="padding-top:5px;" wicket:id="adminPanel">[admin links]</div>
    <div wicket:id="adminPanel">[admin links]</div>
        
    <table class="repositories">
        <tr>
            <th wicket:id="orderByRepository"><wicket:message key="gb.repository">Repository</wicket:message></th>
            <th wicket:id="orderByDescription"><wicket:message key="gb.description">Description</wicket:message></th>
            <th wicket:id="orderByOwner"><wicket:message key="gb.owner">Owner</wicket:message></th>
            <th></th>
            <th wicket:id="orderByDate"><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
            <th></th>
        </tr>
        <tbody>
               <tr wicket:id="repository">
                 <td><div class="list" wicket:id="repositoryName">[repository name]</div></td>
                 <td><div class="list" wicket:id="repositoryDescription">[repository description]</div></td>
                 <td class="author"><span wicket:id="repositoryOwner">[repository owner]</span></td>
                 <td class="icon"><img wicket:id="ticketsIcon" /><img wicket:id="docsIcon" /><img wicket:id="accessRestrictionIcon" /></td>
                 <td><span wicket:id="repositoryLastChange">[last change]</span></td>
                 <td class="rightAlign"><span wicket:id="repositoryLinks"></span></td>
        <span wicket:id="headerContent"></span>
        <tbody>
               <tr wicket:id="row">
                   <span wicket:id="rowContent"></span>
               </tr>
        </tbody>
    </table>
@@ -48,6 +39,45 @@
    <wicket:fragment wicket:id="repositoryAdminLinks">
        <span class="link"><a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a> | <a wicket:id="renameRepository"><wicket:message key="gb.rename">[rename]</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="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a></span>
    </wicket:fragment>
    <wicket:fragment wicket:id="flatHeader">
        <tr>
            <th wicket:id="orderByRepository"><wicket:message key="gb.repository">Repository</wicket:message></th>
            <th wicket:id="orderByDescription"><wicket:message key="gb.description">Description</wicket:message></th>
            <th wicket:id="orderByOwner"><wicket:message key="gb.owner">Owner</wicket:message></th>
            <th></th>
            <th wicket:id="orderByDate"><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
            <th></th>
        </tr>
    </wicket:fragment>
    <wicket:fragment wicket:id="groupHeader">
        <tr>
            <th><wicket:message key="gb.repository">Repository</wicket:message></th>
            <th><wicket:message key="gb.description">Description</wicket:message></th>
            <th><wicket:message key="gb.owner">Owner</wicket:message></th>
            <th></th>
            <th><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
            <th></th>
        </tr>
    </wicket:fragment>
    <wicket:fragment wicket:id="groupRow">
        <td colspan="6"><span wicket:id="groupName">[group name]</span></td>
    </wicket:fragment>
    <wicket:fragment wicket:id="repositoryRow">
        <td><div class="list" wicket:id="repositoryName">[repository name]</div></td>
        <td><div class="list" wicket:id="repositoryDescription">[repository description]</div></td>
        <td class="author"><span wicket:id="repositoryOwner">[repository owner]</span></td>
        <td style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="ticketsIcon" /><img class="inlineIcon" wicket:id="docsIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td>
        <td><span wicket:id="repositoryLastChange">[last change]</span></td>
        <td class="rightAlign"><span wicket:id="repositoryLinks"></span></td>
    </wicket:fragment>
    
</wicket:extend>
</body>
src/com/gitblit/wicket/pages/RepositoriesPage.java
@@ -4,8 +4,11 @@
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -20,6 +23,8 @@
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.IDataProvider;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.resource.ContextRelativeResource;
@@ -42,13 +47,22 @@
    public RepositoriesPage() {
        super();
        setupPage("", "");
        final boolean showAdmin;
        if (GitBlit.self().settings().getBoolean(Keys.web.authenticateAdminPages, true)) {
            boolean allowAdmin = GitBlit.self().settings().getBoolean(Keys.web.allowAdministration, false);
            showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
            // authentication requires state and session
            setStatelessHint(false);
        } else {
            showAdmin = GitBlit.self().settings().getBoolean(Keys.web.allowAdministration, false);
            if (GitBlit.self().settings().getBoolean(Keys.web.authenticateViewPages, false)) {
                // authentication requires state and session
                setStatelessHint(false);
            } else {
                // no authentication required, no state and no session required
                setStatelessHint(true);
            }
        }
        Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
@@ -66,7 +80,7 @@
        // Load the markdown welcome message
        String messageSource = GitBlit.self().settings().getString(Keys.web.repositoriesMessage, "gitblit");
        String message = "";
        String message = "<br/>";
        if (messageSource.equalsIgnoreCase("gitblit")) {
            // Read default welcome message
            try {
@@ -99,70 +113,114 @@
        add(repositoriesMessage);
        final Map<AccessRestrictionType, String> accessRestrictionTranslations = getAccessRestrictions();
        UserModel user = GitBlitWebSession.get().getUser();
        List<RepositoryModel> rows = GitBlit.self().getRepositoryModels(user);
        DataProvider dp = new DataProvider(rows);
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repository", dp) {
        final UserModel user = GitBlitWebSession.get().getUser();
        List<RepositoryModel> models = GitBlit.self().getRepositoryModels(user);
        IDataProvider<RepositoryModel> dp;
        if (GitBlit.self().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) {
            Map<String, List<RepositoryModel>> groups = new HashMap<String, List<RepositoryModel>>();
            for (RepositoryModel model : models) {
                String rootPath = StringUtils.getRootPath(model.name);
                if (StringUtils.isEmpty(rootPath)) {
                    rootPath = GitBlit.self().settings().getString(Keys.web.repositoryRootGroupName, " ");
                }
                if (!groups.containsKey(rootPath)) {
                    groups.put(rootPath, new ArrayList<RepositoryModel>());
                }
                groups.get(rootPath).add(model);
            }
            List<String> roots = new ArrayList<String>(groups.keySet());
            Collections.sort(roots);
            List<RepositoryModel> groupedModels = new ArrayList<RepositoryModel>();
            for (String root : roots) {
                groupedModels.add(new GroupRepositoryModel(root));
                groupedModels.addAll(groups.get(root));
            }
            dp = new ListDataProvider<RepositoryModel>(groupedModels);
        } else {
            dp = new DataProvider(models);
        }
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {
            private static final long serialVersionUID = 1L;
            int counter = 0;
            public void populateItem(final Item<RepositoryModel> item) {
                final RepositoryModel entry = item.getModelObject();
                if (entry instanceof GroupRepositoryModel) {
                    Fragment row = new Fragment("rowContent", "groupRow", this);
                    item.add(row);
                    row.add(new Label("groupName", entry.name));
                    WicketUtils.setCssClass(item, "group");
                    return;
                }
                Fragment row = new Fragment("rowContent", "repositoryRow", this);
                item.add(row);
                if (entry.hasCommits) {
                    // Existing repository
                    PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
                    item.add(new LinkPanel("repositoryName", "list", entry.name, SummaryPage.class, pp));
                    item.add(new LinkPanel("repositoryDescription", "list", entry.description, SummaryPage.class, pp));
                    row.add(new LinkPanel("repositoryName", "list", entry.name, SummaryPage.class, pp));
                    row.add(new LinkPanel("repositoryDescription", "list", entry.description, SummaryPage.class, pp));
                } else {
                    // New repository
                    item.add(new Label("repositoryName", entry.name + "<span class='empty'>(empty)</span>").setEscapeModelStrings(false));
                    item.add(new Label("repositoryDescription", entry.description));
                    row.add(new Label("repositoryName", entry.name + "<span class='empty'>(empty)</span>").setEscapeModelStrings(false));
                    row.add(new Label("repositoryDescription", entry.description));
                }
                if (entry.useTickets) {
                    item.add(WicketUtils.newImage("ticketsIcon", "bug_16x16.png", getString("gb.tickets")));
                    row.add(WicketUtils.newImage("ticketsIcon", "bug_16x16.png", getString("gb.tickets")));
                } else {
                    item.add(WicketUtils.newBlankImage("ticketsIcon"));
                    row.add(WicketUtils.newBlankImage("ticketsIcon"));
                }
                if (entry.useDocs) {
                    item.add(WicketUtils.newImage("docsIcon", "book_16x16.png", getString("gb.docs")));
                    row.add(WicketUtils.newImage("docsIcon", "book_16x16.png", getString("gb.docs")));
                } else {
                    item.add(WicketUtils.newBlankImage("docsIcon"));
                }
                switch (entry.accessRestriction) {
                case NONE:
                    item.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
                    break;
                case PUSH:
                    item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
                    break;
                case CLONE:
                    item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
                    break;
                case VIEW:
                    item.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
                    break;
                default:
                    item.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
                    row.add(WicketUtils.newBlankImage("docsIcon"));
                }
                item.add(new Label("repositoryOwner", entry.owner));
                if (entry.isFrozen) {
                    row.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", getString("gb.isFrozen")));
                } else {
                    row.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
                }
                switch (entry.accessRestriction) {
                case NONE:
                    row.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
                    break;
                case PUSH:
                    row.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
                    break;
                case CLONE:
                    row.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
                    break;
                case VIEW:
                    row.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
                    break;
                default:
                    row.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
                }
                row.add(new Label("repositoryOwner", entry.owner));
                String lastChange = TimeUtils.timeAgo(entry.lastChange);
                Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
                item.add(lastChangeLabel);
                row.add(lastChangeLabel);
                WicketUtils.setCssClass(lastChangeLabel, TimeUtils.timeAgoCss(entry.lastChange));
                boolean showOwner = user != null && user.getUsername().equalsIgnoreCase(entry.owner);
                if (showAdmin) {
                    Fragment repositoryLinks = new Fragment("repositoryLinks", "repositoryAdminLinks", this);
                    repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository", EditRepositoryPage.class, WicketUtils.newRepositoryParameter(entry.name)));
                    repositoryLinks.add(new BookmarkablePageLink<Void>("renameRepository", EditRepositoryPage.class, WicketUtils.newRepositoryParameter(entry.name)).setEnabled(false));
                    repositoryLinks.add(new BookmarkablePageLink<Void>("deleteRepository", EditRepositoryPage.class, WicketUtils.newRepositoryParameter(entry.name)).setEnabled(false));
                    item.add(repositoryLinks);
                    row.add(repositoryLinks);
                } else if (showOwner) {
                    Fragment repositoryLinks = new Fragment("repositoryLinks", "repositoryOwnerLinks", this);
                    repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository", EditRepositoryPage.class, WicketUtils.newRepositoryParameter(entry.name)));
                    row.add(repositoryLinks);
                } else {
                    item.add(new Label("repositoryLinks"));
                    row.add(new Label("repositoryLinks"));
                }
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
@@ -170,10 +228,20 @@
        };
        add(dataView);
        add(newSort("orderByRepository", SortBy.repository, dp, dataView));
        add(newSort("orderByDescription", SortBy.description, dp, dataView));
        add(newSort("orderByOwner", SortBy.owner, dp, dataView));
        add(newSort("orderByDate", SortBy.date, dp, dataView));
        if (dp instanceof SortableDataProvider<?>) {
            // add sortable header
            SortableDataProvider<?> sdp = (SortableDataProvider<?>) dp;
            Fragment fragment = new Fragment("headerContent", "flatHeader", this);
            fragment.add(newSort("orderByRepository", SortBy.repository, sdp, dataView));
            fragment.add(newSort("orderByDescription", SortBy.description, sdp, dataView));
            fragment.add(newSort("orderByOwner", SortBy.owner, sdp, dataView));
            fragment.add(newSort("orderByDate", SortBy.date, sdp, dataView));
            add(fragment);
        } else {
            // not sortable
            Fragment fragment = new Fragment("headerContent", "groupHeader", this);
            add(fragment);
        }
    }
    protected enum SortBy {
@@ -258,4 +326,13 @@
            return list.subList(first, first + count).iterator();
        }
    }
    private class GroupRepositoryModel extends RepositoryModel {
        private static final long serialVersionUID = 1L;
        GroupRepositoryModel(String name) {
            super(name, "", "", new Date(0));
        }
    }
}
src/com/gitblit/wicket/resources/cold_16x16.png
src/com/gitblit/wicket/resources/gitblit.css
@@ -47,6 +47,10 @@
    font-style: italic;
}
img.inlineIcon {
    padding-left: 1px;
    padding-right: 1px;
}
a {
    color: #0000cc;
@@ -552,6 +556,17 @@
tr th.wicket_orderUp a { background-image: url(arrow_up.png); }
tr th.wicket_orderNone a { background-image: url(arrow_off.png); }
tr.group {
    background-color: #E66C2C;
}
tr.group td {
    font-weight: bold;
    border-bottom: 1px solid orange;
    color: white;
    background-color: #E66C2C;
}
tr.light {
    background-color: #ffffff;
}
src/com/gitblit/wicket/resources/welcome.mkd
@@ -1,5 +1,3 @@
## Welcome to Git:Blit
A quick and easy way to host or view your own Git repositories.
Built with [JGit](http://eclipse.org/jgit), [Wicket](http://wicket.apache.org), [WicketStuff GoogleCharts](https://github.com/wicketstuff/core/wiki/GoogleCharts), [MarkdownPapers](http://markdown.tautua.org), [Jetty](http://eclipse.org/jetty), [SLF4J](http://www.slf4j.org), [Log4j](http://logging.apache.org/log4j), [google-code-prettify](http://code.google.com/p/google-code-prettify), [JCommander](http://jcommander.org), [BouncyCastle](http://www.bouncycastle.org), [JavaService](http://forge.ow2.org/projects/javaservice), and most icons courtesy of [FatCow Hosting](http://www.fatcow.com/free-icons)
A quick and easy way to host or view your own [Git](http://www.git-scm.com) repositories.