James Moger
2013-01-23 1613f4067028b73e05544b55d45b6e136ce0238d
Merged multiple owners (pull request #63)
23 files modified
323 ■■■■ changed files
docs/04_releases.mkd 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 22 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitFilter.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/client/EditRepositoryDialog.java 24 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitClient.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/client/JPalette.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/client/RepositoriesPanel.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/RepositoriesTableModel.java 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/models/RepositoryModel.java 47 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/UserModel.java 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/ArrayUtils.java 30 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.java 20 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.html 8 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.java 36 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java 20 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoriesPanel.java 28 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/FederationTests.java 2 ●●● patch | view | raw | blame | history
tests/com/gitblit/tests/GitServletTest.java 5 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/PermissionsTest.java 51 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/RpcTests.java 2 ●●● patch | view | raw | blame | history
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
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);
                }
            }
        }
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
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);
    }
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) {
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));
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);
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:
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;
    }
}
    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);
            }
        }
    }
}
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;
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;
    }
}
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
gb.isSparkleshared = repository is Sparkleshared
gb.owners = owners
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>
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);
                        }
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));
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>
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) {
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();
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())) {
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;
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);
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));
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;