From f08aab5c5e632431635e73b47b6096dc47243755 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 13 Dec 2011 08:37:02 -0500
Subject: [PATCH] Teams JSON-RPC support

---
 docs/02_rpc.mkd                                  |   65 +-
 src/com/gitblit/client/GitblitClient.java        |   93 +++
 src/com/gitblit/client/EditTeamDialog.java       |  269 +++++++++++
 src/com/gitblit/client/RepositoriesPanel.java    |   22 
 src/com/gitblit/client/GitblitPanel.java         |   44 +
 src/com/gitblit/utils/RpcUtils.java              |  130 +++++
 src/com/gitblit/client/EditUserDialog.java       |   66 ++
 src/com/gitblit/client/UsersTableModel.java      |   13 
 src/com/gitblit/RpcServlet.java                  |   57 ++
 src/com/gitblit/client/TeamsTableModel.java      |  105 ++++
 src/com/gitblit/RpcFilter.java                   |    2 
 tests/com/gitblit/tests/RpcTests.java            |   78 +++
 src/com/gitblit/client/TeamsPanel.java           |  378 +++++++++++++++
 src/com/gitblit/client/UsersPanel.java           |   38 +
 src/com/gitblit/client/EditRepositoryDialog.java |   31 +
 src/com/gitblit/Constants.java                   |   10 
 16 files changed, 1,334 insertions(+), 67 deletions(-)

diff --git a/docs/02_rpc.mkd b/docs/02_rpc.mkd
index 7fe7634..5cd0052 100644
--- a/docs/02_rpc.mkd
+++ b/docs/02_rpc.mkd
@@ -57,32 +57,49 @@
 
 ## JSON Remote Procedure Call (RPC) Interface
 
+### RPC Protocol Versions
 <table>
-<tr><th colspan='2'>url parameters</th><th rowspan='2'>required<br/>user<br/>permission</th><th colspan='2'>json</th></tr>
+<tbody>
+<tr><th>Release</th><th>Protocol Version</th></tr>
+<tr><td>Gitblit v0.7.0</td><td>1 (inferred version)</td></tr>
+<tr><td>Gitblit v0.8.0</td><td>2</td></tr>
+</tbody>
+</table>
+
+### RPC Request and Response Types
+<table>
+<tr><th colspan='2'>url parameters</th><th rowspan='2'>required<br/>user<br/>permission</th><th rowspan='2'>protocol<br/>version</th><th colspan='2'>json</th></tr>
 <tr><th>req=</th><th>name=</th><th>post body</th><th>response body</th></tr>
-<tr><td colspan='5'><em>web.enableRpcServlet=true</em></td></tr>
-<tr><td>LIST_REPOSITORIES</td><td>-</td><td>-</td><td>-</td><td>Map&lt;String, RepositoryModel&gt;</td></tr>
-<tr><td>LIST_BRANCHES</td><td>-</td><td>-</td><td>-</td><td>Map&lt;String, List&lt;String&gt;&gt;</td></tr>
-<tr><td>LIST_SETTINGS</td><td>-</td><td><em>-</em></td><td>-</td><td>ServerSettings (basic keys)</td></tr>
-<tr><td colspan='5'><em>web.enableRpcManagement=true</em></td></tr>
-<tr><td>CREATE_REPOSITORY</td><td>repository name</td><td><em>admin</em></td><td>RepositoryModel</td><td>-</td></tr>
-<tr><td>EDIT_REPOSITORY</td><td>repository name</td><td><em>admin</em></td><td>RepositoryModel</td><td>-</td></tr>
-<tr><td>DELETE_REPOSITORY</td><td>repository name</td><td><em>admin</em></td><td>-</td><td>-</td></tr>
-<tr><td>LIST_USERS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List&lt;UserModel&gt;</td></tr>
-<tr><td>CREATE_USER</td><td>user name</td><td><em>admin</em></td><td>UserModel</td><td>-</td></tr>
-<tr><td>EDIT_USER</td><td>user name</td><td><em>admin</em></td><td>UserModel</td><td>-</td></tr>
-<tr><td>DELETE_USER</td><td>user name</td><td><em>admin</em></td><td>-</td><td>-</td></tr>
-<tr><td>LIST_REPOSITORY_MEMBERS</td><td>repository name</td><td><em>admin</em></td><td>-</td><td>List&lt;String&gt;</td></tr>
-<tr><td>SET_REPOSITORY_MEMBERS</td><td>repository name</td><td><em>admin</em></td><td>List&lt;String&gt;</td><td>-</td></tr>
-<tr><td>LIST_SETTINGS</td><td>-</td><td><em>admin</em></td><td>-</td><td>ServerSettings (management keys)</td></tr>
-<tr><td colspan='5'><em>web.enableRpcAdministration=true</em></td></tr>
-<tr><td>LIST_FEDERATION_REGISTRATIONS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List&lt;FederationModel&gt;</td></tr>
-<tr><td>LIST_FEDERATION_RESULTS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List&lt;FederationModel&gt;</td></tr>
-<tr><td>LIST_FEDERATION_PROPOSALS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List&lt;FederationProposal&gt;</td></tr>
-<tr><td>LIST_FEDERATION_SETS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List&lt;FederationSet&gt;</td></tr>
-<tr><td>LIST_SETTINGS</td><td>-</td><td><em>admin</em></td><td>-</td><td>ServerSettings (all keys)</td></tr>
-<tr><td>EDIT_SETTINGS</td><td>-</td><td><em>admin</em></td><td>Map&lt;String, String&gt;</td><td>-</td></tr>
-<tr><td>LIST_STATUS</td><td>-</td><td><em>admin</em></td><td>-</td><td>ServerStatus (see example below)</td></tr>
+<tr><td colspan='6'><em>web.enableRpcServlet=true</em></td></tr>
+<tr><td>GET_PROTOCOL</td><td>-</td><td>-</td><td>2</td><td>-</td><td>Integer</td></tr>
+<tr><td>LIST_REPOSITORIES</td><td>-</td><td>-</td><td>1</td><td>-</td><td>Map&lt;String, RepositoryModel&gt;</td></tr>
+<tr><td>LIST_BRANCHES</td><td>-</td><td>-</td><td>1</td><td>-</td><td>Map&lt;String, List&lt;String&gt;&gt;</td></tr>
+<tr><td>LIST_SETTINGS</td><td>-</td><td><em>-</em></td><td>1</td><td>-</td><td>ServerSettings (basic keys)</td></tr>
+<tr><td colspan='6'><em>web.enableRpcManagement=true</em></td></tr>
+<tr><td>CREATE_REPOSITORY</td><td>repository name</td><td><em>admin</em></td><td>1</td><td>RepositoryModel</td><td>-</td></tr>
+<tr><td>EDIT_REPOSITORY</td><td>repository name</td><td><em>admin</em></td><td>1</td><td>RepositoryModel</td><td>-</td></tr>
+<tr><td>DELETE_REPOSITORY</td><td>repository name</td><td><em>admin</em></td><td>1</td><td>-</td><td>-</td></tr>
+<tr><td>LIST_USERS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>List&lt;UserModel&gt;</td></tr>
+<tr><td>CREATE_USER</td><td>user name</td><td><em>admin</em></td><td>1</td><td>UserModel</td><td>-</td></tr>
+<tr><td>EDIT_USER</td><td>user name</td><td><em>admin</em></td><td>1</td><td>UserModel</td><td>-</td></tr>
+<tr><td>DELETE_USER</td><td>user name</td><td><em>admin</em></td><td>1</td><td>-</td><td>-</td></tr>
+<tr><td>LIST_TEAMS</td><td>-</td><td><em>admin</em></td><td>2</td><td>-</td><td>List&lt;TeamModel&gt;</td></tr>
+<tr><td>CREATE_TEAM</td><td>team name</td><td><em>admin</em></td><td>2</td><td>TeamModel</td><td>-</td></tr>
+<tr><td>EDIT_TEAM</td><td>team name</td><td><em>admin</em></td><td>2</td><td>TeamModel</td><td>-</td></tr>
+<tr><td>DELETE_TEAM</td><td>team name</td><td><em>admin</em></td><td>2</td><td>-</td><td>-</td></tr>
+<tr><td>LIST_REPOSITORY_MEMBERS</td><td>repository name</td><td><em>admin</em></td><td>1</td><td>-</td><td>List&lt;String&gt;</td></tr>
+<tr><td>SET_REPOSITORY_MEMBERS</td><td>repository name</td><td><em>admin</em></td><td>1</td><td>List&lt;String&gt;</td><td>-</td></tr>
+<tr><td>LIST_REPOSITORY_TEAMS</td><td>repository name</td><td><em>admin</em></td><td>2</td><td>-</td><td>List&lt;String&gt;</td></tr>
+<tr><td>SET_REPOSITORY_TEAMS</td><td>repository name</td><td><em>admin</em></td><td>2</td><td>List&lt;String&gt;</td><td>-</td></tr>
+<tr><td>LIST_SETTINGS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>ServerSettings (management keys)</td></tr>
+<tr><td colspan='6'><em>web.enableRpcAdministration=true</em></td></tr>
+<tr><td>LIST_FEDERATION_REGISTRATIONS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>List&lt;FederationModel&gt;</td></tr>
+<tr><td>LIST_FEDERATION_RESULTS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>List&lt;FederationModel&gt;</td></tr>
+<tr><td>LIST_FEDERATION_PROPOSALS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>List&lt;FederationProposal&gt;</td></tr>
+<tr><td>LIST_FEDERATION_SETS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>List&lt;FederationSet&gt;</td></tr>
+<tr><td>LIST_SETTINGS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>ServerSettings (all keys)</td></tr>
+<tr><td>EDIT_SETTINGS</td><td>-</td><td><em>admin</em></td><td>1</td><td>Map&lt;String, String&gt;</td><td>-</td></tr>
+<tr><td>LIST_STATUS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>ServerStatus (see example below)</td></tr>
 </table>
 
 ### RPC/HTTP Response Codes
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index a2b9eeb..3279980 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -203,10 +203,12 @@
 	public static enum RpcRequest {
 		// Order is important here.  anything above LIST_SETTINGS requires
 		// administrator privileges and web.allowRpcManagement.
-		LIST_REPOSITORIES, LIST_BRANCHES, LIST_SETTINGS, CREATE_REPOSITORY, EDIT_REPOSITORY,
-		DELETE_REPOSITORY, LIST_USERS, CREATE_USER, EDIT_USER, DELETE_USER, 
-		LIST_REPOSITORY_MEMBERS, SET_REPOSITORY_MEMBERS, LIST_FEDERATION_REGISTRATIONS,
-		LIST_FEDERATION_RESULTS, LIST_FEDERATION_PROPOSALS, LIST_FEDERATION_SETS,
+		GET_PROTOCOL, LIST_REPOSITORIES, LIST_BRANCHES, LIST_SETTINGS,
+		CREATE_REPOSITORY, EDIT_REPOSITORY, DELETE_REPOSITORY, 
+		LIST_USERS, CREATE_USER, EDIT_USER, DELETE_USER, 
+		LIST_TEAMS, CREATE_TEAM, EDIT_TEAM, DELETE_TEAM,
+		LIST_REPOSITORY_MEMBERS, SET_REPOSITORY_MEMBERS, LIST_REPOSITORY_TEAMS, SET_REPOSITORY_TEAMS, 
+		LIST_FEDERATION_REGISTRATIONS, LIST_FEDERATION_RESULTS, LIST_FEDERATION_PROPOSALS, LIST_FEDERATION_SETS,
 		EDIT_SETTINGS, LIST_STATUS;
 
 		public static RpcRequest fromName(String name) {
diff --git a/src/com/gitblit/RpcFilter.java b/src/com/gitblit/RpcFilter.java
index 2ffb061..4c0f03d 100644
--- a/src/com/gitblit/RpcFilter.java
+++ b/src/com/gitblit/RpcFilter.java
@@ -135,6 +135,8 @@
 
 	private boolean canAccess(UserModel user, RpcRequest requestType) {
 		switch (requestType) {
+		case GET_PROTOCOL:
+			return true;
 		case LIST_REPOSITORIES:
 			return true;
 		default:
diff --git a/src/com/gitblit/RpcServlet.java b/src/com/gitblit/RpcServlet.java
index 068562e..115d553 100644
--- a/src/com/gitblit/RpcServlet.java
+++ b/src/com/gitblit/RpcServlet.java
@@ -33,6 +33,7 @@
 import com.gitblit.models.RefModel;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.ServerSettings;
+import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.HttpUtils;
 import com.gitblit.utils.JGitUtils;
@@ -47,6 +48,8 @@
 public class RpcServlet extends JsonServlet {
 
 	private static final long serialVersionUID = 1L;
+
+	public static final int PROTOCOL_VERSION = 2;
 
 	public RpcServlet() {
 		super();
@@ -77,7 +80,10 @@
 				&& GitBlit.getBoolean(Keys.web.enableRpcAdministration, false);
 
 		Object result = null;
-		if (RpcRequest.LIST_REPOSITORIES.equals(reqType)) {
+		if (RpcRequest.GET_PROTOCOL.equals(reqType)) {
+			// Return the protocol version
+			result = PROTOCOL_VERSION;
+		} else if (RpcRequest.LIST_REPOSITORIES.equals(reqType)) {
 			// Determine the Gitblit clone url
 			String gitblitUrl = HttpUtils.getGitblitURL(request);
 			StringBuilder sb = new StringBuilder();
@@ -128,6 +134,14 @@
 				users.add(GitBlit.self().getUserModel(name));
 			}
 			result = users;
+		} else if (RpcRequest.LIST_TEAMS.equals(reqType)) {
+			// list teams
+			List<String> names = GitBlit.self().getAllTeamnames();
+			List<TeamModel> teams = new ArrayList<TeamModel>();
+			for (String name : names) {
+				teams.add(GitBlit.self().getTeamModel(name));
+			}
+			result = teams;
 		} else if (RpcRequest.CREATE_REPOSITORY.equals(reqType)) {
 			// create repository
 			RepositoryModel model = deserialize(request, response, RepositoryModel.class);
@@ -180,6 +194,33 @@
 			if (!GitBlit.self().deleteUser(model.username)) {
 				response.setStatus(failureCode);
 			}
+		} else if (RpcRequest.CREATE_TEAM.equals(reqType)) {
+			// create team
+			TeamModel model = deserialize(request, response, TeamModel.class);
+			try {
+				GitBlit.self().updateTeamModel(model.name, model, true);
+			} catch (GitBlitException e) {
+				response.setStatus(failureCode);
+			}
+		} else if (RpcRequest.EDIT_TEAM.equals(reqType)) {
+			// edit team
+			TeamModel model = deserialize(request, response, TeamModel.class);
+			// name parameter specifies original team name in event of rename
+			String teamname = objectName;
+			if (teamname == null) {
+				teamname = model.name;
+			}
+			try {
+				GitBlit.self().updateTeamModel(teamname, model, false);
+			} catch (GitBlitException e) {
+				response.setStatus(failureCode);
+			}
+		} else if (RpcRequest.DELETE_TEAM.equals(reqType)) {
+			// delete team
+			TeamModel model = deserialize(request, response, TeamModel.class);
+			if (!GitBlit.self().deleteTeam(model.name)) {
+				response.setStatus(failureCode);
+			}
 		} else if (RpcRequest.LIST_REPOSITORY_MEMBERS.equals(reqType)) {
 			// get repository members
 			RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
@@ -190,6 +231,18 @@
 			Collection<String> names = deserialize(request, response, RpcUtils.NAMES_TYPE);
 			List<String> users = new ArrayList<String>(names);
 			if (!GitBlit.self().setRepositoryUsers(model, users)) {
+				response.setStatus(failureCode);
+			}
+		} else if (RpcRequest.LIST_REPOSITORY_TEAMS.equals(reqType)) {
+			// get repository teams
+			RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
+			result = GitBlit.self().getRepositoryTeams(model);
+		} else if (RpcRequest.SET_REPOSITORY_TEAMS.equals(reqType)) {
+			// update repository team access list
+			RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
+			Collection<String> names = deserialize(request, response, RpcUtils.NAMES_TYPE);
+			List<String> teams = new ArrayList<String>(names);
+			if (!GitBlit.self().setRepositoryTeams(model, teams)) {
 				response.setStatus(failureCode);
 			}
 		} else if (RpcRequest.LIST_FEDERATION_REGISTRATIONS.equals(reqType)) {
@@ -233,7 +286,7 @@
 				keys.add(Keys.web.siteName);
 				keys.add(Keys.web.mountParameters);
 				keys.add(Keys.web.syndicationEntries);
-				
+
 				if (allowManagement) {
 					// keys necessary for repository and/or user management
 					keys.add(Keys.realm.minPasswordLength);
diff --git a/src/com/gitblit/client/EditRepositoryDialog.java b/src/com/gitblit/client/EditRepositoryDialog.java
index ef0f58a..0b6ef59 100644
--- a/src/com/gitblit/client/EditRepositoryDialog.java
+++ b/src/com/gitblit/client/EditRepositoryDialog.java
@@ -52,6 +52,7 @@
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.Constants.FederationStrategy;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
 import com.gitblit.utils.StringUtils;
 
 /**
@@ -96,24 +97,26 @@
 	private JComboBox ownerField;
 
 	private JPalette<String> usersPalette;
-
+	
 	private JPalette<String> setsPalette;
+	
+	private JPalette<String> teamsPalette;
 
 	private Set<String> repositoryNames;
 
-	public EditRepositoryDialog() {
-		this(new RepositoryModel());
+	public EditRepositoryDialog(int protocolVersion) {
+		this(protocolVersion, new RepositoryModel());
 		this.isCreate = true;
 		setTitle(Translation.get("gb.newRepository"));
 	}
 
-	public EditRepositoryDialog(RepositoryModel aRepository) {
+	public EditRepositoryDialog(int protocolVersion, RepositoryModel aRepository) {
 		super();
 		this.repositoryName = aRepository.name;
 		this.repository = new RepositoryModel();
 		this.repositoryNames = new HashSet<String>();
 		this.isCreate = false;
-		initialize(aRepository);
+		initialize(protocolVersion, aRepository);
 		setModal(true);
 		setResizable(false);
 		setTitle(Translation.get("gb.edit") + ": " + aRepository.name);
@@ -132,7 +135,7 @@
 		return rootPane;
 	}
 
-	private void initialize(RepositoryModel anRepository) {
+	private void initialize(int protocolVersion, RepositoryModel anRepository) {
 		nameField = new JTextField(anRepository.name == null ? "" : anRepository.name, 35);
 		descriptionField = new JTextField(anRepository.description == null ? ""
 				: anRepository.description, 35);
@@ -195,6 +198,11 @@
 		accessPanel.add(newFieldPanel(Translation.get("gb.permittedUsers"), usersPalette),
 				BorderLayout.CENTER);
 
+		teamsPalette = new JPalette<String>();
+		JPanel teamsPanel = new JPanel(new BorderLayout(5, 5));
+		teamsPanel.add(newFieldPanel(Translation.get("gb.permittedTeams"), teamsPalette),
+				BorderLayout.CENTER);
+
 		setsPalette = new JPalette<String>();
 		JPanel federationPanel = new JPanel(new BorderLayout(5, 5));
 		federationPanel.add(
@@ -206,6 +214,9 @@
 		JTabbedPane panel = new JTabbedPane(JTabbedPane.TOP);
 		panel.addTab(Translation.get("gb.general"), fieldsPanel);
 		panel.addTab(Translation.get("gb.accessRestriction"), accessPanel);
+		if (protocolVersion >= 2) {
+			panel.addTab(Translation.get("gb.teams"), teamsPanel);
+		}
 		panel.addTab(Translation.get("gb.federation"), federationPanel);
 
 		JButton createButton = new JButton(Translation.get("gb.save"));
@@ -358,6 +369,10 @@
 		}
 		usersPalette.setObjects(all, selected);
 	}
+	
+	public void setTeams(List<String> all, List<String> selected) {
+		teamsPalette.setObjects(all, selected);
+	}
 
 	public void setRepositories(List<RepositoryModel> repositories) {
 		repositoryNames.clear();
@@ -385,6 +400,10 @@
 		return usersPalette.getSelections();
 	}
 
+	public List<String> getPermittedTeams() {
+		return teamsPalette.getSelections();
+	}
+
 	/**
 	 * ListCellRenderer to display descriptive text about the access
 	 * restriction.
diff --git a/src/com/gitblit/client/EditTeamDialog.java b/src/com/gitblit/client/EditTeamDialog.java
new file mode 100644
index 0000000..4297599
--- /dev/null
+++ b/src/com/gitblit/client/EditTeamDialog.java
@@ -0,0 +1,269 @@
+/*
+ * 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.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRootPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.TeamModel;
+import com.gitblit.utils.StringUtils;
+
+public class EditTeamDialog extends JDialog {
+
+	private static final long serialVersionUID = 1L;
+
+	private final String teamname;
+
+	private final TeamModel team;
+
+	private final ServerSettings settings;
+
+	private boolean isCreate;
+
+	private boolean canceled = true;
+
+	private JTextField teamnameField;
+
+	private JPalette<String> repositoryPalette;
+
+	private JPalette<String> userPalette;
+
+	private Set<String> teamnames;
+
+	public EditTeamDialog(int protocolVersion, ServerSettings settings) {
+		this(protocolVersion, new TeamModel(""), settings);
+		this.isCreate = true;
+		setTitle(Translation.get("gb.newTeam"));
+	}
+
+	public EditTeamDialog(int protocolVersion, TeamModel aTeam, ServerSettings settings) {
+		super();
+		this.teamname = aTeam.name;
+		this.team = new TeamModel("");
+		this.settings = settings;
+		this.teamnames = new HashSet<String>();
+		this.isCreate = false;
+		initialize(protocolVersion, aTeam);
+		setModal(true);
+		setTitle(Translation.get("gb.edit") + ": " + aTeam.name);
+		setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+	}
+
+	@Override
+	protected JRootPane createRootPane() {
+		KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+		JRootPane rootPane = new JRootPane();
+		rootPane.registerKeyboardAction(new ActionListener() {
+			public void actionPerformed(ActionEvent actionEvent) {
+				setVisible(false);
+			}
+		}, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
+		return rootPane;
+	}
+
+	private void initialize(int protocolVersion, TeamModel aTeam) {
+		teamnameField = new JTextField(aTeam.name == null ? "" : aTeam.name, 25);
+
+		JPanel fieldsPanel = new JPanel(new GridLayout(0, 1));
+		fieldsPanel.add(newFieldPanel(Translation.get("gb.teamName"), teamnameField));
+
+		final Insets _insets = new Insets(5, 5, 5, 5);
+		repositoryPalette = new JPalette<String>();
+		userPalette = new JPalette<String>();
+		
+		JPanel fieldsPanelTop = new JPanel(new BorderLayout());
+		fieldsPanelTop.add(fieldsPanel, BorderLayout.NORTH);
+		
+		JPanel repositoriesPanel = new JPanel(new BorderLayout()) {
+
+			private static final long serialVersionUID = 1L;
+
+			public Insets getInsets() {
+				return _insets;
+			}
+		};
+		repositoriesPanel.add(repositoryPalette, BorderLayout.CENTER);
+
+		JPanel usersPanel = new JPanel(new BorderLayout()) {
+
+			private static final long serialVersionUID = 1L;
+
+			public Insets getInsets() {
+				return _insets;
+			}
+		};
+		usersPanel.add(userPalette, BorderLayout.CENTER);
+
+		JTabbedPane panel = new JTabbedPane(JTabbedPane.TOP);
+		panel.addTab(Translation.get("gb.general"), fieldsPanelTop);
+		panel.addTab(Translation.get("gb.teamMembers"), usersPanel);
+		panel.addTab(Translation.get("gb.restrictedRepositories"), repositoriesPanel);
+
+
+		JButton createButton = new JButton(Translation.get("gb.save"));
+		createButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				if (validateFields()) {
+					canceled = false;
+					setVisible(false);
+				}
+			}
+		});
+
+		JButton cancelButton = new JButton(Translation.get("gb.cancel"));
+		cancelButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				canceled = true;
+				setVisible(false);
+			}
+		});
+
+		JPanel controls = new JPanel();
+		controls.add(cancelButton);
+		controls.add(createButton);
+		
+		JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public Insets getInsets() {
+				return _insets;
+			}
+		};
+		centerPanel.add(panel, BorderLayout.CENTER);
+		centerPanel.add(controls, BorderLayout.SOUTH);
+
+		getContentPane().setLayout(new BorderLayout(5, 5));
+		getContentPane().add(centerPanel, BorderLayout.CENTER);
+		pack();
+	}
+
+	private JPanel newFieldPanel(String label, JComponent comp) {
+		JLabel fieldLabel = new JLabel(label);
+		fieldLabel.setFont(fieldLabel.getFont().deriveFont(Font.BOLD));
+		fieldLabel.setPreferredSize(new Dimension(150, 20));
+		JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
+		panel.add(fieldLabel);
+		panel.add(comp);
+		return panel;
+	}
+
+	private boolean validateFields() {
+		String tname = teamnameField.getText();
+		if (StringUtils.isEmpty(tname)) {
+			error("Please enter a team name!");
+			return false;
+		}
+
+		boolean rename = false;
+		// verify teamname uniqueness on create
+		if (isCreate) {
+			if (teamnames.contains(tname.toLowerCase())) {
+				error(MessageFormat.format("Team name ''{0}'' is unavailable.", tname));
+				return false;
+			}
+		} else {
+			// check rename collision
+			rename = !StringUtils.isEmpty(teamname) && !teamname.equalsIgnoreCase(tname);
+			if (rename) {
+				if (teamnames.contains(tname.toLowerCase())) {
+					error(MessageFormat.format(
+							"Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
+							tname));
+					return false;
+				}
+			}
+		}
+		team.name = tname;
+
+		team.repositories.clear();
+		team.repositories.addAll(repositoryPalette.getSelections());
+		
+		team.users.clear();
+		team.users.addAll(userPalette.getSelections());
+		return true;
+	}
+
+	private void error(String message) {
+		JOptionPane.showMessageDialog(EditTeamDialog.this, message, Translation.get("gb.error"),
+				JOptionPane.ERROR_MESSAGE);
+	}
+
+	public void setTeams(List<TeamModel> teams) {
+		teamnames.clear();
+		for (TeamModel team : teams) {
+			teamnames.add(team.name.toLowerCase());
+		}
+	}
+
+	public void setRepositories(List<RepositoryModel> repositories, List<String> selected) {
+		List<String> restricted = new ArrayList<String>();
+		for (RepositoryModel repo : repositories) {
+			if (repo.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
+				restricted.add(repo.name);
+			}
+		}
+		StringUtils.sortRepositorynames(restricted);
+		if (selected != null) {
+			StringUtils.sortRepositorynames(selected);
+		}
+		repositoryPalette.setObjects(restricted, selected);
+	}
+	
+	public void setUsers(List<String> users, List<String> selected) {
+		Collections.sort(users);
+		if (selected != null) {
+			Collections.sort(selected);
+		}
+		userPalette.setObjects(users, selected);
+	}
+
+	public TeamModel getTeam() {
+		if (canceled) {
+			return null;
+		}
+		return team;
+	}
+}
diff --git a/src/com/gitblit/client/EditUserDialog.java b/src/com/gitblit/client/EditUserDialog.java
index 246a077..3f1b929 100644
--- a/src/com/gitblit/client/EditUserDialog.java
+++ b/src/com/gitblit/client/EditUserDialog.java
@@ -26,6 +26,7 @@
 import java.awt.event.KeyEvent;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -40,6 +41,7 @@
 import javax.swing.JPanel;
 import javax.swing.JPasswordField;
 import javax.swing.JRootPane;
+import javax.swing.JTabbedPane;
 import javax.swing.JTextField;
 import javax.swing.KeyStroke;
 
@@ -47,6 +49,7 @@
 import com.gitblit.Keys;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.ServerSettings;
+import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.StringUtils;
 
@@ -75,23 +78,25 @@
 	private JCheckBox notFederatedCheckbox;
 
 	private JPalette<String> repositoryPalette;
+	
+	private JPalette<TeamModel> teamsPalette;
 
 	private Set<String> usernames;
 
-	public EditUserDialog(ServerSettings settings) {
-		this(new UserModel(""), settings);
+	public EditUserDialog(int protocolVersion, ServerSettings settings) {
+		this(protocolVersion, new UserModel(""), settings);
 		this.isCreate = true;
 		setTitle(Translation.get("gb.newUser"));
 	}
 
-	public EditUserDialog(UserModel anUser, ServerSettings settings) {
+	public EditUserDialog(int protocolVersion, UserModel anUser, ServerSettings settings) {
 		super();
 		this.username = anUser.username;
 		this.user = new UserModel("");
 		this.settings = settings;
 		this.usernames = new HashSet<String>();
 		this.isCreate = false;
-		initialize(anUser);
+		initialize(protocolVersion, anUser);
 		setModal(true);
 		setTitle(Translation.get("gb.edit") + ": " + anUser.username);
 		setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
@@ -109,7 +114,7 @@
 		return rootPane;
 	}
 
-	private void initialize(UserModel anUser) {
+	private void initialize(int protocolVersion, UserModel anUser) {
 		usernameField = new JTextField(anUser.username == null ? "" : anUser.username, 25);
 		passwordField = new JPasswordField(anUser.password == null ? "" : anUser.password, 25);
 		confirmPasswordField = new JPasswordField(anUser.password == null ? "" : anUser.password,
@@ -127,11 +132,40 @@
 		fieldsPanel.add(newFieldPanel(Translation.get("gb.excludeFromFederation"),
 				notFederatedCheckbox));
 
+		final Insets _insets = new Insets(5, 5, 5, 5);
 		repositoryPalette = new JPalette<String>();
-		JPanel panel = new JPanel(new BorderLayout());
-		panel.add(fieldsPanel, BorderLayout.NORTH);
-		panel.add(newFieldPanel(Translation.get("gb.restrictedRepositories"), repositoryPalette),
-				BorderLayout.CENTER);
+		teamsPalette = new JPalette<TeamModel>();
+		
+		JPanel fieldsPanelTop = new JPanel(new BorderLayout());
+		fieldsPanelTop.add(fieldsPanel, BorderLayout.NORTH);
+		
+		JPanel repositoriesPanel = new JPanel(new BorderLayout()) {
+
+			private static final long serialVersionUID = 1L;
+
+			public Insets getInsets() {
+				return _insets;
+			}
+		};
+		repositoriesPanel.add(repositoryPalette, BorderLayout.CENTER);
+
+		JPanel teamsPanel = new JPanel(new BorderLayout()) {
+
+			private static final long serialVersionUID = 1L;
+
+			public Insets getInsets() {
+				return _insets;
+			}
+		};
+		teamsPanel.add(teamsPalette, BorderLayout.CENTER);
+
+		JTabbedPane panel = new JTabbedPane(JTabbedPane.TOP);
+		panel.addTab(Translation.get("gb.general"), fieldsPanelTop);
+		if (protocolVersion > 1) {
+			panel.addTab(Translation.get("gb.teamMemberships"), teamsPanel);
+		}
+		panel.addTab(Translation.get("gb.restrictedRepositories"), repositoriesPanel);
+
 
 		JButton createButton = new JButton(Translation.get("gb.save"));
 		createButton.addActionListener(new ActionListener() {
@@ -154,8 +188,7 @@
 		JPanel controls = new JPanel();
 		controls.add(cancelButton);
 		controls.add(createButton);
-
-		final Insets _insets = new Insets(5, 5, 5, 5);
+		
 		JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
 
 			private static final long serialVersionUID = 1L;
@@ -259,6 +292,9 @@
 
 		user.repositories.clear();
 		user.repositories.addAll(repositoryPalette.getSelections());
+		
+		user.teams.clear();
+		user.teams.addAll(teamsPalette.getSelections());
 		return true;
 	}
 
@@ -287,6 +323,14 @@
 		}
 		repositoryPalette.setObjects(restricted, selected);
 	}
+	
+	public void setTeams(List<TeamModel> teams, List<TeamModel> selected) {
+		Collections.sort(teams);
+		if (selected != null) {
+			Collections.sort(selected);
+		}
+		teamsPalette.setObjects(teams, selected);
+	}
 
 	public UserModel getUser() {
 		if (canceled) {
diff --git a/src/com/gitblit/client/GitblitClient.java b/src/com/gitblit/client/GitblitClient.java
index c027537..b944486 100644
--- a/src/com/gitblit/client/GitblitClient.java
+++ b/src/com/gitblit/client/GitblitClient.java
@@ -37,6 +37,7 @@
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.ServerSettings;
 import com.gitblit.models.ServerStatus;
+import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.RpcUtils;
 import com.gitblit.utils.StringUtils;
@@ -63,6 +64,8 @@
 
 	private final char[] password;
 
+	private volatile int protocolVersion;
+
 	private volatile boolean allowManagement;
 
 	private volatile boolean allowAdministration;
@@ -72,6 +75,8 @@
 	private final List<RepositoryModel> allRepositories;
 
 	private final List<UserModel> allUsers;
+
+	private final List<TeamModel> allTeams;
 
 	private final List<FederationModel> federationRegistrations;
 
@@ -90,6 +95,7 @@
 		this.password = reg.password;
 
 		this.allUsers = new ArrayList<UserModel>();
+		this.allTeams = new ArrayList<TeamModel>();
 		this.allRepositories = new ArrayList<RepositoryModel>();
 		this.federationRegistrations = new ArrayList<FederationModel>();
 		this.availableFeeds = new ArrayList<FeedModel>();
@@ -98,6 +104,7 @@
 	}
 
 	public void login() throws IOException {
+		protocolVersion = RpcUtils.getProtocolVersion(url, account, password);
 		refreshSettings();
 		refreshAvailableFeeds();
 		refreshRepositories();
@@ -107,6 +114,9 @@
 			// credentials may not have administrator access
 			// or server may have disabled rpc management
 			refreshUsers();
+			if (protocolVersion > 1) {
+				refreshTeams();
+			}
 			allowManagement = true;
 		} catch (UnauthorizedException e) {
 		} catch (ForbiddenException e) {
@@ -128,6 +138,10 @@
 		} catch (IOException e) {
 			e.printStackTrace();
 		}
+	}
+
+	public int getProtocolVersion() {
+		return protocolVersion;
 	}
 
 	public boolean allowManagement() {
@@ -198,6 +212,13 @@
 		return allUsers;
 	}
 
+	public List<TeamModel> refreshTeams() throws IOException {
+		List<TeamModel> teams = RpcUtils.getTeams(url, account, password);
+		allTeams.clear();
+		allTeams.addAll(teams);
+		return allTeams;
+	}
+
 	public ServerSettings refreshSettings() throws IOException {
 		settings = RpcUtils.getSettings(url, account, password);
 		return settings;
@@ -253,8 +274,8 @@
 			for (FeedModel feed : reg.feeds) {
 				feed.lastRefreshDate = feed.currentRefreshDate;
 				feed.currentRefreshDate = new Date();
-				List<FeedEntryModel> entries = SyndicationUtils.readFeed(url,
-						feed.repository, feed.branch, -1, page, account, password);
+				List<FeedEntryModel> entries = SyndicationUtils.readFeed(url, feed.repository,
+						feed.branch, -1, page, account, password);
 				allEntries.addAll(entries);
 			}
 		}
@@ -301,8 +322,8 @@
 		return syndicatedEntries;
 	}
 
-	public List<FeedEntryModel> log(String repository, String branch, int numberOfEntries,
-			int page) throws IOException {
+	public List<FeedEntryModel> log(String repository, String branch, int numberOfEntries, int page)
+			throws IOException {
 		return SyndicationUtils.readFeed(url, repository, branch, numberOfEntries, page, account,
 				password);
 	}
@@ -343,6 +364,29 @@
 		return usernames;
 	}
 
+	public List<TeamModel> getTeams() {
+		return allTeams;
+	}
+
+	public List<String> getTeamnames() {
+		List<String> teamnames = new ArrayList<String>();
+		for (TeamModel team : this.allTeams) {
+			teamnames.add(team.name);
+		}
+		Collections.sort(teamnames);
+		return teamnames;
+	}
+
+	public List<String> getPermittedTeamnames(RepositoryModel repository) {
+		List<String> teamnames = new ArrayList<String>();
+		for (TeamModel team : this.allTeams) {
+			if (team.repositories.contains(repository.name)) {
+				teamnames.add(team.name);
+			}
+		}
+		return teamnames;
+	}
+
 	public List<String> getFederationSets() {
 		return settings.get(Keys.federation.sets).getStrings();
 	}
@@ -353,11 +397,21 @@
 
 	public boolean createRepository(RepositoryModel repository, List<String> permittedUsers)
 			throws IOException {
+		return createRepository(repository, permittedUsers, null);
+	}
+
+	public boolean createRepository(RepositoryModel repository, List<String> permittedUsers,
+			List<String> permittedTeams) throws IOException {
 		boolean success = true;
 		success &= RpcUtils.createRepository(repository, url, account, password);
-		if (permittedUsers.size() > 0) {
+		if (permittedUsers != null && permittedUsers.size() > 0) {
 			// if new repository has named members, set them
 			success &= RpcUtils.setRepositoryMembers(repository, permittedUsers, url, account,
+					password);
+		}
+		if (permittedTeams != null && permittedTeams.size() > 0) {
+			// if new repository has named teams, set them
+			success &= RpcUtils.setRepositoryTeams(repository, permittedTeams, url, account,
 					password);
 		}
 		return success;
@@ -365,11 +419,22 @@
 
 	public boolean updateRepository(String name, RepositoryModel repository,
 			List<String> permittedUsers) throws IOException {
+		return updateRepository(name, repository, permittedUsers, null);
+	}
+
+	public boolean updateRepository(String name, RepositoryModel repository,
+			List<String> permittedUsers, List<String> permittedTeams) throws IOException {
 		boolean success = true;
 		success &= RpcUtils.updateRepository(name, repository, url, account, password);
-		// always set the repository members
-		success &= RpcUtils
-				.setRepositoryMembers(repository, permittedUsers, url, account, password);
+		// set the repository members
+		if (permittedUsers != null) {
+			success &= RpcUtils.setRepositoryMembers(repository, permittedUsers, url, account,
+					password);
+		}
+		if (permittedTeams != null) {
+			success &= RpcUtils.setRepositoryTeams(repository, permittedTeams, url, account,
+					password);
+		}
 		return success;
 	}
 
@@ -389,6 +454,18 @@
 		return RpcUtils.deleteUser(user, url, account, password);
 	}
 
+	public boolean createTeam(TeamModel team) throws IOException {
+		return RpcUtils.createTeam(team, url, account, password);
+	}
+
+	public boolean updateTeam(String name, TeamModel team) throws IOException {
+		return RpcUtils.updateTeam(name, team, url, account, password);
+	}
+
+	public boolean deleteTeam(TeamModel team) throws IOException {
+		return RpcUtils.deleteTeam(team, url, account, password);
+	}
+
 	public boolean updateSettings(Map<String, String> newSettings) throws IOException {
 		return RpcUtils.updateSettings(newSettings, url, account, password);
 	}
diff --git a/src/com/gitblit/client/GitblitPanel.java b/src/com/gitblit/client/GitblitPanel.java
index 0e670ae..f14ce79 100644
--- a/src/com/gitblit/client/GitblitPanel.java
+++ b/src/com/gitblit/client/GitblitPanel.java
@@ -50,6 +50,8 @@
 	private FeedsPanel feedsPanel;
 
 	private UsersPanel usersPanel;
+	
+	private TeamsPanel teamsPanel;
 
 	private SettingsPanel settingsPanel;
 
@@ -62,6 +64,7 @@
 		tabs = new JTabbedPane(JTabbedPane.BOTTOM);
 		tabs.addTab(Translation.get("gb.repositories"), createRepositoriesPanel());
 		tabs.addTab(Translation.get("gb.activity"), createFeedsPanel());
+		tabs.addTab(Translation.get("gb.teams"), createTeamsPanel());
 		tabs.addTab(Translation.get("gb.users"), createUsersPanel());
 		tabs.addTab(Translation.get("gb.settings"), createSettingsPanel());
 		tabs.addTab(Translation.get("gb.status"), createStatusPanel());
@@ -89,6 +92,11 @@
 			protected void updateUsersTable() {
 				usersPanel.updateTable(false);
 			}
+			
+			@Override
+			protected void updateTeamsTable() {
+				teamsPanel.updateTable(false);
+			}
 
 		};
 		return repositoriesPanel;
@@ -107,9 +115,30 @@
 	}
 
 	private JPanel createUsersPanel() {
-		usersPanel = new UsersPanel(gitblit);
+		usersPanel = new UsersPanel(gitblit) {
+			
+			private static final long serialVersionUID = 1L;
+			
+			@Override
+			protected void updateTeamsTable() {
+				teamsPanel.updateTable(false);
+			}
+		};
 		return usersPanel;
 	}
+	
+	private JPanel createTeamsPanel() {
+		teamsPanel = new TeamsPanel(gitblit) {
+			
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void updateUsersTable() {
+				usersPanel.updateTable(false);
+			}
+		};
+		return teamsPanel;
+	}	
 
 	private JPanel createSettingsPanel() {
 		settingsPanel = new SettingsPanel(gitblit);
@@ -128,6 +157,19 @@
 		feedsPanel.updateTable(true);
 
 		if (gitblit.allowManagement()) {
+			if (gitblit.getProtocolVersion() >= 2) {
+				// refresh teams panel
+				teamsPanel.updateTable(false);
+			} else {
+				// remove teams panel
+				String teams = Translation.get("gb.teams");
+				for (int i = 0; i < tabs.getTabCount(); i++) {
+					if (teams.equals(tabs.getTitleAt(i))) {
+						tabs.removeTabAt(i);
+						break;
+					}
+				}
+			}
 			usersPanel.updateTable(false);
 		} else {
 			// user does not have administrator privileges
diff --git a/src/com/gitblit/client/RepositoriesPanel.java b/src/com/gitblit/client/RepositoriesPanel.java
index 70ff6cf..cd3f46b 100644
--- a/src/com/gitblit/client/RepositoriesPanel.java
+++ b/src/com/gitblit/client/RepositoriesPanel.java
@@ -277,6 +277,8 @@
 	protected abstract void subscribeFeeds(List<FeedModel> feeds);
 
 	protected abstract void updateUsersTable();
+	
+	protected abstract void updateTeamsTable();
 
 	protected void disableManagement() {
 		createRepository.setVisible(false);
@@ -349,14 +351,16 @@
 	 * 
 	 */
 	protected void createRepository() {
-		EditRepositoryDialog dialog = new EditRepositoryDialog();
+		EditRepositoryDialog dialog = new EditRepositoryDialog(gitblit.getProtocolVersion());
 		dialog.setLocationRelativeTo(RepositoriesPanel.this);
 		dialog.setUsers(null, gitblit.getUsernames(), null);
+		dialog.setTeams(gitblit.getTeamnames(), null);
 		dialog.setRepositories(gitblit.getRepositories());
 		dialog.setFederationSets(gitblit.getFederationSets(), null);
 		dialog.setVisible(true);
 		final RepositoryModel newRepository = dialog.getRepository();
 		final List<String> permittedUsers = dialog.getPermittedUsers();
+		final List<String> permittedTeams = dialog.getPermittedTeams();
 		if (newRepository == null) {
 			return;
 		}
@@ -365,11 +369,14 @@
 
 			@Override
 			protected Boolean doRequest() throws IOException {
-				boolean success = gitblit.createRepository(newRepository, permittedUsers);
+				boolean success = gitblit.createRepository(newRepository, permittedUsers, permittedTeams);
 				if (success) {
 					gitblit.refreshRepositories();
 					if (permittedUsers.size() > 0) {
 						gitblit.refreshUsers();
+					}
+					if (permittedTeams.size() > 0) {
+						gitblit.refreshTeams();
 					}
 				}
 				return success;
@@ -379,6 +386,7 @@
 			protected void onSuccess() {
 				updateTable(false);
 				updateUsersTable();
+				updateTeamsTable();
 			}
 
 			@Override
@@ -397,16 +405,18 @@
 	 * @param repository
 	 */
 	protected void editRepository(final RepositoryModel repository) {
-		EditRepositoryDialog dialog = new EditRepositoryDialog(repository);
+		EditRepositoryDialog dialog = new EditRepositoryDialog(gitblit.getProtocolVersion(), repository);
 		dialog.setLocationRelativeTo(RepositoriesPanel.this);
 		List<String> usernames = gitblit.getUsernames();
 		List<String> members = gitblit.getPermittedUsernames(repository);
 		dialog.setUsers(repository.owner, usernames, members);
+		dialog.setTeams(gitblit.getTeamnames(), gitblit.getPermittedTeamnames(repository));
 		dialog.setRepositories(gitblit.getRepositories());
 		dialog.setFederationSets(gitblit.getFederationSets(), repository.federationSets);
 		dialog.setVisible(true);
 		final RepositoryModel revisedRepository = dialog.getRepository();
 		final List<String> permittedUsers = dialog.getPermittedUsers();
+		final List<String> permittedTeams = dialog.getPermittedTeams();
 		if (revisedRepository == null) {
 			return;
 		}
@@ -416,10 +426,11 @@
 			@Override
 			protected Boolean doRequest() throws IOException {
 				boolean success = gitblit.updateRepository(repository.name, revisedRepository,
-						permittedUsers);
+						permittedUsers, permittedTeams);
 				if (success) {
 					gitblit.refreshRepositories();
 					gitblit.refreshUsers();
+					gitblit.refreshTeams();
 				}
 				return success;
 			}
@@ -428,6 +439,7 @@
 			protected void onSuccess() {
 				updateTable(false);
 				updateUsersTable();
+				updateTeamsTable();
 			}
 
 			@Override
@@ -460,6 +472,7 @@
 					if (success) {
 						gitblit.refreshRepositories();
 						gitblit.refreshUsers();
+						gitblit.refreshTeams();
 					}
 					return success;
 				}
@@ -468,6 +481,7 @@
 				protected void onSuccess() {
 					updateTable(false);
 					updateUsersTable();
+					updateTeamsTable();
 				}
 
 				@Override
diff --git a/src/com/gitblit/client/TeamsPanel.java b/src/com/gitblit/client/TeamsPanel.java
new file mode 100644
index 0000000..2d1d914
--- /dev/null
+++ b/src/com/gitblit/client/TeamsPanel.java
@@ -0,0 +1,378 @@
+/*
+ * 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.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.TeamModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Users panel displays a list of user accounts and allows management of those
+ * accounts.
+ * 
+ * @author James Moger
+ * 
+ */
+public abstract class TeamsPanel extends JPanel {
+
+	private static final long serialVersionUID = 1L;
+
+	private final GitblitClient gitblit;
+
+	private HeaderPanel header;
+
+	private JTable table;
+
+	private TeamsTableModel tableModel;
+
+	private TableRowSorter<TeamsTableModel> defaultSorter;
+
+	private JTextField filterTextfield;
+
+	public TeamsPanel(GitblitClient gitblit) {
+		super();
+		this.gitblit = gitblit;
+		initialize();
+	}
+
+	private void initialize() {
+		JButton refreshTeams = new JButton(Translation.get("gb.refresh"));
+		refreshTeams.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				refreshTeams();
+			}
+		});
+
+		JButton createTeam = new JButton(Translation.get("gb.create"));
+		createTeam.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				createTeam();
+			}
+		});
+
+		final JButton editTeam = new JButton(Translation.get("gb.edit"));
+		editTeam.setEnabled(false);
+		editTeam.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				editTeam(getSelectedTeams().get(0));
+			}
+		});
+
+		final JButton delTeam = new JButton(Translation.get("gb.delete"));
+		delTeam.setEnabled(false);
+		delTeam.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				deleteTeams(getSelectedTeams());
+			}
+		});
+
+		NameRenderer nameRenderer = new NameRenderer();
+		tableModel = new TeamsTableModel();
+		defaultSorter = new TableRowSorter<TeamsTableModel>(tableModel);
+		table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+		String name = table.getColumnName(TeamsTableModel.Columns.Name.ordinal());
+		table.setRowHeight(nameRenderer.getFont().getSize() + 8);
+		table.getColumn(name).setCellRenderer(nameRenderer);
+
+		int w = 125;
+		name = table.getColumnName(TeamsTableModel.Columns.Members.ordinal());
+		table.getColumn(name).setMinWidth(w);
+		table.getColumn(name).setMaxWidth(w);
+		name = table.getColumnName(TeamsTableModel.Columns.Repositories.ordinal());
+		table.getColumn(name).setMinWidth(w);
+		table.getColumn(name).setMaxWidth(w);
+
+		table.setRowSorter(defaultSorter);
+		table.getRowSorter().toggleSortOrder(TeamsTableModel.Columns.Name.ordinal());
+		table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+			@Override
+			public void valueChanged(ListSelectionEvent e) {
+				if (e.getValueIsAdjusting()) {
+					return;
+				}
+				boolean selected = table.getSelectedRow() > -1;
+				boolean singleSelection = table.getSelectedRows().length == 1;
+				editTeam.setEnabled(singleSelection && selected);
+				delTeam.setEnabled(selected);
+			}
+		});
+
+		table.addMouseListener(new MouseAdapter() {
+			public void mouseClicked(MouseEvent e) {
+				if (e.getClickCount() == 2) {
+					editTeam(getSelectedTeams().get(0));
+				}
+			}
+		});
+
+		filterTextfield = new JTextField();
+		filterTextfield.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				filterTeams(filterTextfield.getText());
+			}
+		});
+		filterTextfield.addKeyListener(new KeyAdapter() {
+			public void keyReleased(KeyEvent e) {
+				filterTeams(filterTextfield.getText());
+			}
+		});
+
+		JPanel teamFilterPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		teamFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+		teamFilterPanel.add(filterTextfield, BorderLayout.CENTER);
+
+		JPanel teamTablePanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		teamTablePanel.add(teamFilterPanel, BorderLayout.NORTH);
+		teamTablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
+
+		JPanel teamControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+		teamControls.add(refreshTeams);
+		teamControls.add(createTeam);
+		teamControls.add(editTeam);
+		teamControls.add(delTeam);
+
+		setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		header = new HeaderPanel(Translation.get("gb.teams"), "users_16x16.png");
+		add(header, BorderLayout.NORTH);
+		add(teamTablePanel, BorderLayout.CENTER);
+		add(teamControls, BorderLayout.SOUTH);
+	}
+
+	@Override
+	public void requestFocus() {
+		filterTextfield.requestFocus();
+	}
+
+	@Override
+	public Insets getInsets() {
+		return Utils.INSETS;
+	}
+
+	protected abstract void updateUsersTable();
+
+	protected void updateTable(boolean pack) {
+		tableModel.list.clear();
+		tableModel.list.addAll(gitblit.getTeams());
+		tableModel.fireTableDataChanged();
+		header.setText(Translation.get("gb.teams") + " (" + gitblit.getTeams().size() + ")");
+		if (pack) {
+			Utils.packColumns(table, Utils.MARGIN);
+		}
+	}
+
+	private void filterTeams(final String fragment) {
+		if (StringUtils.isEmpty(fragment)) {
+			table.setRowSorter(defaultSorter);
+			return;
+		}
+		RowFilter<TeamsTableModel, Object> containsFilter = new RowFilter<TeamsTableModel, Object>() {
+			public boolean include(Entry<? extends TeamsTableModel, ? extends Object> entry) {
+				for (int i = entry.getValueCount() - 1; i >= 0; i--) {
+					if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
+						return true;
+					}
+				}
+				return false;
+			}
+		};
+		TableRowSorter<TeamsTableModel> sorter = new TableRowSorter<TeamsTableModel>(tableModel);
+		sorter.setRowFilter(containsFilter);
+		table.setRowSorter(sorter);
+	}
+
+	private List<TeamModel> getSelectedTeams() {
+		List<TeamModel> teams = new ArrayList<TeamModel>();
+		for (int viewRow : table.getSelectedRows()) {
+			int modelRow = table.convertRowIndexToModel(viewRow);
+			TeamModel model = tableModel.list.get(modelRow);
+			teams.add(model);
+		}
+		return teams;
+	}
+
+	protected void refreshTeams() {
+		GitblitWorker worker = new GitblitWorker(TeamsPanel.this, RpcRequest.LIST_TEAMS) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				gitblit.refreshTeams();
+				return true;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+		};
+		worker.execute();
+	}
+
+	/**
+	 * Displays the create team dialog and fires a SwingWorker to update the
+	 * server, if appropriate.
+	 * 
+	 */
+	protected void createTeam() {
+		EditTeamDialog dialog = new EditTeamDialog(gitblit.getProtocolVersion(),
+				gitblit.getSettings());
+		dialog.setLocationRelativeTo(TeamsPanel.this);
+		dialog.setTeams(gitblit.getTeams());
+		dialog.setRepositories(gitblit.getRepositories(), null);
+		dialog.setUsers(gitblit.getUsernames(), null);
+		dialog.setVisible(true);
+		final TeamModel newTeam = dialog.getTeam();
+		if (newTeam == null) {
+			return;
+		}
+
+		GitblitWorker worker = new GitblitWorker(this, RpcRequest.CREATE_TEAM) {
+
+			@Override
+			protected Boolean doRequest() throws IOException {
+				boolean success = gitblit.createTeam(newTeam);
+				if (success) {
+					gitblit.refreshTeams();
+					gitblit.refreshUsers();
+				}
+				return success;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+				updateUsersTable();
+			}
+
+			@Override
+			protected void onFailure() {
+				showFailure("Failed to execute request \"{0}\" for team \"{1}\".",
+						getRequestType(), newTeam.name);
+			}
+		};
+		worker.execute();
+	}
+
+	/**
+	 * Displays the edit team dialog and fires a SwingWorker to update the
+	 * server, if appropriate.
+	 * 
+	 * @param user
+	 */
+	protected void editTeam(final TeamModel team) {
+		EditTeamDialog dialog = new EditTeamDialog(gitblit.getProtocolVersion(), team,
+				gitblit.getSettings());
+		dialog.setLocationRelativeTo(TeamsPanel.this);
+		dialog.setTeams(gitblit.getTeams());
+		dialog.setRepositories(gitblit.getRepositories(), new ArrayList<String>(team.repositories));
+		dialog.setUsers(gitblit.getUsernames(), team.users == null ? null : new ArrayList<String>(
+				team.users));
+		dialog.setVisible(true);
+		final TeamModel revisedTeam = dialog.getTeam();
+		if (revisedTeam == null) {
+			return;
+		}
+
+		GitblitWorker worker = new GitblitWorker(this, RpcRequest.EDIT_TEAM) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				boolean success = gitblit.updateTeam(team.name, revisedTeam);
+				if (success) {
+					gitblit.refreshTeams();
+					gitblit.refreshUsers();
+				}
+				return success;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+				updateUsersTable();
+			}
+
+			@Override
+			protected void onFailure() {
+				showFailure("Failed to execute request \"{0}\" for team \"{1}\".",
+						getRequestType(), team.name);
+			}
+		};
+		worker.execute();
+	}
+
+	protected void deleteTeams(final List<TeamModel> teams) {
+		if (teams == null || teams.size() == 0) {
+			return;
+		}
+		StringBuilder message = new StringBuilder("Delete the following teams?\n\n");
+		for (TeamModel team : teams) {
+			message.append(team.name).append("\n");
+		}
+		int result = JOptionPane.showConfirmDialog(TeamsPanel.this, message.toString(),
+				"Delete Teams?", JOptionPane.YES_NO_OPTION);
+		if (result == JOptionPane.YES_OPTION) {
+			GitblitWorker worker = new GitblitWorker(this, RpcRequest.DELETE_TEAM) {
+				@Override
+				protected Boolean doRequest() throws IOException {
+					boolean success = true;
+					for (TeamModel team : teams) {
+						success &= gitblit.deleteTeam(team);
+					}
+					if (success) {
+						gitblit.refreshTeams();
+						gitblit.refreshUsers();
+					}
+					return success;
+				}
+
+				@Override
+				protected void onSuccess() {
+					updateTable(false);
+					updateUsersTable();
+				}
+
+				@Override
+				protected void onFailure() {
+					showFailure("Failed to delete specified teams!");
+				}
+			};
+			worker.execute();
+		}
+	}
+}
diff --git a/src/com/gitblit/client/TeamsTableModel.java b/src/com/gitblit/client/TeamsTableModel.java
new file mode 100644
index 0000000..e6d8a94
--- /dev/null
+++ b/src/com/gitblit/client/TeamsTableModel.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.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.models.TeamModel;
+
+/**
+ * Table model of a list of teams.
+ * 
+ * @author James Moger
+ * 
+ */
+public class TeamsTableModel extends AbstractTableModel {
+
+	private static final long serialVersionUID = 1L;
+
+	List<TeamModel> list;
+
+	enum Columns {
+		Name, Members, Repositories;
+
+		@Override
+		public String toString() {
+			return name().replace('_', ' ');
+		}
+	}
+
+	public TeamsTableModel() {
+		this(new ArrayList<TeamModel>());
+	}
+
+	public TeamsTableModel(List<TeamModel> teams) {
+		this.list = teams;
+		Collections.sort(this.list);
+	}
+
+	@Override
+	public int getRowCount() {
+		return list.size();
+	}
+
+	@Override
+	public int getColumnCount() {
+		return Columns.values().length;
+	}
+
+	@Override
+	public String getColumnName(int column) {
+		Columns col = Columns.values()[column];
+		switch (col) {
+		case Name:
+			return Translation.get("gb.name");
+		case Members:
+			return Translation.get("gb.teamMembers");
+		case Repositories:
+			return Translation.get("gb.repositories");
+		}
+		return "";
+	}
+
+	/**
+	 * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+	 * 
+	 * @param columnIndex
+	 *            the column being queried
+	 * @return the Object.class
+	 */
+	public Class<?> getColumnClass(int columnIndex) {
+		return String.class;
+	}
+
+	@Override
+	public Object getValueAt(int rowIndex, int columnIndex) {
+		TeamModel model = list.get(rowIndex);
+		Columns col = Columns.values()[columnIndex];
+		switch (col) {
+		case Name:
+			return model.name;
+		case Members:
+			return model.users.size() == 0 ? "" : String.valueOf(model.users.size());
+		case Repositories:
+			return model.repositories.size() == 0 ? "" : String.valueOf(model.repositories.size());
+		}
+		return null;
+	}
+}
diff --git a/src/com/gitblit/client/UsersPanel.java b/src/com/gitblit/client/UsersPanel.java
index 5d31774..0dfa043 100644
--- a/src/com/gitblit/client/UsersPanel.java
+++ b/src/com/gitblit/client/UsersPanel.java
@@ -41,6 +41,7 @@
 import javax.swing.table.TableRowSorter;
 
 import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.StringUtils;
 
@@ -51,7 +52,7 @@
  * @author James Moger
  * 
  */
-public class UsersPanel extends JPanel {
+public abstract class UsersPanel extends JPanel {
 
 	private static final long serialVersionUID = 1L;
 
@@ -111,6 +112,18 @@
 		String name = table.getColumnName(UsersTableModel.Columns.Name.ordinal());
 		table.setRowHeight(nameRenderer.getFont().getSize() + 8);
 		table.getColumn(name).setCellRenderer(nameRenderer);
+		
+		int w = 125;
+		name = table.getColumnName(UsersTableModel.Columns.AccessLevel.ordinal());
+		table.getColumn(name).setMinWidth(w);
+		table.getColumn(name).setMaxWidth(w);
+		name = table.getColumnName(UsersTableModel.Columns.Teams.ordinal());
+		table.getColumn(name).setMinWidth(w);
+		table.getColumn(name).setMaxWidth(w);
+		name = table.getColumnName(UsersTableModel.Columns.Repositories.ordinal());
+		table.getColumn(name).setMinWidth(w);
+		table.getColumn(name).setMaxWidth(w);
+		
 		table.setRowSorter(defaultSorter);
 		table.getRowSorter().toggleSortOrder(UsersTableModel.Columns.Name.ordinal());
 		table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@@ -167,7 +180,7 @@
 		add(userTablePanel, BorderLayout.CENTER);
 		add(userControls, BorderLayout.SOUTH);
 	}
-	
+
 	@Override
 	public void requestFocus() {
 		filterTextfield.requestFocus();
@@ -178,6 +191,8 @@
 		return Utils.INSETS;
 	}
 
+	protected abstract void updateTeamsTable();
+	
 	protected void updateTable(boolean pack) {
 		tableModel.list.clear();
 		tableModel.list.addAll(gitblit.getUsers());
@@ -240,10 +255,12 @@
 	 * 
 	 */
 	protected void createUser() {
-		EditUserDialog dialog = new EditUserDialog(gitblit.getSettings());
+		EditUserDialog dialog = new EditUserDialog(gitblit.getProtocolVersion(),
+				gitblit.getSettings());
 		dialog.setLocationRelativeTo(UsersPanel.this);
 		dialog.setUsers(gitblit.getUsers());
 		dialog.setRepositories(gitblit.getRepositories(), null);
+		dialog.setTeams(gitblit.getTeams(), null);
 		dialog.setVisible(true);
 		final UserModel newUser = dialog.getUser();
 		if (newUser == null) {
@@ -257,6 +274,9 @@
 				boolean success = gitblit.createUser(newUser);
 				if (success) {
 					gitblit.refreshUsers();
+					if (newUser.teams.size() > 0) {
+						gitblit.refreshTeams();
+					}
 				}
 				return success;
 			}
@@ -264,6 +284,9 @@
 			@Override
 			protected void onSuccess() {
 				updateTable(false);
+				if (newUser.teams.size() > 0) {
+					updateTeamsTable();
+				}
 			}
 
 			@Override
@@ -282,10 +305,13 @@
 	 * @param user
 	 */
 	protected void editUser(final UserModel user) {
-		EditUserDialog dialog = new EditUserDialog(user, gitblit.getSettings());
+		EditUserDialog dialog = new EditUserDialog(gitblit.getProtocolVersion(), user,
+				gitblit.getSettings());
 		dialog.setLocationRelativeTo(UsersPanel.this);
 		dialog.setUsers(gitblit.getUsers());
 		dialog.setRepositories(gitblit.getRepositories(), new ArrayList<String>(user.repositories));
+		dialog.setTeams(gitblit.getTeams(), user.teams == null ? null : new ArrayList<TeamModel>(
+				user.teams));
 		dialog.setVisible(true);
 		final UserModel revisedUser = dialog.getUser();
 		if (revisedUser == null) {
@@ -298,6 +324,7 @@
 				boolean success = gitblit.updateUser(user.username, revisedUser);
 				if (success) {
 					gitblit.refreshUsers();
+					gitblit.refreshTeams();
 				}
 				return success;
 			}
@@ -305,6 +332,7 @@
 			@Override
 			protected void onSuccess() {
 				updateTable(false);
+				updateTeamsTable();
 			}
 
 			@Override
@@ -336,6 +364,7 @@
 					}
 					if (success) {
 						gitblit.refreshUsers();
+						gitblit.refreshTeams();
 					}
 					return success;
 				}
@@ -343,6 +372,7 @@
 				@Override
 				protected void onSuccess() {
 					updateTable(false);
+					updateTeamsTable();
 				}
 
 				@Override
diff --git a/src/com/gitblit/client/UsersTableModel.java b/src/com/gitblit/client/UsersTableModel.java
index de282b8..fa86a13 100644
--- a/src/com/gitblit/client/UsersTableModel.java
+++ b/src/com/gitblit/client/UsersTableModel.java
@@ -36,7 +36,7 @@
 	List<UserModel> list;
 
 	enum Columns {
-		Name, AccessLevel;
+		Name, AccessLevel, Teams, Repositories;
 
 		@Override
 		public String toString() {
@@ -71,6 +71,10 @@
 			return Translation.get("gb.name");
 		case AccessLevel:
 			return Translation.get("gb.accessLevel");
+		case Teams:
+			return Translation.get("gb.teamMemberships");
+		case Repositories:
+			return Translation.get("gb.repositories");
 		}
 		return "";
 	}
@@ -97,6 +101,13 @@
 			if (model.canAdmin) {
 				return "administrator";
 			}
+			return "";
+		case Teams:
+			return (model.teams == null || model.teams.size() == 0) ? "" : String
+					.valueOf(model.teams.size());
+		case Repositories:
+			return (model.repositories == null || model.repositories.size() == 0) ? "" : String
+					.valueOf(model.repositories.size());
 		}
 		return null;
 	}
diff --git a/src/com/gitblit/utils/RpcUtils.java b/src/com/gitblit/utils/RpcUtils.java
index 387433a..02a63a4 100644
--- a/src/com/gitblit/utils/RpcUtils.java
+++ b/src/com/gitblit/utils/RpcUtils.java
@@ -24,6 +24,7 @@
 
 import com.gitblit.Constants;
 import com.gitblit.Constants.RpcRequest;
+import com.gitblit.GitBlitException.UnknownRequestException;
 import com.gitblit.models.FederationModel;
 import com.gitblit.models.FederationProposal;
 import com.gitblit.models.FederationSet;
@@ -31,6 +32,7 @@
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.ServerSettings;
 import com.gitblit.models.ServerStatus;
+import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.google.gson.reflect.TypeToken;
 
@@ -52,6 +54,9 @@
 	}.getType();
 
 	private static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
+	}.getType();
+
+	private static final Type TEAMS_TYPE = new TypeToken<Collection<TeamModel>>() {
 	}.getType();
 
 	private static final Type REGISTRATIONS_TYPE = new TypeToken<Collection<FederationModel>>() {
@@ -96,7 +101,28 @@
 			req = RpcRequest.LIST_REPOSITORIES;
 		}
 		return remoteURL + Constants.RPC_PATH + "?req=" + req.name().toLowerCase()
-				+ (name == null ? "" : ("&name=" + name));
+				+ (name == null ? "" : ("&name=" + StringUtils.encodeURL(name)));
+	}
+
+	/**
+	 * Returns the version of the RPC protocol on the server.
+	 * 
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return the protocol version
+	 * @throws IOException
+	 */
+	public static int getProtocolVersion(String serverUrl, String account, char[] password)
+			throws IOException {
+		String url = asLink(serverUrl, RpcRequest.GET_PROTOCOL);
+		int protocol = 1;
+		try {
+			protocol = JsonUtils.retrieveJson(url, Integer.class, account, password);
+		} catch (UnknownRequestException e) {
+			// v0.7.0 (protocol 1) did not have this request type 
+		}
+		return protocol;
 	}
 
 	/**
@@ -131,6 +157,24 @@
 		String url = asLink(serverUrl, RpcRequest.LIST_USERS);
 		Collection<UserModel> models = JsonUtils.retrieveJson(url, USERS_TYPE, account, password);
 		List<UserModel> list = new ArrayList<UserModel>(models);
+		return list;
+	}
+
+	/**
+	 * Tries to pull the gitblit team definitions from the remote gitblit
+	 * instance.
+	 * 
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return a collection of UserModel objects
+	 * @throws IOException
+	 */
+	public static List<TeamModel> getTeams(String serverUrl, String account, char[] password)
+			throws IOException {
+		String url = asLink(serverUrl, RpcRequest.LIST_TEAMS);
+		Collection<TeamModel> models = JsonUtils.retrieveJson(url, TEAMS_TYPE, account, password);
+		List<TeamModel> list = new ArrayList<TeamModel>(models);
 		return list;
 	}
 
@@ -236,6 +280,53 @@
 	}
 
 	/**
+	 * Create a team on the Gitblit server.
+	 * 
+	 * @param team
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return true if the action succeeded
+	 * @throws IOException
+	 */
+	public static boolean createTeam(TeamModel team, String serverUrl, String account,
+			char[] password) throws IOException {
+		return doAction(RpcRequest.CREATE_TEAM, null, team, serverUrl, account, password);
+
+	}
+
+	/**
+	 * Send a revised version of the team model to the Gitblit server.
+	 * 
+	 * @param team
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return true if the action succeeded
+	 * @throws IOException
+	 */
+	public static boolean updateTeam(String teamname, TeamModel team, String serverUrl,
+			String account, char[] password) throws IOException {
+		return doAction(RpcRequest.EDIT_TEAM, teamname, team, serverUrl, account, password);
+
+	}
+
+	/**
+	 * Deletes a team from the Gitblit server.
+	 * 
+	 * @param team
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return true if the action succeeded
+	 * @throws IOException
+	 */
+	public static boolean deleteTeam(TeamModel team, String serverUrl, String account,
+			char[] password) throws IOException {
+		return doAction(RpcRequest.DELETE_TEAM, null, team, serverUrl, account, password);
+	}
+
+	/**
 	 * Retrieves the list of users that can access the specified repository.
 	 * 
 	 * @param repository
@@ -253,7 +344,7 @@
 	}
 
 	/**
-	 * Sets the repository membership list.
+	 * Sets the repository user membership list.
 	 * 
 	 * @param repository
 	 * @param memberships
@@ -271,6 +362,41 @@
 	}
 
 	/**
+	 * Retrieves the list of teams that can access the specified repository.
+	 * 
+	 * @param repository
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return list of teams
+	 * @throws IOException
+	 */
+	public static List<String> getRepositoryTeams(RepositoryModel repository, String serverUrl,
+			String account, char[] password) throws IOException {
+		String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_TEAMS, repository.name);
+		Collection<String> list = JsonUtils.retrieveJson(url, NAMES_TYPE, account, password);
+		return new ArrayList<String>(list);
+	}
+
+	/**
+	 * Sets the repository team membership list.
+	 * 
+	 * @param repository
+	 * @param teams
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return true if the action succeeded
+	 * @throws IOException
+	 */
+	public static boolean setRepositoryTeams(RepositoryModel repository,
+			List<String> teams, String serverUrl, String account, char[] password)
+			throws IOException {
+		return doAction(RpcRequest.SET_REPOSITORY_TEAMS, repository.name, teams, serverUrl,
+				account, password);
+	}
+
+	/**
 	 * Retrieves the list of federation registrations. These are the list of
 	 * registrations that this Gitblit instance is pulling from.
 	 * 
diff --git a/tests/com/gitblit/tests/RpcTests.java b/tests/com/gitblit/tests/RpcTests.java
index 30dc84d..123dd97 100644
--- a/tests/com/gitblit/tests/RpcTests.java
+++ b/tests/com/gitblit/tests/RpcTests.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -34,12 +35,14 @@
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.GitBlitException.UnauthorizedException;
 import com.gitblit.Keys;
+import com.gitblit.RpcServlet;
 import com.gitblit.models.FederationModel;
 import com.gitblit.models.FederationProposal;
 import com.gitblit.models.FederationSet;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.ServerSettings;
 import com.gitblit.models.ServerStatus;
+import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.RpcUtils;
 
@@ -70,6 +73,12 @@
 	}
 
 	@Test
+	public void testGetProtocolVersion() throws IOException {
+		int protocol = RpcUtils.getProtocolVersion(url, null, null);
+		assertEquals(RpcServlet.PROTOCOL_VERSION, protocol);
+	}
+
+	@Test
 	public void testListRepositories() throws IOException {
 		Map<String, RepositoryModel> map = RpcUtils.getRepositories(url, null, null);
 		assertNotNull("Repository list is null!", map);
@@ -87,6 +96,20 @@
 
 		list = RpcUtils.getUsers(url, "admin", "admin".toCharArray());
 		assertTrue("User list is empty!", list.size() > 0);
+	}
+
+	@Test
+	public void testListTeams() throws IOException {
+		List<TeamModel> list = null;
+		try {
+			list = RpcUtils.getTeams(url, null, null);
+		} catch (UnauthorizedException e) {
+		}
+		assertNull("Server allows anyone to admin!", list);
+
+		list = RpcUtils.getTeams(url, "admin", "admin".toCharArray());
+		assertTrue("Team list is empty!", list.size() > 0);
+		assertEquals("admins", list.get(0).name);
 	}
 
 	@Test
@@ -214,6 +237,61 @@
 	}
 
 	@Test
+	public void testTeamAdministration() throws IOException {
+		List<TeamModel> teams = RpcUtils.getTeams(url, account, password.toCharArray());
+		assertEquals(1, teams.size());
+		
+		// Create the A-Team
+		TeamModel aTeam = new TeamModel("A-Team");
+		aTeam.users.add("admin");
+		aTeam.repositories.add("helloworld.git");
+		assertTrue(RpcUtils.createTeam(aTeam, url, account, password.toCharArray()));
+
+		aTeam = null;
+		teams = RpcUtils.getTeams(url, account, password.toCharArray());
+		assertEquals(2, teams.size());
+		for (TeamModel team : teams) {
+			if (team.name.equals("A-Team")) {
+				aTeam = team;
+				break;
+			}
+		}
+		assertNotNull(aTeam);
+		assertTrue(aTeam.hasUser("admin"));
+		assertTrue(aTeam.hasRepository("helloworld.git"));
+
+		RepositoryModel helloworld = null;
+		Map<String, RepositoryModel> repositories = RpcUtils.getRepositories(url, account,
+				password.toCharArray());
+		for (RepositoryModel repository : repositories.values()) {
+			if (repository.name.equals("helloworld.git")) {
+				helloworld = repository;
+				break;
+			}
+		}
+		assertNotNull(helloworld);
+		
+		// Confirm that we have added the team
+		List<String> helloworldTeams = RpcUtils.getRepositoryTeams(helloworld, url, account,
+				password.toCharArray());
+		assertEquals(1, helloworldTeams.size());
+		assertTrue(helloworldTeams.contains(aTeam.name));
+
+		// set no teams
+		assertTrue(RpcUtils.setRepositoryTeams(helloworld, new ArrayList<String>(), url, account,
+				password.toCharArray()));
+		helloworldTeams = RpcUtils.getRepositoryTeams(helloworld, url, account,
+				password.toCharArray());
+		assertEquals(0, helloworldTeams.size());
+		
+		// delete the A-Team
+		assertTrue(RpcUtils.deleteTeam(aTeam, url, account, password.toCharArray()));
+
+		teams = RpcUtils.getTeams(url, account, password.toCharArray());
+		assertEquals(1, teams.size());
+	}
+
+	@Test
 	public void testFederationRegistrations() throws Exception {
 		List<FederationModel> registrations = RpcUtils.getFederationRegistrations(url, account,
 				password.toCharArray());

--
Gitblit v1.9.1