From 1613f4067028b73e05544b55d45b6e136ce0238d Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 23 Jan 2013 07:58:51 -0500
Subject: [PATCH] Merged multiple owners (pull request #63)

---
 src/com/gitblit/wicket/pages/SummaryPage.html             |    8 +
 src/com/gitblit/wicket/pages/EditRepositoryPage.java      |   20 ++
 src/com/gitblit/client/GitblitClient.java                 |    2 
 src/com/gitblit/client/RepositoriesPanel.java             |    4 
 src/com/gitblit/client/RepositoriesTableModel.java        |    3 
 tests/com/gitblit/tests/PermissionsTest.java              |   51 ++++++++
 src/com/gitblit/wicket/panels/RepositoriesPanel.java      |   28 +++-
 src/com/gitblit/models/UserModel.java                     |    3 
 src/com/gitblit/wicket/pages/RepositoryPage.java          |    2 
 tests/com/gitblit/tests/GitServletTest.java               |    5 
 src/com/gitblit/GitFilter.java                            |    2 
 src/com/gitblit/GitBlit.java                              |   22 ++-
 src/com/gitblit/client/JPalette.java                      |    2 
 src/com/gitblit/wicket/GitBlitWebApp.properties           |    3 
 src/com/gitblit/utils/ArrayUtils.java                     |   30 +++++
 docs/04_releases.mkd                                      |    5 
 tests/com/gitblit/tests/RpcTests.java                     |    2 
 src/com/gitblit/models/RepositoryModel.java               |   47 +++++++
 src/com/gitblit/client/EditRepositoryDialog.java          |   24 +--
 tests/com/gitblit/tests/FederationTests.java              |    2 
 src/com/gitblit/wicket/pages/EditRepositoryPage.html      |    2 
 src/com/gitblit/wicket/pages/SummaryPage.java             |   36 ++++-
 src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java |   20 ++-
 23 files changed, 244 insertions(+), 79 deletions(-)

diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd
index 6f6fd75..678a7e0 100644
--- a/docs/04_releases.mkd
+++ b/docs/04_releases.mkd
@@ -9,8 +9,9 @@
 - Can't set reset settings with $ or { characters through Gitblit Manager because they are not properly escaped
 
 #### additions
-
- - Chinese translation (github/dapengme)
+ 
+ - Implemented multiple repository owners (github/akquinet)
+ - Chinese translation (github/dapengme, github/yin8086)
 
 ### Older Releases
 
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 7f06b35..6bf75d7 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -939,14 +939,14 @@
 			for (RepositoryModel model : getRepositoryModels(user)) {
 				if (model.isUsersPersonalRepository(username)) {
 					// personal repository
-					model.owner = user.username;
+					model.addOwner(user.username);
 					String oldRepositoryName = model.name;
 					model.name = "~" + user.username + model.name.substring(model.projectPath.length());
 					model.projectPath = "~" + user.username;
 					updateRepositoryModel(oldRepositoryName, model, false);
 				} else if (model.isOwner(username)) {
 					// common/shared repo
-					model.owner = user.username;
+					model.addOwner(user.username);
 					updateRepositoryModel(model.name, model, false);
 				}
 			}
@@ -1665,7 +1665,7 @@
 		
 		if (config != null) {
 			model.description = getConfig(config, "description", "");
-			model.owner = getConfig(config, "owner", "");
+			model.addOwners(ArrayUtils.fromString(getConfig(config, "owner", "")));
 			model.useTickets = getConfig(config, "useTickets", false);
 			model.useDocs = getConfig(config, "useDocs", false);
 			model.allowForks = getConfig(config, "allowForks", true);
@@ -2183,7 +2183,7 @@
 	public void updateConfiguration(Repository r, RepositoryModel repository) {
 		StoredConfig config = r.getConfig();
 		config.setString(Constants.CONFIG_GITBLIT, null, "description", repository.description);
-		config.setString(Constants.CONFIG_GITBLIT, null, "owner", repository.owner);
+		config.setString(Constants.CONFIG_GITBLIT, null, "owner", ArrayUtils.toString(repository.owners));
 		config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets);
 		config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs);
 		config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);
@@ -3320,15 +3320,17 @@
 		// create a Gitblit repository model for the clone
 		RepositoryModel cloneModel = repository.cloneAs(cloneName);
 		// owner has REWIND/RW+ permissions
-		cloneModel.owner = user.username;
+		cloneModel.addOwner(user.username);
 		updateRepositoryModel(cloneName, cloneModel, false);
 
 		// add the owner of the source repository to the clone's access list
-		if (!StringUtils.isEmpty(repository.owner)) {
-			UserModel originOwner = getUserModel(repository.owner);
-			if (originOwner != null) {
-				originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
-				updateUserModel(originOwner.username, originOwner, false);
+		if (!ArrayUtils.isEmpty(repository.owners)) {
+			for (String owner : repository.owners) {
+				UserModel originOwner = getUserModel(owner);
+				if (originOwner != null) {
+					originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+					updateUserModel(originOwner.username, originOwner, false);
+				}
 			}
 		}
 
diff --git a/src/com/gitblit/GitFilter.java b/src/com/gitblit/GitFilter.java
index 2b769d4..a0d395b 100644
--- a/src/com/gitblit/GitFilter.java
+++ b/src/com/gitblit/GitFilter.java
@@ -222,7 +222,7 @@
 				// create repository
 				RepositoryModel model = new RepositoryModel();
 				model.name = repository;
-				model.owner = user.username;
+				model.addOwner(user.username);
 				model.projectPath = StringUtils.getFirstPathElement(repository);
 				if (model.isUsersPersonalRepository(user.username)) {
 					// personal repository, default to private for user
diff --git a/src/com/gitblit/client/EditRepositoryDialog.java b/src/com/gitblit/client/EditRepositoryDialog.java
index 6f9ed52..8851de4 100644
--- a/src/com/gitblit/client/EditRepositoryDialog.java
+++ b/src/com/gitblit/client/EditRepositoryDialog.java
@@ -38,7 +38,6 @@
 
 import javax.swing.BoxLayout;
 import javax.swing.ButtonGroup;
-import javax.swing.DefaultComboBoxModel;
 import javax.swing.DefaultListCellRenderer;
 import javax.swing.ImageIcon;
 import javax.swing.JButton;
@@ -117,7 +116,7 @@
 
 	private JComboBox federationStrategy;
 
-	private JComboBox ownerField;
+	private JPalette<String> ownersPalette;
 
 	private JComboBox headRefField;
 	
@@ -126,7 +125,7 @@
 	private JTextField gcThreshold;
 	
 	private JComboBox maxActivityCommits;
-
+	
 	private RegistrantPermissionsPanel usersPalette;
 
 	private JPalette<String> setsPalette;
@@ -207,7 +206,7 @@
 		gcThreshold = new JTextField(8);
 		gcThreshold.setText(anRepository.gcThreshold);
 
-		ownerField = new JComboBox();
+		ownersPalette = new JPalette<String>(true);
 
 		useTickets = new JCheckBox(Translation.get("gb.useTicketsDescription"),
 				anRepository.useTickets);
@@ -334,10 +333,10 @@
 
 		usersPalette = new RegistrantPermissionsPanel(RegistrantType.USER);
 
-		JPanel northFieldsPanel = new JPanel(new GridLayout(0, 1, 0, 5));
-		northFieldsPanel.add(newFieldPanel(Translation.get("gb.owner"), ownerField));
+		JPanel northFieldsPanel = new JPanel(new BorderLayout(0, 5));
+		northFieldsPanel.add(newFieldPanel(Translation.get("gb.owners"), ownersPalette), BorderLayout.NORTH);
 		northFieldsPanel.add(newFieldPanel(Translation.get("gb.accessRestriction"),
-				accessRestriction), BorderLayout.NORTH);
+				accessRestriction), BorderLayout.CENTER);
 
 		JPanel northAccessPanel = new JPanel(new BorderLayout(5, 5));
 		northAccessPanel.add(northFieldsPanel, BorderLayout.NORTH);
@@ -556,8 +555,8 @@
 
 		repository.name = rname;
 		repository.description = descriptionField.getText();
-		repository.owner = ownerField.getSelectedItem() == null ? null
-				: ownerField.getSelectedItem().toString();
+		repository.owners.clear();
+		repository.owners.addAll(ownersPalette.getSelections());
 		repository.HEAD = headRefField.getSelectedItem() == null ? null
 				: headRefField.getSelectedItem().toString();
 		repository.gcPeriod = (Integer) gcPeriod.getSelectedItem();
@@ -629,11 +628,8 @@
 		this.allowNamed.setSelected(!authenticated);
 	}
 
-	public void setUsers(String owner, List<String> all, List<RegistrantAccessPermission> permissions) {
-		ownerField.setModel(new DefaultComboBoxModel(all.toArray()));
-		if (!StringUtils.isEmpty(owner)) {
-			ownerField.setSelectedItem(owner);
-		}
+	public void setUsers(List<String> owners, List<String> all, List<RegistrantAccessPermission> permissions) {
+		ownersPalette.setObjects(all, owners);
 		usersPalette.setObjects(all, permissions);
 	}
 
diff --git a/src/com/gitblit/client/GitblitClient.java b/src/com/gitblit/client/GitblitClient.java
index 1101cd6..cc7d58a 100644
--- a/src/com/gitblit/client/GitblitClient.java
+++ b/src/com/gitblit/client/GitblitClient.java
@@ -162,7 +162,7 @@
 	}
 
 	public boolean isOwner(RepositoryModel model) {
-		return account != null && account.equalsIgnoreCase(model.owner);
+		return model.isOwner(account);
 	}
 
 	public String getURL(String action, String repository, String objectId) {
diff --git a/src/com/gitblit/client/JPalette.java b/src/com/gitblit/client/JPalette.java
index 4ead099..a0c2b25 100644
--- a/src/com/gitblit/client/JPalette.java
+++ b/src/com/gitblit/client/JPalette.java
@@ -144,7 +144,7 @@
 		table.getColumn(table.getColumnName(0)).setCellRenderer(nameRenderer);
 
 		JScrollPane jsp = new JScrollPane(table);
-		jsp.setPreferredSize(new Dimension(225, 175));
+		jsp.setPreferredSize(new Dimension(225, 160));
 		JPanel panel = new JPanel(new BorderLayout());
 		JLabel jlabel = new JLabel(label);
 		jlabel.setFont(jlabel.getFont().deriveFont(Font.BOLD));
diff --git a/src/com/gitblit/client/RepositoriesPanel.java b/src/com/gitblit/client/RepositoriesPanel.java
index 769d33b..64bde9b 100644
--- a/src/com/gitblit/client/RepositoriesPanel.java
+++ b/src/com/gitblit/client/RepositoriesPanel.java
@@ -49,8 +49,8 @@
 import com.gitblit.Constants;
 import com.gitblit.Constants.RpcRequest;
 import com.gitblit.Keys;
-import com.gitblit.models.RegistrantAccessPermission;
 import com.gitblit.models.FeedModel;
+import com.gitblit.models.RegistrantAccessPermission;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.utils.StringUtils;
 
@@ -453,7 +453,7 @@
 		dialog.setLocationRelativeTo(RepositoriesPanel.this);
 		List<String> usernames = gitblit.getUsernames();
 		List<RegistrantAccessPermission> members = gitblit.getUserAccessPermissions(repository);
-		dialog.setUsers(repository.owner, usernames, members);
+		dialog.setUsers(new ArrayList<String>(repository.owners), usernames, members);
 		dialog.setTeams(gitblit.getTeamnames(), gitblit.getTeamAccessPermissions(repository));
 		dialog.setRepositories(gitblit.getRepositories());
 		dialog.setFederationSets(gitblit.getFederationSets(), repository.federationSets);
diff --git a/src/com/gitblit/client/RepositoriesTableModel.java b/src/com/gitblit/client/RepositoriesTableModel.java
index c3eaf6e..6b295a4 100644
--- a/src/com/gitblit/client/RepositoriesTableModel.java
+++ b/src/com/gitblit/client/RepositoriesTableModel.java
@@ -23,6 +23,7 @@
 import javax.swing.table.AbstractTableModel;
 
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.ArrayUtils;
 
 /**
  * Table model of a list of repositories.
@@ -111,7 +112,7 @@
 		case Description:
 			return model.description;
 		case Owner:
-			return model.owner;
+			return ArrayUtils.toString(model.owners);
 		case Indicators:
 			return model;
 		case Last_Change:
diff --git a/src/com/gitblit/models/RepositoryModel.java b/src/com/gitblit/models/RepositoryModel.java
index 022fd20..a2dab3c 100644
--- a/src/com/gitblit/models/RepositoryModel.java
+++ b/src/com/gitblit/models/RepositoryModel.java
@@ -17,6 +17,7 @@
 
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -43,7 +44,7 @@
 	// field names are reflectively mapped in EditRepository page
 	public String name;
 	public String description;
-	public String owner;
+	public List<String> owners;
 	public Date lastChange;
 	public boolean hasCommits;
 	public boolean showRemoteBranches;
@@ -91,13 +92,15 @@
 	public RepositoryModel(String name, String description, String owner, Date lastchange) {
 		this.name = name;
 		this.description = description;
-		this.owner = owner;
 		this.lastChange = lastchange;
 		this.accessRestriction = AccessRestrictionType.NONE;
 		this.authorizationControl = AuthorizationControl.NAMED;
 		this.federationSets = new ArrayList<String>();
 		this.federationStrategy = FederationStrategy.FEDERATE_THIS;	
 		this.projectPath = StringUtils.getFirstPathElement(name);
+		this.owners = new ArrayList<String>();
+		
+		addOwner(owner);
 	}
 	
 	public List<String> getLocalBranches() {
@@ -162,7 +165,10 @@
 	}
 	
 	public boolean isOwner(String username) {
-		return owner != null && username != null && owner.equalsIgnoreCase(username);
+		if (StringUtils.isEmpty(username) || ArrayUtils.isEmpty(owners)) {
+			return false;
+		}
+		return owners.contains(username.toLowerCase());
 	}
 	
 	public boolean isPersonalRepository() {
@@ -201,4 +207,37 @@
 		clone.sparkleshareId = sparkleshareId; 
 		return clone;
 	}
-}
\ No newline at end of file
+
+	public void addOwner(String username) {
+		if (!StringUtils.isEmpty(username)) {
+			String name = username.toLowerCase();
+			// a set would be more efficient, but this complicates JSON
+			// deserialization so we enforce uniqueness with an arraylist
+			if (!owners.contains(name)) {
+				owners.add(name);
+			}
+		}
+	}
+
+	public void removeOwner(String username) {
+		if (!StringUtils.isEmpty(username)) {
+			owners.remove(username.toLowerCase());
+		}
+	}
+
+	public void addOwners(Collection<String> usernames) {
+		if (!ArrayUtils.isEmpty(usernames)) {
+			for (String username : usernames) {
+				addOwner(username);
+			}
+		}
+	}
+
+	public void removeOwners(Collection<String> usernames) {
+		if (!ArrayUtils.isEmpty(owners)) {
+			for (String username : usernames) {
+				removeOwner(username);
+			}
+		}
+	}
+}
diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java
index 54e81cb..bec011d 100644
--- a/src/com/gitblit/models/UserModel.java
+++ b/src/com/gitblit/models/UserModel.java
@@ -108,8 +108,7 @@
 	@Deprecated
 	@Unused
 	public boolean canAccessRepository(RepositoryModel repository) {
-		boolean isOwner = !StringUtils.isEmpty(repository.owner)
-				&& repository.owner.equals(username);
+		boolean isOwner = repository.isOwner(username);
 		boolean allowAuthenticated = isAuthenticated && AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl);
 		return canAdmin() || isOwner || repositories.contains(repository.name.toLowerCase())
 				|| hasTeamAccess(repository.name) || allowAuthenticated;
diff --git a/src/com/gitblit/utils/ArrayUtils.java b/src/com/gitblit/utils/ArrayUtils.java
index 41d110a..6583467 100644
--- a/src/com/gitblit/utils/ArrayUtils.java
+++ b/src/com/gitblit/utils/ArrayUtils.java
@@ -15,7 +15,9 @@
  */
 package com.gitblit.utils;
 
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 
 
 /**
@@ -41,4 +43,32 @@
 	public static boolean isEmpty(Collection<?> collection) {
 		return collection == null || collection.size() == 0;
 	}
+	
+	public static String toString(Collection<?> collection) {
+		if (isEmpty(collection)) {
+			return "";
+		}
+		StringBuilder sb = new StringBuilder();
+		for (Object o : collection) {
+			sb.append(o.toString()).append(", ");
+		}
+		// trim trailing comma-space
+		sb.setLength(sb.length() - 2);
+		return sb.toString();
+	}
+	
+	public static Collection<String> fromString(String value) {
+		if (StringUtils.isEmpty(value)) {
+			value = "";
+		}
+		List<String> list = new ArrayList<String>();
+		String [] values = value.split(",|;");
+		for (String v : values) {
+			String string = v.trim();
+			if (!StringUtils.isEmpty(string)) {
+				list.add(string);
+			}
+		}
+		return list;
+	}
 }
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index dfdf70c..a993f9f 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -441,4 +441,5 @@
 gb.siteName = site name
 gb.siteNameDescription = short, descriptive name of your server 
 gb.excludeFromActivity = exclude from activity page
-gb.isSparkleshared = repository is Sparkleshared
\ No newline at end of file
+gb.isSparkleshared = repository is Sparkleshared
+gb.owners = owners
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/com/gitblit/wicket/pages/EditRepositoryPage.html
index 60893f4..7fc0de2 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.html
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -50,7 +50,7 @@
 		<div class="tab-pane" id="permissions">
 			<table class="plain">
 				<tbody class="settings">
-					<tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select class="span2" wicket:id="owner" tabindex="15" /> &nbsp;<span class="help-inline"><wicket:message key="gb.ownerDescription"></wicket:message></span></td></tr>
+					<tr><th><wicket:message key="gb.owners"></wicket:message></th><td class="edit"><span wicket:id="owners" tabindex="15" /> </td></tr>
 					<tr><th colspan="2"><hr/></th></tr>
 					<tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span4" wicket:id="accessRestriction" tabindex="16" /></td></tr>
 					<tr><th colspan="2"><hr/></th></tr>
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
index a071b69..d68d655 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -94,7 +94,7 @@
 			// personal create permissions, inject personal repository path
 			model.name = user.getPersonalPath() + "/";
 			model.projectPath = user.getPersonalPath();
-			model.owner = user.username;
+			model.addOwner(user.username);
 			// personal repositories are private by default
 			model.accessRestriction = AccessRestrictionType.VIEW;
 			model.authorizationControl = AuthorizationControl.NAMED;
@@ -164,6 +164,12 @@
 		final RegistrantPermissionsPanel teamsPalette = new RegistrantPermissionsPanel("teams", 
 				RegistrantType.TEAM, GitBlit.self().getAllTeamnames(), repositoryTeams, getAccessPermissions());
 
+		// owners palette
+		List<String> owners = new ArrayList<String>(repositoryModel.owners);
+		List<String> persons = GitBlit.self().getAllUsernames();
+		final Palette<String> ownersPalette = new Palette<String>("owners", new ListModel<String>(owners), new CollectionModel<String>(
+		      persons), new StringChoiceRenderer(), 12, true);
+		
 		// indexed local branches palette
 		List<String> allLocalBranches = new ArrayList<String>();
 		allLocalBranches.add(Constants.DEFAULT_BRANCH);
@@ -326,6 +332,13 @@
 					}
 					repositoryModel.indexedBranches = indexedBranches;
 
+					// owners
+					repositoryModel.owners.clear();
+					Iterator<String> owners = ownersPalette.getSelectedChoices();
+					while (owners.hasNext()) {
+						repositoryModel.addOwner(owners.next());
+					}
+					
 					// pre-receive scripts
 					List<String> preReceiveScripts = new ArrayList<String>();
 					Iterator<String> pres = preReceivePalette.getSelectedChoices();
@@ -377,8 +390,7 @@
 		// field names reflective match RepositoryModel fields
 		form.add(new TextField<String>("name").setEnabled(allowEditName));
 		form.add(new TextField<String>("description"));
-		form.add(new DropDownChoice<String>("owner", GitBlit.self().getAllUsernames())
-				.setEnabled(GitBlitWebSession.get().canAdmin() && !repositoryModel.isPersonalRepository()));
+		form.add(ownersPalette);
 		form.add(new CheckBox("allowForks").setEnabled(GitBlit.getBoolean(Keys.web.allowForking, true)));
 		DropDownChoice<AccessRestrictionType> accessRestriction = new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays
 				.asList(AccessRestrictionType.values()), new AccessRestrictionRenderer());
@@ -559,7 +571,7 @@
 						isAdmin = true;
 						return;
 					} else {
-						if (!model.owner.equalsIgnoreCase(user.username)) {
+						if (!model.isOwner(user.username)) {
 							// User is not an Admin nor Owner
 							error(getString("gb.errorOnlyAdminOrOwnerMayEditRepository"), true);
 						}
diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java
index 78ffe80..a477b74 100644
--- a/src/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -184,7 +184,7 @@
 			showAdmin = GitBlit.getBoolean(Keys.web.allowAdministration, false);
 		}
 		isOwner = GitBlitWebSession.get().isLoggedIn()
-				&& (model.owner != null && model.owner.equalsIgnoreCase(GitBlitWebSession.get()
+				&& (model.isOwner(GitBlitWebSession.get()
 						.getUsername()));
 		if (showAdmin || isOwner) {
 			pages.put("edit", new PageRegistration("gb.edit", EditRepositoryPage.class, params));
diff --git a/src/com/gitblit/wicket/pages/SummaryPage.html b/src/com/gitblit/wicket/pages/SummaryPage.html
index 45ffddf..3e85df9 100644
--- a/src/com/gitblit/wicket/pages/SummaryPage.html
+++ b/src/com/gitblit/wicket/pages/SummaryPage.html
@@ -16,7 +16,7 @@
 		<div class="hidden-phone" style="padding-bottom: 10px;"> 
 			<table class="plain">
 				<tr><th><wicket:message key="gb.description">[description]</wicket:message></th><td><span wicket:id="repositoryDescription">[repository description]</span></td></tr>
-				<tr><th><wicket:message key="gb.owner">[owner]</wicket:message></th><td><span wicket:id="repositoryOwner">[repository owner]</span></td></tr>
+				<tr><th><wicket:message key="gb.owners">[owner]</wicket:message></th><td><span wicket:id="repositoryOwners"><span wicket:id="owner"></span><span wicket:id="comma"></span></span></td></tr>
 				<tr><th><wicket:message key="gb.lastChange">[last change]</wicket:message></th><td><span wicket:id="repositoryLastChange">[repository last change]</span></td></tr>
 				<tr><th><wicket:message key="gb.stats">[stats]</wicket:message></th><td><span wicket:id="branchStats">[branch stats]</span> <span class="link"><a wicket:id="metrics"><wicket:message key="gb.metrics">[metrics]</wicket:message></a></span></td></tr>
 				<tr><th style="vertical-align:top;"><wicket:message key="gb.repositoryUrl">[URL]</wicket:message>&nbsp;<img style="vertical-align: top;padding-left:3px;" wicket:id="accessRestrictionIcon" /></th><td><span wicket:id="repositoryCloneUrl">[repository clone url]</span><div wicket:id="otherUrls"></div></td></tr>
@@ -44,7 +44,11 @@
 		<div style="border:1px solid #ddd;border-radius: 0 0 3px 3px;padding: 20px;">
 			<div wicket:id="readmeContent" class="markdown"></div>
 		</div>
-	</wicket:fragment>	
+	</wicket:fragment>
+	
+	<wicket:fragment wicket:id="ownersFragment">
+		
+	</wicket:fragment>
 </wicket:extend>	
 </body>
 </html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/SummaryPage.java b/src/com/gitblit/wicket/pages/SummaryPage.java
index 8df2ceb..bd40a1b 100644
--- a/src/com/gitblit/wicket/pages/SummaryPage.java
+++ b/src/com/gitblit/wicket/pages/SummaryPage.java
@@ -27,6 +27,9 @@
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.wicketstuff.googlecharts.Chart;
@@ -82,18 +85,29 @@
 
 		// repository description
 		add(new Label("repositoryDescription", getRepositoryModel().description));
-		String owner = getRepositoryModel().owner;
-		if (StringUtils.isEmpty(owner)) {
-			add(new Label("repositoryOwner").setVisible(false));
-		} else {
-			UserModel ownerModel = GitBlit.self().getUserModel(owner);
-			if (ownerModel != null) {
-				add(new LinkPanel("repositoryOwner", null, ownerModel.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(owner)));
-			} else {
-				add(new Label("repositoryOwner", owner));
+		
+		// owner links
+		final List<String> owners = new ArrayList<String>(getRepositoryModel().owners);
+		ListDataProvider<String> ownersDp = new ListDataProvider<String>(owners);
+		DataView<String> ownersView = new DataView<String>("repositoryOwners", ownersDp) {
+			private static final long serialVersionUID = 1L;
+			int counter = 0;
+			public void populateItem(final Item<String> item) {
+				UserModel ownerModel = GitBlit.self().getUserModel(item.getModelObject());
+				if (ownerModel != null) {
+					item.add(new LinkPanel("owner", null, ownerModel.getDisplayName(), UserPage.class,
+							WicketUtils.newUsernameParameter(ownerModel.username)).setRenderBodyOnly(true));
+				} else {
+					item.add(new Label("owner").setVisible(false));
+				}
+				counter++;
+				item.add(new Label("comma", ",").setVisible(counter < owners.size()));
+				item.setRenderBodyOnly(true);
 			}
-		}
-
+		};
+		ownersView.setRenderBodyOnly(true);
+		add(ownersView);
+		
 		add(WicketUtils.createTimestampLabel("repositoryLastChange",
 				JGitUtils.getLastChange(r), getTimeZone(), getTimeUtils()));
 		if (metricsTotal == null) {
diff --git a/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java b/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
index 3c9bf7f..7b4ee9f 100644
--- a/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
+++ b/src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
@@ -127,16 +127,24 @@
 			add(WicketUtils.newBlankImage("accessRestrictionIcon"));
 		}
 
-		if (StringUtils.isEmpty(entry.owner)) {
+		if (ArrayUtils.isEmpty(entry.owners)) {
 			add(new Label("repositoryOwner").setVisible(false));
 		} else {
-			UserModel ownerModel = GitBlit.self().getUserModel(entry.owner);
-			String owner = entry.owner;
-			if (ownerModel != null) {
-				owner = ownerModel.getDisplayName();
+			String owner = "";
+			for (String username : entry.owners) {
+				UserModel ownerModel = GitBlit.self().getUserModel(username);
+			
+				if (ownerModel != null) {
+					owner = ownerModel.getDisplayName();
+				}				
 			}
-			add(new Label("repositoryOwner", owner + " (" +
+			if (entry.owners.size() > 1) {
+				owner += ", ...";
+			}
+			Label ownerLabel = (new Label("repositoryOwner", owner + " (" +
 					localizer.getString("gb.owner", parent) + ")"));
+			WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners));
+			add(ownerLabel);
 		}
 
 		UserModel user = GitBlitWebSession.get().getUser();
diff --git a/src/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/com/gitblit/wicket/panels/RepositoriesPanel.java
index ee5edfc..726af61 100644
--- a/src/com/gitblit/wicket/panels/RepositoriesPanel.java
+++ b/src/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -49,6 +49,7 @@
 import com.gitblit.models.ProjectModel;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.WicketUtils;
@@ -294,14 +295,23 @@
 					row.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
 				}
 
-				String owner = entry.owner;
-				if (!StringUtils.isEmpty(owner)) {
-					UserModel ownerModel = GitBlit.self().getUserModel(owner);
-					if (ownerModel != null) {
-						owner = ownerModel.getDisplayName();
+				String owner = "";
+				if (!ArrayUtils.isEmpty(entry.owners)) {
+					// display first owner
+					for (String username : entry.owners) {
+						UserModel ownerModel = GitBlit.self().getUserModel(username);
+						if (ownerModel != null) {
+							owner = ownerModel.getDisplayName();
+							break;
+						}
+					}
+					if (entry.owners.size() > 1) {
+						owner += ", ...";
 					}
 				}
-				row.add(new Label("repositoryOwner", owner));
+				Label ownerLabel = new Label("repositoryOwner", owner);
+				WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners));
+				row.add(ownerLabel);
 
 				String lastChange;
 				if (entry.lastChange.getTime() == 0) {
@@ -522,10 +532,12 @@
 				Collections.sort(list, new Comparator<RepositoryModel>() {
 					@Override
 					public int compare(RepositoryModel o1, RepositoryModel o2) {
+						String own1 = ArrayUtils.toString(o1.owners);
+						String own2 = ArrayUtils.toString(o2.owners);
 						if (asc) {
-							return o1.owner.compareTo(o2.owner);
+							return own1.compareTo(own2);
 						}
-						return o2.owner.compareTo(o1.owner);
+						return own2.compareTo(own1);
 					}
 				});
 			} else if (prop.equals(SortBy.description.name())) {
diff --git a/tests/com/gitblit/tests/FederationTests.java b/tests/com/gitblit/tests/FederationTests.java
index c8f686a..ced500a 100644
--- a/tests/com/gitblit/tests/FederationTests.java
+++ b/tests/com/gitblit/tests/FederationTests.java
@@ -72,7 +72,7 @@
 			model.accessRestriction = AccessRestrictionType.VIEW;
 			model.description = "cloneable repository " + i;
 			model.lastChange = new Date();
-			model.owner = "adminuser";
+			model.addOwner("adminuser");
 			model.name = "repo" + i + ".git";
 			model.size = "5 MB";
 			model.hasCommits = true;
diff --git a/tests/com/gitblit/tests/GitServletTest.java b/tests/com/gitblit/tests/GitServletTest.java
index 771c4b9..a05b365 100644
--- a/tests/com/gitblit/tests/GitServletTest.java
+++ b/tests/com/gitblit/tests/GitServletTest.java
@@ -40,6 +40,7 @@
 import com.gitblit.models.PushLogEntry;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.PushLogUtils;
 
@@ -725,7 +726,7 @@
 			
 			// confirm default personal repository permissions
 			RepositoryModel model = GitBlit.self().getRepositoryModel(MessageFormat.format("~{0}/ticgit.git", user.username));
-			assertEquals("Unexpected owner", user.username, model.owner);
+			assertEquals("Unexpected owner", user.username, ArrayUtils.toString(model.owners));
 			assertEquals("Unexpected authorization control", AuthorizationControl.NAMED, model.authorizationControl);
 			assertEquals("Unexpected access restriction", AccessRestrictionType.VIEW, model.accessRestriction);
 			
@@ -749,7 +750,7 @@
 			
 			// confirm default project repository permissions
 			RepositoryModel model = GitBlit.self().getRepositoryModel("project/ticgit.git");
-			assertEquals("Unexpected owner", user.username, model.owner);
+			assertEquals("Unexpected owner", user.username, ArrayUtils.toString(model.owners));
 			assertEquals("Unexpected authorization control", AuthorizationControl.fromName(GitBlit.getString(Keys.git.defaultAuthorizationControl, "NAMED")), model.authorizationControl);
 			assertEquals("Unexpected access restriction", AccessRestrictionType.fromName(GitBlit.getString(Keys.git.defaultAccessRestriction, "NONE")), model.accessRestriction);
 
diff --git a/tests/com/gitblit/tests/PermissionsTest.java b/tests/com/gitblit/tests/PermissionsTest.java
index b6ffa62..5a95104 100644
--- a/tests/com/gitblit/tests/PermissionsTest.java
+++ b/tests/com/gitblit/tests/PermissionsTest.java
@@ -2327,7 +2327,7 @@
 		repository.accessRestriction = AccessRestrictionType.VIEW;
 
 		UserModel user = new UserModel("test");
-		repository.owner = user.username;
+		repository.addOwner(user.username);
 
 		assertFalse("user SHOULD NOT HAVE a repository permission!", user.hasRepositoryPermission(repository.name));
 		assertTrue("owner CAN NOT view!", user.canView(repository));
@@ -2345,13 +2345,58 @@
 	}
 	
 	@Test
+	public void testMultipleOwners() throws Exception {
+		RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date());
+		repository.authorizationControl = AuthorizationControl.NAMED;
+		repository.accessRestriction = AccessRestrictionType.VIEW;
+
+		UserModel user = new UserModel("test");
+		repository.addOwner(user.username);
+		UserModel user2 = new UserModel("test2");
+		repository.addOwner(user2.username);
+
+		// first owner
+		assertFalse("user SHOULD NOT HAVE a repository permission!", user.hasRepositoryPermission(repository.name));
+		assertTrue("owner CAN NOT view!", user.canView(repository));
+		assertTrue("owner CAN NOT clone!", user.canClone(repository));
+		assertTrue("owner CAN NOT push!", user.canPush(repository));
+		
+		assertTrue("owner CAN NOT create ref!", user.canCreateRef(repository));
+		assertTrue("owner CAN NOT delete ref!", user.canDeleteRef(repository));
+		assertTrue("owner CAN NOT rewind ref!", user.canRewindRef(repository));
+
+		assertTrue("owner CAN NOT fork!", user.canFork(repository));
+		
+		assertFalse("owner CAN NOT delete!", user.canDelete(repository));
+		assertTrue("owner CAN NOT edit!", user.canEdit(repository));
+		
+		// second owner
+		assertFalse("user SHOULD NOT HAVE a repository permission!", user2.hasRepositoryPermission(repository.name));
+		assertTrue("owner CAN NOT view!", user2.canView(repository));
+		assertTrue("owner CAN NOT clone!", user2.canClone(repository));
+		assertTrue("owner CAN NOT push!", user2.canPush(repository));
+		
+		assertTrue("owner CAN NOT create ref!", user2.canCreateRef(repository));
+		assertTrue("owner CAN NOT delete ref!", user2.canDeleteRef(repository));
+		assertTrue("owner CAN NOT rewind ref!", user2.canRewindRef(repository));
+
+		assertTrue("owner CAN NOT fork!", user2.canFork(repository));
+		
+		assertFalse("owner CAN NOT delete!", user2.canDelete(repository));
+		assertTrue("owner CAN NOT edit!", user2.canEdit(repository));
+		
+		assertTrue(repository.isOwner(user.username));
+		assertTrue(repository.isOwner(user2.username));	
+	}
+	
+	@Test
 	public void testOwnerPersonalRepository() throws Exception {
 		RepositoryModel repository = new RepositoryModel("~test/myrepo.git", null, null, new Date());
 		repository.authorizationControl = AuthorizationControl.NAMED;
 		repository.accessRestriction = AccessRestrictionType.VIEW;
 
 		UserModel user = new UserModel("test");
-		repository.owner = user.username;
+		repository.addOwner(user.username);
 
 		assertFalse("user SHOULD NOT HAVE a repository permission!", user.hasRepositoryPermission(repository.name));
 		assertTrue("user CAN NOT view!", user.canView(repository));
@@ -2375,7 +2420,7 @@
 		repository.accessRestriction = AccessRestrictionType.VIEW;
 
 		UserModel user = new UserModel("visitor");
-		repository.owner = "test";
+		repository.addOwner("test");
 
 		assertFalse("user HAS a repository permission!", user.hasRepositoryPermission(repository.name));
 		assertFalse("user CAN view!", user.canView(repository));
diff --git a/tests/com/gitblit/tests/RpcTests.java b/tests/com/gitblit/tests/RpcTests.java
index 62d87bf..3241a8a 100644
--- a/tests/com/gitblit/tests/RpcTests.java
+++ b/tests/com/gitblit/tests/RpcTests.java
@@ -167,7 +167,7 @@
 		RepositoryModel model = new RepositoryModel();
 		model.name = "garbagerepo.git";
 		model.description = "created by RpcUtils";
-		model.owner = "garbage";
+		model.addOwner("garbage");
 		model.accessRestriction = AccessRestrictionType.VIEW;
 		model.authorizationControl = AuthorizationControl.AUTHENTICATED;
 

--
Gitblit v1.9.1