James Moger
2011-10-05 841651baee2181c1543555d1eabcd0e4fee48827
New setting to disable RPC administration. Advancing the RPC client.
6 files added
10 files modified
1198 ■■■■■ changed files
build.xml 68 ●●●●● patch | view | raw | blame | history
distrib/gitblit.properties 12 ●●●● patch | view | raw | blame | history
docs/00_index.mkd 12 ●●●● patch | view | raw | blame | history
docs/04_releases.mkd 11 ●●●● patch | view | raw | blame | history
src/com/gitblit/RpcFilter.java 22 ●●●●● patch | view | raw | blame | history
src/com/gitblit/build/Build.java 19 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/ClosableTabComponent.java 149 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/DateCellRenderer.java 19 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitClient.java 141 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitClientLauncher.java 105 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitPanel.java 378 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitRegistration.java 47 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/NameRenderer.java 65 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/RepositoriesModel.java 31 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/TypeRenderer.java 119 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/splash.png patch | view | raw | blame | history
build.xml
@@ -85,6 +85,7 @@
        <property name="distribution.zipfile" value="gitblit-${gb.version}.zip" />
        <property name="distribution.warfile" value="gitblit-${gb.version}.war" />
        <property name="fedclient.zipfile" value="fedclient-${gb.version}.zip" />
        <property name="rpcclient.zipfile" value="rpcclient-${gb.version}.zip" />
    </target>
    
    
@@ -265,6 +266,9 @@
                <arg value="%FEDCLIENT%=${fedclient.zipfile}" />
                <arg value="--substitute" />
                <arg value="%RPCCLIENT%=${rpcclient.zipfile}" />
                <arg value="--substitute" />
                <arg value="%BUILDDATE%=${gb.versionDate}" />
                <arg value="--substitute" />
@@ -409,6 +413,53 @@
        </zip>
    </target>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Build the stand-alone, Gitblit RPC Client
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildRpcClient" depends="compile" description="Builds the stand-alone Gitblit RPC client">
        <echo>Building Gitblit RPC Client ${gb.version}</echo>
        <genjar jarfile="rpcclient.jar">
            <resource file="${basedir}/src/com/gitblit/client/splash.png" />
            <resource file="${basedir}/resources/gitblt-favicon.png" />
            <resource file="${basedir}/resources/lock_go_16x16.png" />
            <resource file="${basedir}/resources/lock_pull_16x16.png" />
            <resource file="${basedir}/resources/shield_16x16.png" />
            <resource file="${basedir}/resources/federated_16x16.png" />
            <resource file="${basedir}/resources/cold_16x16.png" />
            <resource file="${basedir}/resources/book_16x16.png" />
            <resource file="${basedir}/resources/bug_16x16.png" />
            <resource file="${basedir}/resources/blank.png" />
            <class name="com.gitblit.client.GitblitClientLauncher" />
            <classfilter>
                <exclude name="org.apache." />
                <exclude name="org.bouncycastle." />
                <exclude name="org.eclipse." />
                <exclude name="org.slf4j." />
                <exclude name="com.beust." />
                <exclude name="com.google." />
            </classfilter>
            <classpath refid="master-classpath" />
            <manifest>
                <attribute name="Main-Class" value="com.gitblit.client.GitblitClientLauncher" />
                <attribute name="SplashScreen-Image" value="splash.png" />
                <attribute name="Specification-Version" value="${gb.version}" />
                <attribute name="Release-Date" value="${gb.versionDate}" />
            </manifest>
        </genjar>
        <!-- Build the rpc client zip file -->
        <zip destfile="${rpcclient.zipfile}">
            <fileset dir="${basedir}">
                <include name="rpcclient.jar" />
            </fileset>
        </zip>
    </target>
        
    <!-- 
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -525,6 +576,9 @@
            <arg value="%FEDCLIENT%=${fedclient.zipfile}" />
            <arg value="--substitute" />
            <arg value="%RPCCLIENT%=${rpcclient.zipfile}" />
            <arg value="--substitute" />
            <arg value="%BUILDDATE%=${gb.versionDate}" />
            <arg value="--substitute" />
@@ -554,7 +608,7 @@
        Compile from source, publish binaries, and build & deploy site
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildAll" depends="buildGO,buildWAR,buildFederationClient,buildSite">
    <target name="buildAll" depends="buildGO,buildWAR,buildFederationClient,buildRpcClient,buildSite">
        <!-- Cleanup -->
        <delete dir="${project.build.dir}" />
        <delete dir="${project.war.dir}" />
@@ -567,7 +621,7 @@
        Publish binaries to Google Code
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="publishBinaries" depends="buildGO,buildWAR,buildFederationClient" description="Publish the Gitblit binaries to Google Code">
    <target name="publishBinaries" depends="buildGO,buildWAR,buildFederationClient,buildRpcClient" description="Publish the Gitblit binaries to Google Code">
        
        <echo>Uploading Gitblit ${gb.version} binaries</echo>
        
@@ -600,6 +654,16 @@
            targetfilename="fedclient-${gb.version}.zip"
            summary="Gitblit Federation Client v${gb.version} (command-line tool to clone data from federated Gitblit instances)"
            labels="Featured, Type-Package, OpSys-All" />
        <!-- Upload RpcClient -->
        <gcupload
            username="${googlecode.user}"
            password="${googlecode.password}"
            projectname="gitblit"
            filename="${rpcclient.zipfile}"
            targetfilename="rpcclient-${gb.version}.zip"
            summary="Gitblit RPC Client v${gb.version} (Swing tool to clone repositories and remotely administer a Gitblit server)"
            labels="Featured, Type-Package, OpSys-All" />
    </target>
    
distrib/gitblit.properties
@@ -87,11 +87,17 @@
# SINCE 0.5.0 
web.allowAdministration = true
# Allows remote clients to list repositories and administer the Gitblit instance
# if they have administrator permissions.
# Allows remote clients to list repositories and possibly administer the Gitblit
# server, if the authenticated account has administrator permissions.
#
# SINCE 0.6.1 
web.enableRpcServlet = false
web.enableRpcServlet = true
# Allows remote clients to administer the Gitblit instance, if the authenticated
# account has administrator permissions.  Requires *web.enableRpcServlet=true*.
#
# SINCE 0.6.1
web.enableRpcAdministration = false
# Allow dynamic zip downloads.
#
docs/00_index.mkd
@@ -17,6 +17,7 @@
### Tools
<ul class='noBullets'>
<li>*Gitblit RPC Client* - a Java Swing tool to clone repositories and remotely administer a Gitblit server
<li>*Gitblit Federation Client* - a command line tool to clone/pull groups of repositories and optionally users and settings
</ul>
@@ -26,11 +27,16 @@
### Current Release
**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)|[fedclient](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%)) based on [%JGIT%][jgit] &nbsp; *released %BUILDDATE%*
**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)|[fedclient](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%)|[rpcclient](http://code.google.com/p/gitblit/downloads/detail?name=%RPCCLIENT%)) based on [%JGIT%][jgit] &nbsp; *released %BUILDDATE%*
- fixed/broke: federation protocol.  serialized dates now include the timezone.  This breaks 0.6.0 clients/servers.
- improved: updated ui with Twitter's Bootstrap CSS toolkit
- improved: overhauled web ui with Twitter's Bootstrap CSS toolkit
<br/>**New:** *web.loginMessage = gitblit*
- added: authenticated JSON RPC mechanism
<br/>**New:** *web.enableRpcServlet = true*
<br/>**New:** *web.enableRpcAdministration = false*
- added: reusable JSON RPC client class
- added: Swing RPC Client application for cloning and administration of repositories, users, and federation proposals.
- fixed/broke: federation protocol.  dates are now serialized to the [iso8601](http://en.wikipedia.org/wiki/ISO_8601) standard.  This breaks 0.6.0 federation clients/servers.
- fixed: Null pointer exception if did not set federation strategy (issue 20)
- fixed: Gitblit GO allows SSL renegotiation if running on Java 1.6.0_22 or later
- added: IUserService.setup(IStoredSettings) for custom user service implementations
docs/04_releases.mkd
@@ -1,11 +1,16 @@
## Release History
### Current Release
**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)|[fedclient](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%)) based on [%JGIT%][jgit] &nbsp; *released %BUILDDATE%*
**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)|[fedclient](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%)|[rpcclient](http://code.google.com/p/gitblit/downloads/detail?name=%RPCCLIENT%)) based on [%JGIT%][jgit] &nbsp; *released %BUILDDATE%*
- fixed/broke: federation protocol.  serialized dates now include the timezone.  This breaks 0.6.0 clients/servers.
- improved: updated ui with Twitter's Bootstrap CSS toolkit
- improved: overhauled web ui with Twitter's Bootstrap CSS toolkit
<br/>**New:** *web.loginMessage = gitblit*
- added: authenticated JSON RPC mechanism
<br/>**New:** *web.enableRpcServlet = true*
<br/>**New:** *web.enableRpcAdministration = false*
- added: reusable JSON RPC client class
- added: Swing RPC Client application for cloning and administration of repositories, users, and federation proposals.
- fixed/broke: federation protocol.  dates are now serialized to the [iso8601](http://en.wikipedia.org/wiki/ISO_8601) standard.  This breaks 0.6.0 federation clients/servers.
- fixed: Null pointer exception if did not set federation strategy (issue 20)
- fixed: Gitblit GO allows SSL renegotiation if running on Java 1.6.0_22 or later
- added: IUserService.setup(IStoredSettings) for custom user service implementations
src/com/gitblit/RpcFilter.java
@@ -57,20 +57,21 @@
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        if (!GitBlit.getBoolean(Keys.web.enableRpcServlet, false)) {
            logger.warn(Keys.web.enableRpcServlet + " must be set TRUE for rpc requests.");
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        String fullUrl = getFullUrl(httpRequest);
        RpcRequest requestType = RpcRequest.fromName(httpRequest.getParameter("req"));
        boolean adminRequest = requestType.exceeds(RpcRequest.LIST_REPOSITORIES);
        // conditionally reject all rpc requests
        if (!GitBlit.getBoolean(Keys.web.enableRpcServlet, true)) {
            logger.warn(Keys.web.enableRpcServlet + " must be set TRUE for rpc requests.");
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, false);
        boolean authenticateAdmin = GitBlit.getBoolean(Keys.web.authenticateAdminPages, true);
        // Wrap the HttpServletRequest with the RpcServletnRequest which
        // overrides the servlet container user principal methods.
        AuthenticatedRequest authenticatedRequest = new AuthenticatedRequest(httpRequest);
@@ -79,6 +80,13 @@
            authenticatedRequest.setUser(user);
        }
        
        // conditionally reject rpc administration requests
        if (adminRequest && !GitBlit.getBoolean(Keys.web.enableRpcAdministration, false)) {
            logger.warn(Keys.web.enableRpcAdministration + " must be set TRUE for administrative rpc requests.");
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        // BASIC authentication challenge and response processing
        if ((adminRequest && authenticateAdmin) || (!adminRequest && authenticateView)) {
            if (user == null) {
src/com/gitblit/build/Build.java
@@ -48,7 +48,11 @@
 * 
 */
public class Build {
    public interface DownloadListener {
        public void downloading(String name);
    }
    /**
     * BuildType enumeration representing compile-time or runtime. This is used
     * to download dependencies either for Gitblit GO runtime or for setting up
@@ -57,6 +61,8 @@
    public static enum BuildType {
        RUNTIME, COMPILETIME;
    }
    private static DownloadListener downloadListener;
    public static void main(String... args) {
        runtime();
@@ -123,6 +129,14 @@
        downloadFromApache(MavenObject.SLF4JAPI, BuildType.RUNTIME);
        downloadFromApache(MavenObject.SLF4LOG4J, BuildType.RUNTIME);
        downloadFromApache(MavenObject.LOG4J, BuildType.RUNTIME);
        downloadFromApache(MavenObject.GSON, BuildType.RUNTIME);
        downloadFromApache(MavenObject.JSCH, BuildType.RUNTIME);
        downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
    }
    public static void rpcClient(DownloadListener listener) {
        downloadListener = listener;
        downloadFromApache(MavenObject.GSON, BuildType.RUNTIME);
        downloadFromApache(MavenObject.JSCH, BuildType.RUNTIME);
        
@@ -273,6 +287,9 @@
                    throw new RuntimeException("Failed to create destination folder structure!");
                }
            }
            if (downloadListener != null) {
                downloadListener.downloading(mo.name);
            }
            ByteArrayOutputStream buff = new ByteArrayOutputStream();
            try {
                URL url = new URL(mavenURL);
src/com/gitblit/client/ClosableTabComponent.java
New file
@@ -0,0 +1,149 @@
/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.client;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.plaf.basic.BasicButtonUI;
/**
 * Closable tab control.
 */
public class ClosableTabComponent extends JPanel {
    private static final long serialVersionUID = 1L;
    private static final MouseListener BUTTON_MOUSE_LISTENER = new MouseAdapter() {
        public void mouseEntered(MouseEvent e) {
            Component component = e.getComponent();
            if (component instanceof AbstractButton) {
                AbstractButton button = (AbstractButton) component;
                button.setBorderPainted(true);
            }
        }
        public void mouseExited(MouseEvent e) {
            Component component = e.getComponent();
            if (component instanceof AbstractButton) {
                AbstractButton button = (AbstractButton) component;
                button.setBorderPainted(false);
            }
        }
    };
    private final JTabbedPane pane;
    private final JLabel label;
    private final JButton button = new TabButton();
    private final CloseTabListener closeListener;
    public interface CloseTabListener {
        void closeTab(Component c);
    }
    public ClosableTabComponent(String title, ImageIcon icon, JTabbedPane pane,
            CloseTabListener closeListener) {
        super(new FlowLayout(FlowLayout.LEFT, 0, 0));
        this.closeListener = closeListener;
        if (pane == null) {
            throw new NullPointerException("TabbedPane is null");
        }
        this.pane = pane;
        setOpaque(false);
        label = new JLabel(title);
        if (icon != null) {
            label.setIcon(icon);
        }
        add(label);
        label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
        add(button);
        setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
    }
    private class TabButton extends JButton implements ActionListener {
        private static final long serialVersionUID = 1L;
        public TabButton() {
            int size = 17;
            setPreferredSize(new Dimension(size, size));
            setToolTipText("Close");
            setUI(new BasicButtonUI());
            setContentAreaFilled(false);
            setFocusable(false);
            setBorder(BorderFactory.createEtchedBorder());
            setBorderPainted(false);
            addMouseListener(BUTTON_MOUSE_LISTENER);
            setRolloverEnabled(true);
            addActionListener(this);
        }
        public void actionPerformed(ActionEvent e) {
            int i = pane.indexOfTabComponent(ClosableTabComponent.this);
            Component c = pane.getComponentAt(i);
            if (i != -1) {
                pane.remove(i);
            }
            if (closeListener != null) {
                closeListener.closeTab(c);
            }
        }
        public void updateUI() {
        }
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g;
            Stroke stroke = g2.getStroke();
            g2.setStroke(new BasicStroke(2));
            g.setColor(Color.BLACK);
            if (getModel().isRollover()) {
                Color highlight = new Color(0, 51, 153);
                g.setColor(highlight);
            }
            int delta = 5;
            g.drawLine(delta, delta, getWidth() - delta - 1, getHeight() - delta - 1);
            g.drawLine(getWidth() - delta - 1, delta, delta, getHeight() - delta - 1);
            g2.setStroke(stroke);
            int i = pane.indexOfTabComponent(ClosableTabComponent.this);
            pane.setTitleAt(i, label.getText());
        }
    }
}
src/com/gitblit/client/DateCellRenderer.java
@@ -15,29 +15,44 @@
 */
package com.gitblit.client;
import java.awt.Color;
import java.awt.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.table.DefaultTableCellRenderer;
import com.gitblit.utils.TimeUtils;
/**
 * Time ago cell renderer with real date tooltip.
 *
 * @author James Moger
 *
 */
public class DateCellRenderer extends DefaultTableCellRenderer {
    private static final long serialVersionUID = 1L;
    private final String pattern;
    public DateCellRenderer(String pattern) {
    public DateCellRenderer(String pattern, Color foreground) {
        this.pattern = (pattern == null ? "yyyy-MM-dd HH:mm" : pattern);
        setForeground(foreground);
        setHorizontalAlignment(SwingConstants.CENTER);
    }
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
            boolean hasFocus, int row, int column) {
        super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        if (value instanceof Date) {
            Date date = (Date) value;
            String timeAgo = TimeUtils.timeAgo(date);
            String strDate = new SimpleDateFormat(pattern).format((Date) value);
            this.setText(strDate);
            this.setText(timeAgo);
            this.setToolTipText(strDate);
        }
        return this;
    }
src/com/gitblit/client/GitblitClient.java
@@ -16,84 +16,179 @@
package com.gitblit.client;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Menu;
import java.awt.MenuBar;
import java.awt.MenuItem;
import java.awt.MenuShortcut;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import com.gitblit.Constants;
import com.gitblit.utils.StringUtils;
/**
 * Sample RPC application.
 *
 * @author James Moger
 *
 */
public class GitblitClient extends JFrame {
    private static final long serialVersionUID = 1L;
    private JTabbedPane serverTabs;
    private GitblitRegistration localhost = new GitblitRegistration("default",
            "https://localhost:8443", "admin", "admin".toCharArray());
    private List<GitblitRegistration> registrations = new ArrayList<GitblitRegistration>();
    private JMenu recentMenu;
    private GitblitClient() {
        super();
    }
    private void initialize() {
        setupMenu();
        setContentPane(getCenterPanel());
        setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
        setTitle("Gitblit Client v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")");
        setTitle("Gitblit RPC Client v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(800, 600);
        setLocationRelativeTo(null);
        setSize(950, 600);
    }
    private void setupMenu() {
        MenuBar menuBar = new MenuBar();
        setMenuBar(menuBar);
        Menu serversMenu = new Menu("Servers");
    public void setVisible(boolean value) {
        if (value) {
            if (registrations.size() == 0) {
                // default prompt
                if (loginPrompt(localhost)) {
                    pack();
                }
            } else if (registrations.size() == 1) {
                // single registration prompt
                if (loginPrompt(registrations.get(0))) {
                    pack();
                }
            }
            super.setVisible(value);
            setLocationRelativeTo(null);
        }
    }
    private JMenuBar setupMenu() {
        JMenuBar menuBar = new JMenuBar();
        JMenu serversMenu = new JMenu("Servers");
        menuBar.add(serversMenu);
        MenuItem login = new MenuItem("Login...", new MenuShortcut(KeyEvent.VK_L, false));
        recentMenu = new JMenu("Recent");
        serversMenu.add(recentMenu);
        JMenuItem login = new JMenuItem("Login...");
        login.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, KeyEvent.CTRL_DOWN_MASK, false));
        login.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                String url = JOptionPane.showInputDialog(GitblitClient.this,
                        "Please enter Gitblit server URL", "https://localhost:8443");
                if (StringUtils.isEmpty(url)) {
                    return;
                }
                login(url, "admin", "admin".toCharArray());
                loginPrompt(localhost);
            }
        });
        serversMenu.add(login);
        return menuBar;
    }
    private JPanel newLabelPanel(String text, JTextField field) {
        JLabel label = new JLabel(text);
        label.setPreferredSize(new Dimension(75, 10));
        JPanel jpanel = new JPanel(new BorderLayout());
        jpanel.add(label, BorderLayout.WEST);
        jpanel.add(field, BorderLayout.CENTER);
        return jpanel;
    }
    private JPanel getCenterPanel() {
        serverTabs = new JTabbedPane(JTabbedPane.TOP);
        JMenuBar menubar = setupMenu();
        JPanel panel = new JPanel(new BorderLayout());
        panel.add(menubar, BorderLayout.NORTH);
        panel.add(serverTabs, BorderLayout.CENTER);
        return panel;
    }
    private void login(String url, String account, char[] password) {
    private boolean loginPrompt(GitblitRegistration reg) {
        JTextField urlField = new JTextField(reg.url, 30);
        JTextField nameField = new JTextField(reg.name);
        JTextField accountField = new JTextField(reg.account);
        JPasswordField passwordField = new JPasswordField(new String(reg.password));
        JPanel panel = new JPanel(new GridLayout(0, 1, 5, 5));
        panel.add(newLabelPanel("name", nameField));
        panel.add(newLabelPanel("url", urlField));
        panel.add(newLabelPanel("account", accountField));
        panel.add(newLabelPanel("password", passwordField));
        int result = JOptionPane.showConfirmDialog(GitblitClient.this, panel, "Login",
                JOptionPane.OK_CANCEL_OPTION);
        if (result != JOptionPane.OK_OPTION) {
            return false;
        }
        String url = urlField.getText();
        if (StringUtils.isEmpty(url)) {
            return false;
        }
        reg = new GitblitRegistration(nameField.getText(), url, accountField.getText(),
                passwordField.getPassword());
        login(reg);
        registrations.add(0, reg);
        rebuildRecentMenu();
        return true;
    }
    private void login(GitblitRegistration reg) {
        try {
            GitblitPanel panel = new GitblitPanel(url, account, password);
            GitblitPanel panel = new GitblitPanel(reg);
            panel.login();
            serverTabs.addTab(url.substring(url.indexOf("//") + 2), panel);
            serverTabs.setSelectedIndex(serverTabs.getTabCount() - 1);
            serverTabs.addTab(reg.name, panel);
            int idx = serverTabs.getTabCount() - 1;
            serverTabs.setSelectedIndex(idx);
            serverTabs.setTabComponentAt(idx, new ClosableTabComponent(reg.name, null, serverTabs,
                    panel));
        } catch (IOException e) {
            JOptionPane.showMessageDialog(GitblitClient.this, e.getMessage(), "Error",
                    JOptionPane.ERROR_MESSAGE);
        }
    }
    private void rebuildRecentMenu() {
        recentMenu.removeAll();
        for (final GitblitRegistration reg : registrations) {
            JMenuItem item = new JMenuItem(reg.name);
            item.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    login(reg);
                }
            });
            recentMenu.add(item);
        }
    }
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (Exception e) {
                }
                GitblitClient frame = new GitblitClient();
                frame.initialize();
                frame.setVisible(true);
src/com/gitblit/client/GitblitClientLauncher.java
New file
@@ -0,0 +1,105 @@
/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.client;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.SplashScreen;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import com.gitblit.Launcher;
import com.gitblit.build.Build;
import com.gitblit.build.Build.DownloadListener;
/**
 * Downloads dependencies and launches RPC client.
 *
 * @author James Moger
 *
 */
public class GitblitClientLauncher {
    public static void main(String[] args) {
        final SplashScreen splash = SplashScreen.getSplashScreen();
        DownloadListener downloadListener = new DownloadListener() {
            @Override
            public void downloading(String name) {
                updateSplash(splash, "Downloading " + name + "...");
            }
        };
        // download rpc client runtime dependencies
        Build.rpcClient(downloadListener);
        updateSplash(splash, "Scanning Library Folder...");
        File libFolder = new File("ext");
        List<File> jars = Launcher.findJars(libFolder.getAbsoluteFile());
        // sort the jars by name and then reverse the order so the newer version
        // of the library gets loaded in the event that this is an upgrade
        Collections.sort(jars);
        Collections.reverse(jars);
        for (File jar : jars) {
            try {
                updateSplash(splash, "Loading " + jar.getName() + "...");
                Launcher.addJarFile(jar);
            } catch (IOException e) {
            }
        }
        updateSplash(splash, "Starting Gitblit RPC Client...");
        GitblitClient.main(args);
    }
    private static void updateSplash(final SplashScreen splash, final String string) {
        if (splash == null) {
            return;
        }
        try {
            EventQueue.invokeAndWait(new Runnable() {
                public void run() {
                    Graphics2D g = splash.createGraphics();
                    if (g != null) {
                        // Splash is 320x120
                        FontMetrics fm = g.getFontMetrics();
                        g.setColor(Color.darkGray);
                        int h = fm.getHeight() + fm.getMaxDescent();
                        int x = 5;
                        int y = 115;
                        int w = 320 - 2 * x;
                        g.fillRect(x, y - h, w, h);
                        g.setColor(Color.lightGray);
                        g.drawRect(x, y - h, w, h);
                        g.setColor(Color.WHITE);
                        int xw = fm.stringWidth(string);
                        g.drawString(string, x + ((w - xw) / 2), y - 5);
                        g.dispose();
                        splash.update();
                    }
                }
            });
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}
src/com/gitblit/client/GitblitPanel.java
@@ -16,59 +16,398 @@
package com.gitblit.client;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.SwingConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableRowSorter;
import com.gitblit.GitBlitException.ForbiddenException;
import com.gitblit.client.ClosableTabComponent.CloseTabListener;
import com.gitblit.models.FederationModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.RpcUtils;
import com.gitblit.utils.StringUtils;
public class GitblitPanel extends JPanel {
/**
 * GitblitPanel performs the login, all business logic, and contains all widgets
 * to represent the state of a repository for the given account credentials.
 *
 * @author James Moger
 *
 */
public class GitblitPanel extends JPanel implements CloseTabListener {
    private static final long serialVersionUID = 1L;
    String url;
    String account;
    char[] password;
    private final int margin = 5;
    JTabbedPane tabs;
    private final Insets insets = new Insets(margin, margin, margin, margin);
    private String url;
    private String account;
    private char[] password;
    private boolean isAdmin;
    private JTabbedPane tabs;
    private JTable repositoriesTable;
    private RepositoriesModel repositoriesModel;
    private JList usersList;
    private JPanel usersPanel;
    private JButton createRepository;
    private JButton delRepository;
    private NameRenderer nameRenderer;
    private TypeRenderer typeRenderer;
    private DefaultTableCellRenderer ownerRenderer;
    private DefaultTableCellRenderer sizeRenderer;
    private TableRowSorter<RepositoriesModel> defaultSorter;
    public GitblitPanel(GitblitRegistration reg) {
        this(reg.url, reg.account, reg.password);
    }
    public GitblitPanel(String url, String account, char[] password) {
        this.url = url;
        this.account = account;
        this.password = password;
        tabs = new JTabbedPane(JTabbedPane.TOP);
        repositoriesTable = new JTable();
        repositoriesTable.setDefaultRenderer(Date.class, new DateCellRenderer(null));
        final JButton browseRepository = new JButton("Browse");
        browseRepository.setEnabled(false);
        browseRepository.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                RepositoryModel model = getSelectedRepositories().get(0);
                String u = MessageFormat.format("{0}/summary/{1}", GitblitPanel.this.url,
                        StringUtils.encodeURL(model.name));
                try {
                    Desktop.getDesktop().browse(new URI(u));
                } catch (Exception x) {
                    x.printStackTrace();
                }
            }
        });
        tabs.addTab("Repositories", new JScrollPane(repositoriesTable));
        createRepository = new JButton("Create");
        createRepository.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.out.println("TODO Create Repository");
            }
        });
        final JButton editRepository = new JButton("Edit");
        editRepository.setEnabled(false);
        editRepository.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                for (RepositoryModel model : getSelectedRepositories()) {
                    System.out.println("TODO Edit " + model);
                }
            }
        });
        delRepository = new JButton("Delete");
        delRepository.setEnabled(false);
        delRepository.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                for (RepositoryModel model : getSelectedRepositories()) {
                    System.out.println("TODO Delete " + model);
                }
            }
        });
        final JButton cloneRepository = new JButton("Clone");
        cloneRepository.setEnabled(false);
        cloneRepository.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                for (RepositoryModel model : getSelectedRepositories()) {
                    System.out.println("TODO Clone " + model);
                }
            }
        });
        nameRenderer = new NameRenderer(Color.gray, new Color(0x00, 0x69, 0xD6));
        typeRenderer = new TypeRenderer();
        sizeRenderer = new DefaultTableCellRenderer();
        sizeRenderer.setHorizontalAlignment(SwingConstants.RIGHT);
        sizeRenderer.setForeground(new Color(0, 0x80, 0));
        ownerRenderer = new DefaultTableCellRenderer();
        ownerRenderer.setForeground(Color.gray);
        ownerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
        repositoriesModel = new RepositoriesModel();
        defaultSorter = new TableRowSorter<RepositoriesModel>(repositoriesModel);
        repositoriesTable = new JTable(repositoriesModel);
        repositoriesTable.setRowSorter(defaultSorter);
        repositoriesTable.getRowSorter().toggleSortOrder(RepositoriesModel.Columns.Name.ordinal());
        repositoriesTable.setCellSelectionEnabled(false);
        repositoriesTable.setRowSelectionAllowed(true);
        repositoriesTable.setRowHeight(nameRenderer.getFont().getSize() + 8);
        repositoriesTable.getTableHeader().setReorderingAllowed(false);
        repositoriesTable.setGridColor(new Color(0xd9d9d9));
        repositoriesTable.setBackground(Color.white);
        repositoriesTable.setDefaultRenderer(Date.class,
                new DateCellRenderer(null, Color.orange.darker()));
        setRenderer(RepositoriesModel.Columns.Name, nameRenderer);
        setRenderer(RepositoriesModel.Columns.Type, typeRenderer);
        setRenderer(RepositoriesModel.Columns.Owner, ownerRenderer);
        setRenderer(RepositoriesModel.Columns.Size, sizeRenderer);
        repositoriesTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting()) {
                    return;
                }
                boolean singleSelection = repositoriesTable.getSelectedRowCount() == 1;
                boolean selected = repositoriesTable.getSelectedRow() > -1;
                browseRepository.setEnabled(singleSelection);
                delRepository.setEnabled(selected);
                cloneRepository.setEnabled(selected);
                if (selected) {
                    int viewRow = repositoriesTable.getSelectedRow();
                    int modelRow = repositoriesTable.convertRowIndexToModel(viewRow);
                    RepositoryModel model = ((RepositoriesModel) repositoriesTable.getModel()).list
                            .get(modelRow);
                    editRepository.setEnabled(singleSelection
                            && (isAdmin || model.owner.equalsIgnoreCase(GitblitPanel.this.account)));
                } else {
                    editRepository.setEnabled(false);
                }
            }
        });
        final JTextField repositoryFilter = new JTextField();
        repositoryFilter.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                filterRepositories(repositoryFilter.getText());
            }
        });
        JPanel filterPanel = new JPanel(new BorderLayout(margin, margin));
        filterPanel.add(new JLabel("Filter"), BorderLayout.WEST);
        filterPanel.add(repositoryFilter, BorderLayout.CENTER);
        JPanel tablePanel = new JPanel(new BorderLayout(margin, margin));
        tablePanel.add(filterPanel, BorderLayout.NORTH);
        tablePanel.add(new JScrollPane(repositoriesTable), BorderLayout.CENTER);
        JPanel repositoryControls = new JPanel();
        repositoryControls.add(browseRepository);
        repositoryControls.add(cloneRepository);
        repositoryControls.add(createRepository);
        repositoryControls.add(editRepository);
        repositoryControls.add(delRepository);
        JPanel repositoriesPanel = new JPanel(new BorderLayout(margin, margin));
        repositoriesPanel.add(newHeaderLabel("Repositories"), BorderLayout.NORTH);
        repositoriesPanel.add(tablePanel, BorderLayout.CENTER);
        repositoriesPanel.add(repositoryControls, BorderLayout.SOUTH);
        JButton createUser = new JButton("Create");
        createUser.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.out.println("TODO Create User");
            }
        });
        final JButton editUser = new JButton("Edit");
        editUser.setEnabled(false);
        editUser.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                for (UserModel user : getSelectedUsers()) {
                    System.out.println("TODO Edit " + user);
                }
            }
        });
        final JButton delUser = new JButton("Delete");
        delUser.setEnabled(false);
        delUser.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                for (UserModel user : getSelectedUsers()) {
                    System.out.println("TODO Delete " + user);
                }
            }
        });
        usersList = new JList();
        usersList.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting()) {
                    return;
                }
                boolean selected = usersList.getSelectedIndex() > -1;
                boolean singleSelection = usersList.getSelectedIndices().length == 1;
                editUser.setEnabled(singleSelection && selected);
                delUser.setEnabled(selected);
            }
        });
        JPanel userControls = new JPanel();
        userControls.add(createUser);
        userControls.add(editUser);
        userControls.add(delUser);
        usersPanel = new JPanel(new BorderLayout(margin, margin));
        usersPanel.add(newHeaderLabel("Users"), BorderLayout.NORTH);
        usersPanel.add(new JScrollPane(usersList), BorderLayout.CENTER);
        usersPanel.add(userControls, BorderLayout.SOUTH);
        /*
         * Assemble the main panel
         */
        JPanel mainPanel = new JPanel(new BorderLayout(margin, margin));
        mainPanel.add(repositoriesPanel, BorderLayout.CENTER);
        mainPanel.add(usersPanel, BorderLayout.EAST);
        tabs = new JTabbedPane(JTabbedPane.BOTTOM);
        tabs.addTab("Main", mainPanel);
        tabs.addTab("Federation", new JPanel());
        setLayout(new BorderLayout());
        add(tabs, BorderLayout.CENTER);
    }
    private JLabel newHeaderLabel(String text) {
        JLabel label = new JLabel(text);
        label.setOpaque(true);
        label.setForeground(Color.white);
        label.setBackground(Color.gray);
        label.setFont(label.getFont().deriveFont(14f));
        return label;
    }
    public void login() throws IOException {
        refreshRepositoriesTable();
        try {
            refreshUsersTable();
            isAdmin = true;
            refreshFederationPanel();
        } catch (ForbiddenException e) {
            // user does not have administrator privileges
            // hide admin repository buttons
            createRepository.setVisible(false);
            delRepository.setVisible(false);
            // hide users panel
            usersPanel.setVisible(false);
            // remove federation tab
            tabs.removeTabAt(1);
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }
    private void refreshRepositoriesTable() throws IOException {
        Map<String, RepositoryModel> repositories = RpcUtils
                .getRepositories(url, account, password);
        repositoriesTable.setModel(new RepositoriesModel(repositories));
        repositoriesModel.list.clear();
        repositoriesModel.list.addAll(repositories.values());
        repositoriesModel.fireTableDataChanged();
        packColumns(repositoriesTable, 2);
    }
    private void setRenderer(RepositoriesModel.Columns col, TableCellRenderer renderer) {
        String name = repositoriesTable.getColumnName(col.ordinal());
        repositoriesTable.getColumn(name).setCellRenderer(renderer);
    }
    private void refreshUsersTable() throws IOException {
        List<UserModel> users = RpcUtils.getUsers(url, account, password);
        usersList.setListData(users.toArray());
    }
    private void refreshFederationPanel() throws IOException {
        List<FederationModel> registrations = RpcUtils.getFederationRegistrations(url, account,
                password);
    }
    private void filterRepositories(final String fragment) {
        if (StringUtils.isEmpty(fragment)) {
            repositoriesTable.setRowSorter(defaultSorter);
            return;
        }
        RowFilter<RepositoriesModel, Object> containsFilter = new RowFilter<RepositoriesModel, Object>() {
            public boolean include(Entry<? extends RepositoriesModel, ? extends Object> entry) {
                for (int i = entry.getValueCount() - 1; i >= 0; i--) {
                    if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
                        return true;
                    }
                }
                return false;
            }
        };
        RepositoriesModel model = (RepositoriesModel) repositoriesTable.getModel();
        TableRowSorter<RepositoriesModel> sorter = new TableRowSorter<RepositoriesModel>(model);
        sorter.setRowFilter(containsFilter);
        repositoriesTable.setRowSorter(sorter);
    }
    private List<RepositoryModel> getSelectedRepositories() {
        List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
        for (int viewRow : repositoriesTable.getSelectedRows()) {
            int modelRow = repositoriesTable.convertRowIndexToModel(viewRow);
            RepositoryModel model = ((RepositoriesModel) repositoriesTable.getModel()).list
                    .get(modelRow);
            repositories.add(model);
        }
        return repositories;
    }
    private List<UserModel> getSelectedUsers() {
        List<UserModel> users = new ArrayList<UserModel>();
        for (int viewRow : usersList.getSelectedIndices()) {
            UserModel model = (UserModel) usersList.getModel().getElementAt(viewRow);
            users.add(model);
        }
        return users;
    }
    private void packColumns(JTable table, int margin) {
@@ -109,4 +448,21 @@
        // Set the width
        col.setPreferredWidth(width);
    }
    @Override
    public Insets getInsets() {
        return insets;
    }
    @Override
    public Dimension getPreferredSize() {
        if (isAdmin) {
            return new Dimension(950, 550);
        }
        return new Dimension(775, 450);
    }
    @Override
    public void closeTab(Component c) {
    }
}
src/com/gitblit/client/GitblitRegistration.java
New file
@@ -0,0 +1,47 @@
/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.client;
import java.io.Serializable;
import com.gitblit.utils.StringUtils;
/**
 * Simple class to encapsulate a Gitblit server registration.
 *
 * @author James Moger
 *
 */
public class GitblitRegistration implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    String url;
    String account;
    char[] password;
    public GitblitRegistration(String name, String url, String account, char[] password) {
        this.url = url;
        this.account = account;
        this.password = password;
        if (StringUtils.isEmpty(name)) {
            this.name = url.substring(url.indexOf("//") + 2);
        } else {
            this.name = name;
        }
    }
}
src/com/gitblit/client/NameRenderer.java
New file
@@ -0,0 +1,65 @@
/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.client;
import java.awt.Color;
import java.awt.Component;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
/**
 * Repository name cell renderer. This renderer shows the group name in a gray
 * color and accentuates the repository name in a cornflower blue color.
 *
 * @author James Moger
 *
 */
public class NameRenderer extends DefaultTableCellRenderer {
    private static final long serialVersionUID = 1L;
    final String groupSpan;
    public NameRenderer(Color group, Color repo) {
        groupSpan = "<span style='color:" + getHexColor(group) + "'>";
        setForeground(repo);
    }
    String getHexColor(Color c) {
        StringBuilder sb = new StringBuilder();
        sb.append(Integer.toHexString((c.getRGB() & 0x00FFFFFF)));
        while (sb.length() < 6)
            sb.insert(0, '0');
        sb.insert(0, '#');
        return sb.toString();
    }
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
            boolean hasFocus, int row, int column) {
        super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        String name = value.toString();
        int lastSlash = name.lastIndexOf('/');
        if (!isSelected && lastSlash > -1) {
            String group = name.substring(0, lastSlash + 1);
            String repo = name.substring(lastSlash + 1);
            setText("<html><body>" + groupSpan + group + "</span>" + repo);
        } else {
            this.setText(name);
        }
        return this;
    }
}
src/com/gitblit/client/RepositoriesModel.java
@@ -19,22 +19,25 @@
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.swing.table.AbstractTableModel;
import com.gitblit.models.RepositoryModel;
/**
 * Table model of a list of repositories.
 *
 * @author James Moger
 *
 */
public class RepositoriesModel extends AbstractTableModel {
    private static final long serialVersionUID = 1L;
    Map<String, RepositoryModel> repositories;
    List<RepositoryModel> list;
    enum Columns {
        Name, Description, Owner, Last_Change, Size;
        Name, Description, Owner, Type, Last_Change, Size;
        @Override
        public String toString() {
@@ -42,15 +45,18 @@
        }
    }
    public RepositoriesModel(Map<String, RepositoryModel> repositories) {
        this.repositories = repositories;
        list = new ArrayList<RepositoryModel>(repositories.values());
        Collections.sort(list);
    public RepositoriesModel() {
        this(new ArrayList<RepositoryModel>());
    }
    public RepositoriesModel(List<RepositoryModel> repositories) {
        this.list = repositories;
        Collections.sort(this.list);
    }
    @Override
    public int getRowCount() {
        return repositories.size();
        return list.size();
    }
    @Override
@@ -74,6 +80,9 @@
    public Class<?> getColumnClass(int columnIndex) {
        Columns col = Columns.values()[columnIndex];
        switch (col) {
        case Name:
        case Type:
            return RepositoryModel.class;
        case Last_Change:
            return Date.class;
        }
@@ -86,11 +95,13 @@
        Columns col = Columns.values()[columnIndex];
        switch (col) {
        case Name:
            return model.name;
            return model;
        case Description:
            return model.description;
        case Owner:
            return model.owner;
        case Type:
            return model;
        case Last_Change:
            return model.lastChange;
        case Size:
src/com/gitblit/client/TypeRenderer.java
New file
@@ -0,0 +1,119 @@
/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.client;
import java.awt.Component;
import java.awt.GridLayout;
import java.io.Serializable;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;
import com.gitblit.models.RepositoryModel;
/**
 * Renders the type indicators (tickets, frozen, access restriction, etc) in a
 * single cell.
 *
 * @author James Moger
 *
 */
public class TypeRenderer extends JPanel implements TableCellRenderer, Serializable {
    private static final long serialVersionUID = 1L;
    private final ImageIcon blankIcon;
    private final ImageIcon pushIcon;
    private final ImageIcon pullIcon;
    private final ImageIcon viewIcon;
    private final ImageIcon tixIcon;
    private final ImageIcon doxIcon;
    private final ImageIcon frozenIcon;
    private final ImageIcon federatedIcon;
    public TypeRenderer() {
        super(new GridLayout(1, 0, 1, 0));
        blankIcon = new ImageIcon(getClass().getResource("/blank.png"));
        pushIcon = new ImageIcon(getClass().getResource("/lock_go_16x16.png"));
        pullIcon = new ImageIcon(getClass().getResource("/lock_pull_16x16.png"));
        viewIcon = new ImageIcon(getClass().getResource("/shield_16x16.png"));
        tixIcon = new ImageIcon(getClass().getResource("/bug_16x16.png"));
        doxIcon = new ImageIcon(getClass().getResource("/book_16x16.png"));
        frozenIcon = new ImageIcon(getClass().getResource("/cold_16x16.png"));
        federatedIcon = new ImageIcon(getClass().getResource("/federated_16x16.png"));
    }
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
            boolean hasFocus, int row, int column) {
        if (isSelected)
            setBackground(table.getSelectionBackground());
        else
            setBackground(table.getBackground());
        removeAll();
        if (value instanceof RepositoryModel) {
            RepositoryModel model = (RepositoryModel) value;
            if (model.useTickets) {
                add(new JLabel(tixIcon));
            } else {
                add(new JLabel(blankIcon));
            }
            if (model.useDocs) {
                add(new JLabel(doxIcon));
            } else {
                add(new JLabel(blankIcon));
            }
            if (model.isFrozen) {
                add(new JLabel(frozenIcon));
            } else {
                add(new JLabel(blankIcon));
            }
            if (model.isFederated) {
                add(new JLabel(federatedIcon));
            } else {
                add(new JLabel(blankIcon));
            }
            switch (model.accessRestriction) {
            case NONE:
                add(new JLabel(blankIcon));
                break;
            case PUSH:
                add(new JLabel(pushIcon));
                break;
            case CLONE:
                add(new JLabel(pullIcon));
                break;
            case VIEW:
                add(new JLabel(viewIcon));
                break;
            default:
                add(new JLabel(blankIcon));
            }
        }
        return this;
    }
}
src/com/gitblit/client/splash.png