James Moger
2011-12-20 eb96eab513101783a750c13419de6bdce3d2fa4c
Renamed GitblitUserService, sendMail method, sendmail.groovy, and RepositoryModel.mailingLists
1 files renamed
1 files deleted
1 files added
14 files modified
640 ■■■■ changed files
docs/01_setup.mkd 26 ●●●● patch | view | raw | blame | history
docs/04_releases.mkd 10 ●●●●● patch | view | raw | blame | history
groovy/sendmail.groovy 40 ●●●● patch | view | raw | blame | history
resources/bootstrap.gb.css 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/FederationPullExecutor.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 22 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitblitUserService.java 217 ●●●●● patch | view | raw | blame | history
src/com/gitblit/UserServiceWrapper.java 221 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/EditRepositoryDialog.java 21 ●●●● patch | view | raw | blame | history
src/com/gitblit/models/RepositoryModel.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.html 22 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.java 30 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditTeamPage.html 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.html 8 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoriesPanel.java 3 ●●●● patch | view | raw | blame | history
test-gitblit.properties 2 ●●● patch | view | raw | blame | history
docs/01_setup.mkd
@@ -217,6 +217,18 @@
There are two actual *roles* in Gitblit: *#admin*, which grants administrative powers to that user, and *#notfederated*, which prevents an account from being pulled by another Gitblit instance.  Administrators automatically have access to all repositories.  All other *roles* are repository names.  If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction.  This is how users are granted access to a restricted repository.
## Authentication and Authorization Customization
### Choice 1: Customize Authentication Only
This is the simplest choice where you implement custom authentication and delegate all other standard user and team operations to one of Gitblit's user service implementations.  This choice insulates your customization from changes in User and Team model classes and additional API that may be added to IUserService.
Please subclass [com.gitblit.GitblitUserService](https://github.com/gitblit/gitblit/blob/master/src/com/gitblit/GitblitUserService.java) and override the *setup()* and *authenticate()* methods.
Make sure to set the *serviceImpl* field in your *setup()* method.
You may use your subclass by specifying its fully qualified classname in the *realm.userService* setting.
Your subclass must be on Gitblit's classpath and must have a public default constructor.
### Choice 2: Customize Everything
Instead of maintaining a `users.conf` or `users.properties` file, you may want to integrate Gitblit into an existing environment.
You may use your own custom *com.gitblit.IUserService* implementation by specifying its fully qualified classname in the *realm.userService* setting.
@@ -232,19 +244,21 @@
The Groovy hook mechanism allows for dynamic extension of Gitblit to execute custom tasks on receiving and processing push events.  The scripts run within the context of your Gitblit instance and therefore have access to Gitblit's internals at runtime.
### Rules & Requirements
### Rules, Requirements, & Behaviors
1. Your Groovy scripts must be stored in the *groovy.scriptsFolder* as specified in `gitblit.properties` or `web.xml`.
2. All script files must have the *.groovy* extension. Because of this you may omit the extension when specifying the script.
3. Scripts must be explicitly specified to be executed, no scripts are *automatically* executed by name or extension.
4. A script can be specified to run on *all repositories* by adding the script file name to *groovy.preReceiveScripts* or *groovy.postReceiveScripts* in `gitblit.properties` or `web.xml`.
4. A script can be specified to run on *all repositories* by adding the script file name to *groovy.preReceiveScripts* or *groovy.postReceiveScripts* in `gitblit.properties` or `web.xml`.  Be mindful of access retrictions and global properties like *mail.mailingLists* if specifying *sendmail* to run on all repositories.
5. Scripts may also be specified per-repository in the repository's settings.
6. Global/shared scripts are executed first in their listed order, followed by per-repository scripts in their listed order.
7. A script may only be defined once in a pre-receive list and once in a post-receive list.
6. Globally specified scripts are excluded from the list of available scripts in a repository's settings
7. Globally specified scripts are executed first, in their listed order, followed by per-repository scripts, in their listed order.
8. A script may only be defined once in a pre-receive list and once in a post-receive list.
You may execute the same script on pre-receive and post-receive, just not multiple times within a pre-receive or post-receive event.
8. Gitblit does not differentiate between what can be a pre-receive script and what can be a post-receive script.
9. If a script *returns false* then the hook chain is aborted and none of the subsequent scripts will execute.
9. Gitblit does not differentiate between what can be a pre-receive script and what can be a post-receive script.
10. If a script *returns false* then the pre-receive or post-receive hook chain is aborted and none of the subsequent scripts will execute.
Some sample scripts are included in the GO and WAR distributions to show you how you can tap into Gitblit with the provided bound variables.  Additional implementation details may be specified in the header comment of these examples.  
Hook contributions and improvements are welcome.
### Pre-Receive
docs/04_releases.mkd
@@ -4,16 +4,18 @@
**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%) | [war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%) | [express](http://code.google.com/p/gitblit/downloads/detail?name=%EXPRESS%) | [fedclient](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%) | [manager](http://code.google.com/p/gitblit/downloads/detail?name=%MANAGER%) | [api](http://code.google.com/p/gitblit/downloads/detail?name=%API%)) based on [%JGIT%][jgit]   *released %BUILDDATE%*
- updated: Gitblit GO is now monolithic like the WAR build. (issue 30)  
Either the dependencies are downloaded on first execution OR the dependencies are bundled, either way you would need the dependencies.  This change helps adoption of GO in environments without an internet connection or with a restricted connection.
This change helps adoption of GO in environments without an internet connection or with a restricted connection.
- added: Groovy 1.8.4 and sample pre- and post- push Groovy hook scripts.  Hook scripts can be set per-repository or globally for all repositories.  
Unfortunately this adds another 6 MB to the 8MB Gitblit package, but it allows for a *very* powerful, flexible, platform-independent hook script mechanism.  
    **New:** *groovy.scriptsFolder = groovy*  
    **New:** *groovy.preReceiveScripts =*  
    **New:** *groovy.postReceiveScripts =*
- added: New key for mailing lists.  This is _currently_ used in conjunction with the example *sendemail.groovy* post-receive hook script.
- added: New key for mailing lists.  This is used in conjunction with the *sendmail.groovy* hook script.
    **New:** *mail.mailingLists =*
- added: GitblitUserService.  This is a wrapper object for the built-in user service implementations.  For those wanting to only implement *custom authentication* it is recommended to subclass GitblitUserService and override the appropriate methods.  Going forward, this will insulate customized behavior from new IUserService API and changes in model classes.
- added: new default user service implementation: com.gitblit.ConfigUserService (users.conf)  
This user service implementation allows for serialization and deserialization of more sophisticated Gitblit User objects and will open the door for more advanced Gitblit features. For upgrading installations, a `users.conf` file will automatically be created for you from your existing `users.properties` file on your first launch of Gitblit.  You will have to manually set *realm.userService=users.conf* to switch to the new user service.  The original `users.properties` file and it's corresponding implementation are deprecated.
This user service implementation allows for serialization and deserialization of more sophisticated Gitblit User objects and will open the door for more advanced Gitblit features. For upgrading installations, a `users.conf` file will automatically be created for you from your existing `users.properties` file on your first launch of Gitblit.  You will have to manually set *realm.userService=users.conf* to switch to the new user service.
The original `users.properties` file and it's corresponding implementation are **deprecated**.
    **New:** *realm.userService = users.conf*
- added: Teams for specifying user-repository access in bulk
- added: Gitblit Express bundle to get started running Gitblit on RedHat's OpenShift cloud
@@ -27,7 +29,7 @@
- added: optional flash-based 1-step *copy to clipboard* of the primary repository url
- added: javascript-based 3-step (click, ctrl+c, enter) *copy to clipboard* of the primary repository url
   **New:** *web.allowFlashCopyToClipboard = true*  
- improved: empty repositories now link to the *empty repository* page which gives some direction to the user for the next step in using Gitblit.  This page displays the primary push/clone url of the repository and gives sample syntax for the git command-line client. (issue 31)
- improved: empty repositories now link to a new *empty repository* page which gives some direction to the user for the next step in using Gitblit.  This page displays the primary push/clone url of the repository and gives sample syntax for the git command-line client. (issue 31)
- improved: unit testing framework has been migrated to JUnit4 syntax and the test suite has been redesigned to run all unit tests, including rpc, federation, and git push/clone tests
### Older Releases
groovy/sendmail.groovy
File was renamed from groovy/sendemail.groovy
@@ -26,7 +26,7 @@
import org.slf4j.Logger
/**
 * Sample Gitblit Post-Receive Hook: sendemail
 * Sample Gitblit Post-Receive Hook: sendmail
 *
 * The Post-Receive hook is executed AFTER the pushed commits have been applied
 * to the Git repository.  This is the appropriate point to trigger an
@@ -60,16 +60,16 @@
 */
// Indicate we have started the script
logger.info("sendemail hook triggered by ${user.username} for ${repository.name}")
logger.info("sendmail hook triggered by ${user.username} for ${repository.name}")
/*
 * Primitive example email notification with example repository-specific checks.
 * Primitive email notification.
 * This requires the mail settings to be properly configured in Gitblit.
 */
Repository r = gitblit.getRepository(repository.name)
// reuse some existing repository config settings, if available
// reuse existing repository config settings, if available
Config config = r.getConfig()
def mailinglist = config.getString('hooks', null, 'mailinglist')
def emailprefix = config.getString('hooks', null, 'emailprefix')
@@ -80,23 +80,15 @@
emailprefix = '[Gitblit]'
if (mailinglist != null) {
    def addrs = mailinglist.split('(,|\\s)')
    def addrs = mailinglist.split(/(,|\s)/)
    toAddresses.addAll(addrs)
}
// add all mailing lists defined in gitblit.properties or web.xml
toAddresses.addAll(gitblit.getStrings(Keys.mail.mailingLists))
// add all mail recipients for the repository
toAddresses.addAll(repository.mailRecipients)
// special custom cases
switch(repository.name) {
    case 'ex@mple.git':
        toAddresses.add 'dev-team@somewhere.com'
        toAddresses.add 'qa-team@somewhere.com'
        break
}
// add all mailing lists for the repository
toAddresses.addAll(repository.mailingLists)
// define the summary and commit urls
def repo = repository.name.replace('/', gitblit.getString(Keys.web.forwardSlashCharacter, '/'))
@@ -115,13 +107,19 @@
def changes = ''
def table = { it.authorIdent.name.padRight(25, ' ') + it.shortMessage + "\n$commitUrl" + it.id.name }
for (command in commands) {
    def ref = command.refName.substring('refs/heads/'.length())
    def ref = command.refName
    if (ref.startsWith('refs/heads/')) {
        ref  = command.refName.substring('refs/heads/'.length())
    } else if (ref.startsWith('refs/tags/')) {
        ref  = command.refName.substring('refs/tags/'.length())
    }
    switch (command.type) {
        case ReceiveCommand.Type.CREATE:
            def commits = JGitUtils.getRevLog(r, command.oldId.name, command.newId.name)
            commitCount += commits.size()
            // new branch commits table
            changes += "created $ref ($commits.size commits)\n\n"
            changes += "$ref created ($commits.size commits)\n\n"
            changes += commits.collect(table).join('\n\n')
            changes += '\n'
            break
@@ -129,7 +127,7 @@
            def commits = JGitUtils.getRevLog(r, command.oldId.name, command.newId.name)
            commitCount += commits.size()
            // fast-forward branch commits table
            changes += "updated $ref ($commits.size commits)\n\n"
            changes += "$ref updated ($commits.size commits)\n\n"
            changes += commits.collect(table).join('\n\n')
            changes += '\n'
            break
@@ -137,13 +135,13 @@
            def commits = JGitUtils.getRevLog(r, command.oldId.name, command.newId.name)
            commitCount += commits.size()
            // non-fast-forward branch commits table
            changes += "updated $ref [NON fast-forward] ($commits.size commits)\n\n"
            changes += "$ref updated [NON fast-forward] ($commits.size commits)\n\n"
            changes += commits.collect(table).join('\n\n')
            changes += '\n'
            break
        case ReceiveCommand.Type.DELETE:
            // deleted branch
            changes += "deleted $ref\n\n"
            changes += "$ref deleted\n\n"
            break
        default:
            break
@@ -153,4 +151,4 @@
r.close()
// tell Gitblit to send the message (Gitblit filters duplicate addresses)
gitblit.sendEmail("$emailprefix $user.username pushed $commitCount commits => $repository.name", "$summaryUrl\n\n$changes", toAddresses)
gitblit.sendMail("$emailprefix $user.username pushed $commitCount commits => $repository.name", "$summaryUrl\n\n$changes", toAddresses)
resources/bootstrap.gb.css
@@ -690,6 +690,10 @@
    width: 13em;
}
span.help-inline {
    color: #777;
}
span.metricsTitle {
    font-size: 2em;
}
src/com/gitblit/FederationPullExecutor.java
@@ -113,7 +113,7 @@
                        String message = "Federation pull of " + registration.name + " @ "
                                + registration.url + " is now at " + is.name();
                        GitBlit.self()
                                .sendEmailToAdministrators(
                                .sendMailToAdministrators(
                                        "Pull Status of " + registration.name + " is " + is.name(),
                                        message);
                    }
src/com/gitblit/GitBlit.java
@@ -743,8 +743,8 @@
                    "gitblit", null, "preReceiveScript")));
            model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
                    "gitblit", null, "postReceiveScript")));
            model.mailRecipients = new ArrayList<String>(Arrays.asList(config.getStringList(
                    "gitblit", null, "mailRecipient")));
            model.mailingLists = new ArrayList<String>(Arrays.asList(config.getStringList(
                    "gitblit", null, "mailingList")));
        }
        r.close();
        return model;
@@ -971,8 +971,8 @@
            config.setStringList("gitblit", null, "postReceiveScript",
                    repository.postReceiveScripts);
        }
        if (repository.mailRecipients != null) {
            config.setStringList("gitblit", null, "mailRecipient", repository.mailRecipients);
        if (repository.mailingLists != null) {
            config.setStringList("gitblit", null, "mailingList", repository.mailingLists);
        }
        try {
            config.save();
@@ -1485,7 +1485,7 @@
     * @param subject
     * @param message
     */
    public void sendEmailToAdministrators(String subject, String message) {
    public void sendMailToAdministrators(String subject, String message) {
        try {
            Message mail = mailExecutor.createMessageForAdministrators();
            if (mail != null) {
@@ -1505,8 +1505,8 @@
     * @param message
     * @param toAddresses
     */
    public void sendEmail(String subject, String message, ArrayList<String> toAddresses) {
        this.sendEmail(subject, message, toAddresses.toArray(new String[0]));
    public void sendMail(String subject, String message, ArrayList<String> toAddresses) {
        this.sendMail(subject, message, toAddresses.toArray(new String[0]));
    }
    /**
@@ -1516,7 +1516,7 @@
     * @param message
     * @param toAddresses
     */
    public void sendEmail(String subject, String message, String... toAddresses) {
    public void sendMail(String subject, String message, String... toAddresses) {
        try {
            Message mail = mailExecutor.createMessage(toAddresses);
            if (mail != null) {
@@ -1634,11 +1634,7 @@
                loginService = (IUserService) realmClass.newInstance();
            }
        } catch (Throwable t) {
            loginService = new UserServiceWrapper() {
                @Override
                public void setupService(IStoredSettings settings) {
                }
            };
            loginService = new GitblitUserService();
        }
        setUserService(loginService);
        mailExecutor = new MailExecutor(settings);
src/com/gitblit/GitblitUserService.java
New file
@@ -0,0 +1,217 @@
/*
 * 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;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
/**
 * This class wraps the default user service and is recommended as the starting
 * point for custom user service implementations.
 *
 * This does seem a little convoluted, but the idea is to allow IUserService to
 * evolve with new methods and implementations without breaking custom
 * authentication implementations.
 *
 * The most common implementation of a custom IUserService is to only override
 * authentication and then delegate all other functionality to one of Gitblit's
 * user services. This class optimizes that use-case.
 *
 * Extending GitblitUserService allows for authentication customization without
 * having to keep-up-with IUSerService API changes.
 *
 * @author James Moger
 *
 */
public class GitblitUserService implements IUserService {
    protected IUserService serviceImpl;
    private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class);
    public GitblitUserService() {
    }
    @Override
    public void setup(IStoredSettings settings) {
        File realmFile = GitBlit.getFileOrFolder(Keys.realm.userService, "users.conf");
        serviceImpl = createUserService(realmFile);
    }
    @SuppressWarnings("deprecation")
    protected IUserService createUserService(File realmFile) {
        IUserService service = null;
        if (realmFile.getName().toLowerCase().endsWith(".properties")) {
            // v0.5.0 - v0.7.0 properties-based realm file
            service = new FileUserService(realmFile);
        } else if (realmFile.getName().toLowerCase().endsWith(".conf")) {
            // v0.8.0+ config-based realm file
            service = new ConfigUserService(realmFile);
        }
        assert service != null;
        if (realmFile.exists()) {
            // Create the Administrator account for a new realm file
            try {
                realmFile.createNewFile();
            } catch (IOException x) {
                logger.error(MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x);
            }
            UserModel admin = new UserModel("admin");
            admin.password = "admin";
            admin.canAdmin = true;
            admin.excludeFromFederation = true;
            service.updateUserModel(admin);
        }
        if (service instanceof FileUserService) {
            // automatically create a users.conf realm file from the original
            // users.properties file
            File usersConfig = new File(realmFile.getParentFile(), "users.conf");
            if (!usersConfig.exists()) {
                logger.info(MessageFormat.format("Automatically creating {0} based on {1}",
                        usersConfig.getAbsolutePath(), realmFile.getAbsolutePath()));
                ConfigUserService configService = new ConfigUserService(usersConfig);
                for (String username : serviceImpl.getAllUsernames()) {
                    UserModel userModel = serviceImpl.getUserModel(username);
                    configService.updateUserModel(userModel);
                }
            }
            // issue suggestion about switching to users.conf
            logger.warn("Please consider using \"users.conf\" instead of the deprecated \"users.properties\" file");
        }
        return service;
    }
    @Override
    public boolean supportsCookies() {
        return serviceImpl.supportsCookies();
    }
    @Override
    public char[] getCookie(UserModel model) {
        return serviceImpl.getCookie(model);
    }
    @Override
    public UserModel authenticate(char[] cookie) {
        return serviceImpl.authenticate(cookie);
    }
    @Override
    public UserModel authenticate(String username, char[] password) {
        return serviceImpl.authenticate(username, password);
    }
    @Override
    public UserModel getUserModel(String username) {
        return serviceImpl.getUserModel(username);
    }
    @Override
    public boolean updateUserModel(UserModel model) {
        return serviceImpl.updateUserModel(model);
    }
    @Override
    public boolean updateUserModel(String username, UserModel model) {
        return serviceImpl.updateUserModel(username, model);
    }
    @Override
    public boolean deleteUserModel(UserModel model) {
        return serviceImpl.deleteUserModel(model);
    }
    @Override
    public boolean deleteUser(String username) {
        return serviceImpl.deleteUser(username);
    }
    @Override
    public List<String> getAllUsernames() {
        return serviceImpl.getAllUsernames();
    }
    @Override
    public List<String> getAllTeamNames() {
        return serviceImpl.getAllTeamNames();
    }
    @Override
    public List<String> getTeamnamesForRepositoryRole(String role) {
        return serviceImpl.getTeamnamesForRepositoryRole(role);
    }
    @Override
    public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
        return serviceImpl.setTeamnamesForRepositoryRole(role, teamnames);
    }
    @Override
    public TeamModel getTeamModel(String teamname) {
        return serviceImpl.getTeamModel(teamname);
    }
    @Override
    public boolean updateTeamModel(TeamModel model) {
        return serviceImpl.updateTeamModel(model);
    }
    @Override
    public boolean updateTeamModel(String teamname, TeamModel model) {
        return serviceImpl.updateTeamModel(teamname, model);
    }
    @Override
    public boolean deleteTeamModel(TeamModel model) {
        return serviceImpl.deleteTeamModel(model);
    }
    @Override
    public boolean deleteTeam(String teamname) {
        return serviceImpl.deleteTeam(teamname);
    }
    @Override
    public List<String> getUsernamesForRepositoryRole(String role) {
        return serviceImpl.getUsernamesForRepositoryRole(role);
    }
    @Override
    public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
        return serviceImpl.setUsernamesForRepositoryRole(role, usernames);
    }
    @Override
    public boolean renameRepositoryRole(String oldRole, String newRole) {
        return serviceImpl.renameRepositoryRole(oldRole, newRole);
    }
    @Override
    public boolean deleteRepositoryRole(String role) {
        return serviceImpl.deleteRepositoryRole(role);
    }
}
src/com/gitblit/UserServiceWrapper.java
File was deleted
src/com/gitblit/client/EditRepositoryDialog.java
@@ -89,7 +89,7 @@
    private JCheckBox isFrozen;
    private JTextField mailRecipientsField;
    private JTextField mailingListsField;
    private JComboBox accessRestriction;
@@ -164,8 +164,8 @@
                anRepository.skipSummaryMetrics);
        isFrozen = new JCheckBox(Translation.get("gb.isFrozenDescription"), anRepository.isFrozen);
        mailRecipientsField = new JTextField(anRepository.mailRecipients == null ? ""
                : StringUtils.flattenStrings(anRepository.mailRecipients, " "), 50);
        mailingListsField = new JTextField(anRepository.mailingLists == null ? ""
                : StringUtils.flattenStrings(anRepository.mailingLists, " "), 50);
        accessRestriction = new JComboBox(AccessRestrictionType.values());
        accessRestriction.setRenderer(new AccessRestrictionRenderer());
@@ -198,7 +198,7 @@
        fieldsPanel
                .add(newFieldPanel(Translation.get("gb.skipSummaryMetrics"), skipSummaryMetrics));
        fieldsPanel.add(newFieldPanel(Translation.get("gb.isFrozen"), isFrozen));
        fieldsPanel.add(newFieldPanel(Translation.get("gb.mailRecipients"), mailRecipientsField));
        fieldsPanel.add(newFieldPanel(Translation.get("gb.mailingLists"), mailingListsField));
        usersPalette = new JPalette<String>();
        JPanel accessPanel = new JPanel(new BorderLayout(5, 5));
@@ -371,8 +371,17 @@
        repository.skipSummaryMetrics = skipSummaryMetrics.isSelected();
        repository.isFrozen = isFrozen.isSelected();
        repository.mailRecipients = StringUtils.getStringsFromValue(mailRecipientsField.getText()
                .trim(), " ");
        String ml = mailingListsField.getText();
        if (!StringUtils.isEmpty(ml)) {
            Set<String> list = new HashSet<String>();
            for (String address : ml.split("(,|\\s)")) {
                if (StringUtils.isEmpty(address)) {
                    continue;
                }
                list.add(address.toLowerCase());
            }
            repository.mailingLists = new ArrayList<String>(list);
        }
        repository.accessRestriction = (AccessRestrictionType) accessRestriction.getSelectedItem();
        repository.federationStrategy = (FederationStrategy) federationStrategy.getSelectedItem();
src/com/gitblit/models/RepositoryModel.java
@@ -57,7 +57,7 @@
    public String size;
    public List<String> preReceiveScripts;
    public List<String> postReceiveScripts;
    public List<String> mailRecipients;
    public List<String> mailingLists;
    public RepositoryModel() {
        this("", "", "", new Date(0));
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -196,8 +196,8 @@
gb.permittedTeams = permitted teams
gb.emptyRepository = empty repository
gb.repositoryUrl = repository url
gb.mailRecipients = mail recipients
gb.mailRecipientsDescription = space-delimited, used by sendemail Groovy hook
gb.mailingLists = mailing lists
gb.mailingListsDescription = used by the sendmail hook
gb.preReceiveScripts = pre-receive scripts
gb.postReceiveScripts = post-receive scripts
gb.groovyHookScripts = hook scripts
gb.hookScripts = hook scripts
src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -11,18 +11,18 @@
        <table class="plain">
            <tbody class="settings">
                <tr><td colspan="2"><h3><wicket:message key="gb.general"></wicket:message></h3></td></tr>
                <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input class="span6" type="text" wicket:id="name" id="name" size="40" tabindex="1" /> &nbsp;<i><wicket:message key="gb.nameDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input class="span6" type="text" wicket:id="name" id="name" size="40" tabindex="1" /> &nbsp;<span class="help-inline"><wicket:message key="gb.nameDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.description"></wicket:message></th><td class="edit"><input class="span6" type="text" wicket:id="description" size="40" tabindex="2" /></td></tr>
                <tr><th><wicket:message key="gb.origin"></wicket:message></th><td class="edit"><input class="span7" type="text" wicket:id="origin" size="80" tabindex="3" /></td></tr>
                <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select wicket:id="owner" tabindex="4" /> &nbsp;<i><wicket:message key="gb.ownerDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useTickets" tabindex="5" /> &nbsp;<i><wicket:message key="gb.useTicketsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useDocs" tabindex="6" /> &nbsp;<i><wicket:message key="gb.useDocsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="7" /> &nbsp;<i><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.showReadme"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showReadme" tabindex="8" /> &nbsp;<i><wicket:message key="gb.showReadmeDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.skipSizeCalculation"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="skipSizeCalculation" tabindex="9" /> &nbsp;<i><wicket:message key="gb.skipSizeCalculationDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.skipSummaryMetrics"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="skipSummaryMetrics" tabindex="10" /> &nbsp;<i><wicket:message key="gb.skipSummaryMetricsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="isFrozen" tabindex="11" /> &nbsp;<i><wicket:message key="gb.isFrozenDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.mailRecipients"></wicket:message></th><td class="edit"><input class="span9" type="text" wicket:id="mailRecipients" size="40" tabindex="12" /> &nbsp;<i><wicket:message key="gb.mailRecipientsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select wicket:id="owner" tabindex="4" /> &nbsp;<span class="help-inline"><wicket:message key="gb.ownerDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useTickets" tabindex="5" /> &nbsp;<span class="help-inline"><wicket:message key="gb.useTicketsDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useDocs" tabindex="6" /> &nbsp;<span class="help-inline"><wicket:message key="gb.useDocsDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.showReadme"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showReadme" tabindex="8" /> &nbsp;<span class="help-inline"><wicket:message key="gb.showReadmeDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.skipSizeCalculation"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="skipSizeCalculation" tabindex="9" /> &nbsp;<span class="help-inline"><wicket:message key="gb.skipSizeCalculationDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.skipSummaryMetrics"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="skipSummaryMetrics" tabindex="10" /> &nbsp;<span class="help-inline"><wicket:message key="gb.skipSummaryMetricsDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="isFrozen" tabindex="11" /> &nbsp;<span class="help-inline"><wicket:message key="gb.isFrozenDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.mailingLists"></wicket:message></th><td class="edit"><input class="span12" type="text" wicket:id="mailingLists" size="40" tabindex="12" /> &nbsp;<span class="help-inline"><wicket:message key="gb.mailingListsDescription"></wicket:message></span></td></tr>
                <tr><td colspan="2"><h3><wicket:message key="gb.accessRestriction"></wicket:message></h3></td></tr>    
                <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span6" wicket:id="accessRestriction" tabindex="13" /></td></tr>                
                <tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
@@ -33,7 +33,7 @@
                <tr><td colspan="2"><h3><wicket:message key="gb.hookScripts"></wicket:message></h3></td></tr>    
                <tr><th style="vertical-align: top;"><wicket:message key="gb.preReceiveScripts"></wicket:message></th><td style="padding:2px;"><span wicket:id="preReceiveScripts"></span></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.postReceiveScripts"></wicket:message></th><td style="padding:2px;"><span wicket:id="postReceiveScripts"></span></td></tr>
                <tr><th></th><td class="editButton"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="15" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="16" /> </td></tr>
                <tr><td colspan='2'><div class="actions" "><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="15" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="16" /></div></td></tr>
            </tbody>
        </table>
    </form>    
src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -19,9 +19,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.wicket.PageParameters;
import org.apache.wicket.extensions.markup.html.form.palette.Palette;
@@ -55,7 +57,7 @@
    private boolean isAdmin;
    private IModel<String> mailRecipients;
    private IModel<String> mailingLists;
    public EditRepositoryPage() {
        // create constructor
@@ -118,7 +120,8 @@
        }
        final Palette<String> preReceivePalette = new Palette<String>("preReceiveScripts",
                new ListModel<String>(preReceiveScripts), new CollectionModel<String>(GitBlit
                        .self().getAvailableScripts()), new ChoiceRenderer<String>("", ""), 12, true);
                        .self().getAvailableScripts()), new ChoiceRenderer<String>("", ""), 12,
                true);
        // post-receive palette
        if (repositoryModel.postReceiveScripts != null) {
@@ -126,7 +129,8 @@
        }
        final Palette<String> postReceivePalette = new Palette<String>("postReceiveScripts",
                new ListModel<String>(postReceiveScripts), new CollectionModel<String>(GitBlit
                        .self().getAvailableScripts()), new ChoiceRenderer<String>("", ""), 12, true);
                        .self().getAvailableScripts()), new ChoiceRenderer<String>("", ""), 12,
                true);
        CompoundPropertyModel<RepositoryModel> model = new CompoundPropertyModel<RepositoryModel>(
                repositoryModel);
@@ -191,11 +195,17 @@
                        }
                    }
                    // set mail recipients
                    String ml = mailRecipients.getObject();
                    // set mailing lists
                    String ml = mailingLists.getObject();
                    if (!StringUtils.isEmpty(ml)) {
                        List<String> list = StringUtils.getStringsFromValue(ml.trim(), " ");
                        repositoryModel.mailRecipients = list;
                        Set<String> list = new HashSet<String>();
                        for (String address : ml.split("(,|\\s)")) {
                            if (StringUtils.isEmpty(address)) {
                                continue;
                            }
                            list.add(address.toLowerCase());
                        }
                        repositoryModel.mailingLists = new ArrayList<String>(list);
                    }
                    // pre-receive scripts
@@ -275,9 +285,9 @@
        form.add(new CheckBox("showReadme"));
        form.add(new CheckBox("skipSizeCalculation"));
        form.add(new CheckBox("skipSummaryMetrics"));
        mailRecipients = new Model<String>(repositoryModel.mailRecipients == null ? ""
                : StringUtils.flattenStrings(repositoryModel.mailRecipients, " "));
        form.add(new TextField<String>("mailRecipients", mailRecipients));
        mailingLists = new Model<String>(repositoryModel.mailingLists == null ? ""
                : StringUtils.flattenStrings(repositoryModel.mailingLists, " "));
        form.add(new TextField<String>("mailingLists", mailingLists));
        form.add(usersPalette);
        form.add(teamsPalette);
        form.add(federationSetsPalette);
src/com/gitblit/wicket/pages/EditTeamPage.html
@@ -9,13 +9,13 @@
    <!-- User Table -->
    <form style="padding-top:5px;" wicket:id="editForm">
        <table class="plain">
            <tbody>
            <tbody class="settings">
                <tr><th><wicket:message key="gb.teamName"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" id="name" size="30" tabindex="1" /></td></tr>
                <tr><td colspan="2"><hr></hr></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.teamMembers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
                <tr><td colspan="2"><hr></hr></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
                <tr><th></th><td class="editButton"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="3" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="4" /></td></tr>
                <tr><td colspan='2'><div class="actions"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="3" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="4" /></div></td></tr>
            </tbody>
        </table>
    </form>    
src/com/gitblit/wicket/pages/EditUserPage.html
@@ -9,17 +9,17 @@
    <!-- User Table -->
    <form style="padding-top:5px;" wicket:id="editForm">
        <table class="plain">
            <tbody>
            <tbody class="settings">
                <tr><th><wicket:message key="gb.username"></wicket:message></th><td class="edit"><input type="text" wicket:id="username" id="username" size="30" tabindex="1" /></td></tr>
                <tr><th><wicket:message key="gb.password"></wicket:message></th><td class="edit"><input type="password" wicket:id="password" size="30" tabindex="2" /></td></tr>
                <tr><th><wicket:message key="gb.confirmPassword"></wicket:message></th><td class="edit"><input type="password" wicket:id="confirmPassword" size="30" tabindex="3" /></td></tr>
                <tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<i><wicket:message key="gb.canAdminDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="7" /> &nbsp;<i><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canAdminDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></span></td></tr>
                <tr><td colspan="2"><hr></hr></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.teamMemberships"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
                <tr><td colspan="2"><hr></hr></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
                <tr><th></th><td class="editButton"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="8" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /></td></tr>
                <tr><td colspan='2'><div class="actions"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="8" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /></div></td></tr>
            </tbody>
        </table>
    </form>    
src/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -25,6 +25,7 @@
import java.util.List;
import java.util.Map;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.extensions.markup.html.repeater.data.sort.OrderByBorder;
import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
@@ -157,7 +158,7 @@
                }
                // repository swatch
                Label swatch = new Label("repositorySwatch", " ");
                Component swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
                WicketUtils.setCssBackground(swatch, entry.name);
                row.add(swatch);
                swatch.setVisible(showSwatch);
test-gitblit.properties
@@ -7,7 +7,7 @@
git.enableGitServlet = true
groovy.scriptsFolder = groovy
groovy.preReceiveScripts = blockpush
groovy.postReceiveScripts = sendemail jenkins
groovy.postReceiveScripts = sendmail jenkins
web.authenticateViewPages = false
web.authenticateAdminPages = true
web.allowCookieAuthentication = true