From 841651baee2181c1543555d1eabcd0e4fee48827 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 05 Oct 2011 22:22:43 -0400
Subject: [PATCH] New setting to disable RPC administration. Advancing the RPC client.

---
 src/com/gitblit/client/GitblitClient.java         |  141 +++++-
 src/com/gitblit/client/GitblitPanel.java          |  378 ++++++++++++++++++
 src/com/gitblit/client/DateCellRenderer.java      |   19 
 src/com/gitblit/client/splash.png                 |    0 
 distrib/gitblit.properties                        |   12 
 src/com/gitblit/build/Build.java                  |   19 
 src/com/gitblit/client/GitblitRegistration.java   |   47 ++
 src/com/gitblit/client/TypeRenderer.java          |  119 +++++
 src/com/gitblit/client/GitblitClientLauncher.java |  105 +++++
 src/com/gitblit/RpcFilter.java                    |   22 
 docs/04_releases.mkd                              |   11 
 src/com/gitblit/client/ClosableTabComponent.java  |  149 +++++++
 src/com/gitblit/client/NameRenderer.java          |   65 +++
 build.xml                                         |   68 +++
 src/com/gitblit/client/RepositoriesModel.java     |   31 +
 docs/00_index.mkd                                 |   12 
 16 files changed, 1,133 insertions(+), 65 deletions(-)

diff --git a/build.xml b/build.xml
index 9a58971..fa40abd 100644
--- a/build.xml
+++ b/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>
 
 	
diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index 3de1475..14ec79f 100644
--- a/distrib/gitblit.properties
+++ b/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.
 #
diff --git a/docs/00_index.mkd b/docs/00_index.mkd
index ee54b18..489f84a 100644
--- a/docs/00_index.mkd
+++ b/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
diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd
index 68a0193..43168dd 100644
--- a/docs/04_releases.mkd
+++ b/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
diff --git a/src/com/gitblit/RpcFilter.java b/src/com/gitblit/RpcFilter.java
index 49df844..f92dd96 100644
--- a/src/com/gitblit/RpcFilter.java
+++ b/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) {
diff --git a/src/com/gitblit/build/Build.java b/src/com/gitblit/build/Build.java
index 684f278..862c295 100644
--- a/src/com/gitblit/build/Build.java
+++ b/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);
diff --git a/src/com/gitblit/client/ClosableTabComponent.java b/src/com/gitblit/client/ClosableTabComponent.java
new file mode 100644
index 0000000..a121806
--- /dev/null
+++ b/src/com/gitblit/client/ClosableTabComponent.java
@@ -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());
+		}
+	}
+}
diff --git a/src/com/gitblit/client/DateCellRenderer.java b/src/com/gitblit/client/DateCellRenderer.java
index 591926b..053cf52 100644
--- a/src/com/gitblit/client/DateCellRenderer.java
+++ b/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;
 	}
diff --git a/src/com/gitblit/client/GitblitClient.java b/src/com/gitblit/client/GitblitClient.java
index d10cede..51d8e7e 100644
--- a/src/com/gitblit/client/GitblitClient.java
+++ b/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);
diff --git a/src/com/gitblit/client/GitblitClientLauncher.java b/src/com/gitblit/client/GitblitClientLauncher.java
new file mode 100644
index 0000000..19e9efd
--- /dev/null
+++ b/src/com/gitblit/client/GitblitClientLauncher.java
@@ -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();
+		}
+	}
+}
diff --git a/src/com/gitblit/client/GitblitPanel.java b/src/com/gitblit/client/GitblitPanel.java
index 911ec0c..5482593 100644
--- a/src/com/gitblit/client/GitblitPanel.java
+++ b/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) {
+	}
 }
diff --git a/src/com/gitblit/client/GitblitRegistration.java b/src/com/gitblit/client/GitblitRegistration.java
new file mode 100644
index 0000000..482bf8f
--- /dev/null
+++ b/src/com/gitblit/client/GitblitRegistration.java
@@ -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;
+		}
+	}
+}
diff --git a/src/com/gitblit/client/NameRenderer.java b/src/com/gitblit/client/NameRenderer.java
new file mode 100644
index 0000000..41393fb
--- /dev/null
+++ b/src/com/gitblit/client/NameRenderer.java
@@ -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;
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/client/RepositoriesModel.java b/src/com/gitblit/client/RepositoriesModel.java
index 2a439fb..d8e448f 100644
--- a/src/com/gitblit/client/RepositoriesModel.java
+++ b/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:
diff --git a/src/com/gitblit/client/TypeRenderer.java b/src/com/gitblit/client/TypeRenderer.java
new file mode 100644
index 0000000..8f92dcf
--- /dev/null
+++ b/src/com/gitblit/client/TypeRenderer.java
@@ -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;
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/client/splash.png b/src/com/gitblit/client/splash.png
new file mode 100644
index 0000000..d63932f
--- /dev/null
+++ b/src/com/gitblit/client/splash.png
Binary files differ

--
Gitblit v1.9.1