James Moger
2011-12-27 d7905a15bf41df0574bd8613bf16f2d7ef0cb805
Implemented Team hook scripts
10 files modified
296 ■■■■ changed files
docs/01_setup.mkd 14 ●●●●● patch | view | raw | blame | history
docs/04_releases.mkd 2 ●●● patch | view | raw | blame | history
src/com/gitblit/ConfigUserService.java 33 ●●●● patch | view | raw | blame | history
src/com/gitblit/FileUserService.java 23 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 130 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitServlet.java 14 ●●●● patch | view | raw | blame | history
src/com/gitblit/models/TeamModel.java 3 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.java 14 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditTeamPage.html 3 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditTeamPage.java 60 ●●●●● patch | view | raw | blame | history
docs/01_setup.mkd
@@ -196,6 +196,7 @@
        user = babaracus
        repository = topsecret.git
        mailingList = list@ateam.org
        postReceiveScript = sendmail
The `users.conf` file allows flexibility for adding new fields to a UserModel object that the original `users.properties` file does not afford without imposing the complexity of relying on an embedded SQL database. 
@@ -251,13 +252,14 @@
3. Script filenames must not have spaces!
4. Scripts must be explicitly specified to be executed, no scripts are *automatically* executed by name or extension.
5. 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`.
6. Scripts may also be specified per-repository in the repository's settings.
7. Globally specified scripts are excluded from the list of available scripts in a repository's settings
8. Globally specified scripts are executed first, in their listed order, followed by per-repository scripts, in their listed order.
9. A script may only be defined once in a pre-receive chain and once in a post-receive chain.
6. Scripts can be specified for a team.
7. Scripts may also be specified per-repository in the repository's settings.
8. Globally-specified scripts and team-specified scripts are excluded from the list of available scripts in a repository's settings
9. Globally-specified scripts are executed first, in their listed order; followed by team-specified scripts in their listed order by alphabetical team order; followed by per-repository scripts, in their listed order.
10. A script may only be defined once in a pre-receive chain and once in a post-receive chain.
You may execute the same script on pre-receive and post-receive, just not multiple times within a pre-receive or post-receive event.
10. Gitblit does not differentiate between what can be a pre-receive script and what can be a post-receive script.
11. If a script *returns false* then the hook chain is aborted and none of the subsequent scripts will execute.
11. Gitblit does not differentiate between what can be a pre-receive script and what can be a post-receive script.
12. If a script *returns false* then the 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.
docs/04_releases.mkd
@@ -5,7 +5,7 @@
- updated: Gitblit GO is now monolithic like the WAR build. (issue 30)  
This change helps adoption of GO in environments without an internet connection or with a restricted connection.
- added: Groovy 1.8.4 and a push hook script mechanism.  Hook scripts can be set per-repository or globally for all repositories.
- added: Groovy 1.8.4 and a push hook script mechanism.  Hook scripts can be set per-repository, pre-team, 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 =*  
src/com/gitblit/ConfigUserService.java
@@ -20,6 +20,7 @@
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -62,8 +63,12 @@
    private static final String REPOSITORY = "repository";
    private static final String ROLE = "role";
    private static final String MAILINGLIST = "mailingList";
    private static final String PRERECEIVE = "preReceiveScript";
    private static final String POSTRECEIVE = "postReceiveScript";
    private final File realmFile;
@@ -303,6 +308,7 @@
    public List<String> getAllTeamNames() {
        read();
        List<String> list = new ArrayList<String>(teams.keySet());
        Collections.sort(list);
        return list;
    }
@@ -328,6 +334,7 @@
        } catch (Throwable t) {
            logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
        }
        Collections.sort(list);
        return list;
    }
@@ -468,6 +475,7 @@
    public List<String> getAllUsernames() {
        read();
        List<String> list = new ArrayList<String>(users.keySet());
        Collections.sort(list);
        return list;
    }
@@ -493,6 +501,7 @@
        } catch (Throwable t) {
            logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
        }
        Collections.sort(list);
        return list;
    }
@@ -652,11 +661,22 @@
            }
            // null check on "final" mailing lists because JSON-sourced
            // TeamModel
            // can have a null users object
            // TeamModel can have a null users object
            if (model.mailingLists != null) {
                config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList<String>(
                        model.mailingLists));
            }
            // null check on "final" preReceiveScripts because JSON-sourced
            // TeamModel can have a null preReceiveScripts object
            if (model.preReceiveScripts != null) {
                config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts);
            }
            // null check on "final" postReceiveScripts because JSON-sourced
            // TeamModel can have a null postReceiveScripts object
            if (model.postReceiveScripts != null) {
                config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts);
            }
        }
@@ -724,7 +744,12 @@
                    team.addRepositories(Arrays.asList(config.getStringList(TEAM, teamname,
                            REPOSITORY)));
                    team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER)));
                    team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname, MAILINGLIST)));
                    team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname,
                            MAILINGLIST)));
                    team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
                            teamname, PRERECEIVE)));
                    team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
                            teamname, POSTRECEIVE)));
                    teams.put(team.name.toLowerCase(), team);
src/com/gitblit/FileUserService.java
@@ -20,6 +20,7 @@
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -334,6 +335,7 @@
            }
            list.add(user);
        }
        Collections.sort(list);
        return list;
    }
@@ -368,6 +370,7 @@
        } catch (Throwable t) {
            logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
        }
        Collections.sort(list);
        return list;
    }
@@ -619,11 +622,17 @@
                    List<String> repositories = new ArrayList<String>();
                    List<String> users = new ArrayList<String>();
                    List<String> mailingLists = new ArrayList<String>();
                    List<String> preReceive = new ArrayList<String>();
                    List<String> postReceive = new ArrayList<String>();
                    for (String role : roles) {
                        if (role.charAt(0) == '!') {
                            users.add(role.substring(1));
                        } else if (role.charAt(0) == '&') {
                                mailingLists.add(role.substring(1));
                            mailingLists.add(role.substring(1));
                        } else if (role.charAt(0) == '^') {
                            preReceive.add(role.substring(1));
                        } else if (role.charAt(0) == '%') {
                            postReceive.add(role.substring(1));
                        } else {
                            repositories.add(role);
                        }
@@ -656,6 +665,7 @@
    @Override
    public List<String> getAllTeamNames() {
        List<String> list = new ArrayList<String>(teams.keySet());
        Collections.sort(list);
        return list;
    }
@@ -691,6 +701,7 @@
        } catch (Throwable t) {
            logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
        }
        Collections.sort(list);
        return list;
    }
@@ -841,6 +852,16 @@
            sb.append(address);
            sb.append(',');
        }
        for (String script : model.preReceiveScripts) {
            sb.append('^');
            sb.append(script);
            sb.append(',');
        }
        for (String script : model.postReceiveScripts) {
            sb.append('%');
            sb.append(script);
            sb.append(',');
        }
        // trim trailing comma
        sb.setLength(sb.length() - 1);
        allUsers.remove("@" + teamname);
src/com/gitblit/GitBlit.java
@@ -28,11 +28,12 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -1444,13 +1445,12 @@
    }
    /**
     * Returns the list of all available Groovy push hook scripts that are not
     * already specified globally for all repositories. Script files must have
     * Returns the list of all Groovy push hook scripts. Script files must have
     * .groovy extension
     * 
     * @return list of available hook scripts
     */
    public List<String> getAvailableScripts() {
    public List<String> getAllScripts() {
        File groovyFolder = getGroovyScriptsFolder();
        File[] files = groovyFolder.listFiles(new FileFilter() {
            @Override
@@ -1458,55 +1458,115 @@
                return pathname.isFile() && pathname.getName().endsWith(".groovy");
            }
        });
        Set<String> globals = new HashSet<String>();
        String[] keys = { Keys.groovy.preReceiveScripts, Keys.groovy.postReceiveScripts };
        for (String key : keys) {
            for (String script : getStrings(key)) {
                if (script.endsWith(".groovy")) {
                    globals.add(script.substring(0, script.lastIndexOf('.')));
                } else {
                    globals.add(script);
                }
            }
        }
        // create list of available scripts by excluding scripts that are
        // globally specified
        List<String> scripts = new ArrayList<String>();
        if (files != null) {
            for (File file : files) {
                String script = file.getName().substring(0, file.getName().lastIndexOf('.'));
                if (!globals.contains(script)) {
                    scripts.add(script);
                }
                scripts.add(script);
            }
        }
        return scripts;
    }
    public List<String> getInheritedPreReceiveScripts(RepositoryModel repository) {
        Set<String> globals = new HashSet<String>();
    /**
     * Returns the list of pre-receive scripts the repository inherited from the
     * global settings and team affiliations.
     *
     * @param repository
     *            if null only the globally specified scripts are returned
     * @return a list of scripts
     */
    public List<String> getPreReceiveScriptsInherited(RepositoryModel repository) {
        Set<String> scripts = new LinkedHashSet<String>();
        // Globals
        for (String script : getStrings(Keys.groovy.preReceiveScripts)) {
            if (script.endsWith(".groovy")) {
                globals.add(script.substring(0, script.lastIndexOf('.')));
                scripts.add(script.substring(0, script.lastIndexOf('.')));
            } else {
                globals.add(script);
                scripts.add(script);
            }
        }
        return new ArrayList<String>(globals);
        // Team Scripts
        if (repository != null) {
            for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
                TeamModel team = userService.getTeamModel(teamname);
                scripts.addAll(team.preReceiveScripts);
            }
        }
        return new ArrayList<String>(scripts);
    }
    public List<String> getInheritedPostReceiveScripts(RepositoryModel repository) {
        Set<String> globals = new HashSet<String>();
    /**
     * Returns the list of all available Groovy pre-receive push hook scripts
     * that are not already inherited by the repository. Script files must have
     * .groovy extension
     *
     * @param repository
     *            optional parameter
     * @return list of available hook scripts
     */
    public List<String> getPreReceiveScriptsUnused(RepositoryModel repository) {
        Set<String> inherited = new TreeSet<String>(getPreReceiveScriptsInherited(repository));
        // create list of available scripts by excluding inherited scripts
        List<String> scripts = new ArrayList<String>();
        for (String script : getAllScripts()) {
            if (!inherited.contains(script)) {
                scripts.add(script);
            }
        }
        return scripts;
    }
    /**
     * Returns the list of post-receive scripts the repository inherited from
     * the global settings and team affiliations.
     *
     * @param repository
     *            if null only the globally specified scripts are returned
     * @return a list of scripts
     */
    public List<String> getPostReceiveScriptsInherited(RepositoryModel repository) {
        Set<String> scripts = new LinkedHashSet<String>();
        // Global Scripts
        for (String script : getStrings(Keys.groovy.postReceiveScripts)) {
            if (script.endsWith(".groovy")) {
                globals.add(script.substring(0, script.lastIndexOf('.')));
                scripts.add(script.substring(0, script.lastIndexOf('.')));
            } else {
                globals.add(script);
                scripts.add(script);
            }
        }
        return new ArrayList<String>(globals);
        // Team Scripts
        if (repository != null) {
            for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
                TeamModel team = userService.getTeamModel(teamname);
                scripts.addAll(team.postReceiveScripts);
            }
        }
        return new ArrayList<String>(scripts);
    }
    /**
     * Returns the list of unused Groovy post-receive push hook scripts that are
     * not already inherited by the repository. Script files must have .groovy
     * extension
     *
     * @param repository
     *            optional parameter
     * @return list of available hook scripts
     */
    public List<String> getPostReceiveScriptsUnused(RepositoryModel repository) {
        Set<String> inherited = new TreeSet<String>(getPostReceiveScriptsInherited(repository));
        // create list of available scripts by excluding inherited scripts
        List<String> scripts = new ArrayList<String>();
        for (String script : getAllScripts()) {
            if (!inherited.contains(script)) {
                scripts.add(script);
            }
        }
        return scripts;
    }
    /**
@@ -1572,7 +1632,7 @@
                setting.currentValue = settings.getString(key, "");
            }
        }
        settingsModel.pushScripts = getAvailableScripts();
        settingsModel.pushScripts = getAllScripts();
        return settingsModel;
    }
src/com/gitblit/GitServlet.java
@@ -99,9 +99,9 @@
    }
    /**
     * Transitional wrapper class to configure the JGit 1.2 GitFilter.
     * This GitServlet will probably be replaced by a GitFilter so that Gitblit
     * can serve Git repositories on the root URL and not a /git sub-url.
     * Transitional wrapper class to configure the JGit 1.2 GitFilter. This
     * GitServlet will probably be replaced by a GitFilter so that Gitblit can
     * serve Git repositories on the root URL and not a /git sub-url.
     * 
     * @author James Moger
     * 
@@ -160,9 +160,9 @@
         */
        @Override
        public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
            Set<String> scripts = new LinkedHashSet<String>();
            scripts.addAll(GitBlit.getStrings(Keys.groovy.preReceiveScripts));
            RepositoryModel repository = getRepositoryModel(rp);
            Set<String> scripts = new LinkedHashSet<String>();
            scripts.addAll(GitBlit.self().getPreReceiveScriptsInherited(repository));
            scripts.addAll(repository.preReceiveScripts);
            UserModel user = getUserModel(rp);
            runGroovy(repository, user, commands, scripts);
@@ -188,9 +188,9 @@
                logger.info("skipping post-receive hooks, no refs created, updated, or removed");
                return;
            }
            Set<String> scripts = new LinkedHashSet<String>();
            scripts.addAll(GitBlit.getStrings(Keys.groovy.postReceiveScripts));
            RepositoryModel repository = getRepositoryModel(rp);
            Set<String> scripts = new LinkedHashSet<String>();
            scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
            scripts.addAll(repository.postReceiveScripts);
            UserModel user = getUserModel(rp);
            runGroovy(repository, user, commands, scripts);
src/com/gitblit/models/TeamModel.java
@@ -16,6 +16,7 @@
package com.gitblit.models;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
@@ -37,6 +38,8 @@
    public final Set<String> users = new HashSet<String>();
    public final Set<String> repositories = new HashSet<String>();
    public final Set<String> mailingLists = new HashSet<String>();
    public final List<String> preReceiveScripts = new ArrayList<String>();
    public final List<String> postReceiveScripts = new ArrayList<String>();
    public TeamModel(String name) {
        this.name = name;
src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -121,8 +121,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().getPreReceiveScriptsUnused(repositoryModel)),
                new ChoiceRenderer<String>("", ""), 12, true);
        // post-receive palette
        if (repositoryModel.postReceiveScripts != null) {
@@ -130,8 +130,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().getPostReceiveScriptsUnused(repositoryModel)),
                new ChoiceRenderer<String>("", ""), 12, true);
        CompoundPropertyModel<RepositoryModel> model = new CompoundPropertyModel<RepositoryModel>(
                repositoryModel);
@@ -293,9 +293,11 @@
        form.add(teamsPalette);
        form.add(federationSetsPalette);
        form.add(preReceivePalette);
        form.add(new BulletListPanel("inheritedPreReceive", "inherited", GitBlit.self().getInheritedPreReceiveScripts(repositoryModel)));
        form.add(new BulletListPanel("inheritedPreReceive", "inherited", GitBlit.self()
                .getPreReceiveScriptsInherited(repositoryModel)));
        form.add(postReceivePalette);
        form.add(new BulletListPanel("inheritedPostReceive", "inherited", GitBlit.self().getInheritedPostReceiveScripts(repositoryModel)));
        form.add(new BulletListPanel("inheritedPostReceive", "inherited", GitBlit.self()
                .getPostReceiveScriptsInherited(repositoryModel)));
        form.add(new Button("save"));
        Button cancel = new Button("cancel") {
src/com/gitblit/wicket/pages/EditTeamPage.html
@@ -17,6 +17,9 @@
                <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><td colspan="2"><h3><wicket:message key="gb.hookScripts"></wicket:message> &nbsp;<small><wicket:message key="gb.hookScriptsDescription"></wicket:message></small></h3></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.preReceiveScripts"></wicket:message><p></p><span wicket:id="inheritedPreReceive"></span></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><p></p><span wicket:id="inheritedPostReceive"></span></th><td style="padding:2px;"><span wicket:id="postReceiveScripts"></span></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>
src/com/gitblit/wicket/pages/EditTeamPage.java
@@ -43,6 +43,7 @@
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.BulletListPanel;
@RequiresAdminRole
public class EditTeamPage extends RootSubPage {
@@ -73,7 +74,7 @@
        } else {
            super.setupPage(getString("gb.edit"), teamModel.name);
        }
        CompoundPropertyModel<TeamModel> model = new CompoundPropertyModel<TeamModel>(teamModel);
        List<String> repos = new ArrayList<String>();
@@ -84,17 +85,42 @@
            }
        }
        StringUtils.sortRepositorynames(repos);
        List<String> teamUsers = new ArrayList<String>(teamModel.users);
        Collections.sort(teamUsers);
        List<String> preReceiveScripts = new ArrayList<String>();
        List<String> postReceiveScripts = new ArrayList<String>();
        final String oldName = teamModel.name;
        // repositories palette
        final Palette<String> repositories = new Palette<String>("repositories",
                new ListModel<String>(new ArrayList<String>(teamModel.repositories)),
                new CollectionModel<String>(repos), new ChoiceRenderer<String>("", ""), 10, false);
        // users palette
        final Palette<String> users = new Palette<String>("users", new ListModel<String>(
                new ArrayList<String>(teamUsers)), new CollectionModel<String>(GitBlit.self()
                .getAllUsernames()), new ChoiceRenderer<String>("", ""), 10, false);
        // pre-receive palette
        if (teamModel.preReceiveScripts != null) {
            preReceiveScripts.addAll(teamModel.preReceiveScripts);
        }
        final Palette<String> preReceivePalette = new Palette<String>("preReceiveScripts",
                new ListModel<String>(preReceiveScripts), new CollectionModel<String>(GitBlit
                        .self().getPreReceiveScriptsUnused(null)), new ChoiceRenderer<String>("",
                        ""), 12, true);
        // post-receive palette
        if (teamModel.postReceiveScripts != null) {
            postReceiveScripts.addAll(teamModel.postReceiveScripts);
        }
        final Palette<String> postReceivePalette = new Palette<String>("postReceiveScripts",
                new ListModel<String>(postReceiveScripts), new CollectionModel<String>(GitBlit
                        .self().getPostReceiveScriptsUnused(null)), new ChoiceRenderer<String>("",
                        ""), 12, true);
        Form<TeamModel> form = new Form<TeamModel>("editForm", model) {
            private static final long serialVersionUID = 1L;
@@ -147,7 +173,25 @@
                    teamModel.mailingLists.clear();
                    teamModel.mailingLists.addAll(list);
                }
                // pre-receive scripts
                List<String> preReceiveScripts = new ArrayList<String>();
                Iterator<String> pres = preReceivePalette.getSelectedChoices();
                while (pres.hasNext()) {
                    preReceiveScripts.add(pres.next());
                }
                teamModel.preReceiveScripts.clear();
                teamModel.preReceiveScripts.addAll(preReceiveScripts);
                // post-receive scripts
                List<String> postReceiveScripts = new ArrayList<String>();
                Iterator<String> post = postReceivePalette.getSelectedChoices();
                while (post.hasNext()) {
                    postReceiveScripts.add(post.next());
                }
                teamModel.postReceiveScripts.clear();
                teamModel.postReceiveScripts.addAll(postReceiveScripts);
                try {
                    GitBlit.self().updateTeamModel(oldName, teamModel, isCreate);
                } catch (GitBlitException e) {
@@ -173,8 +217,14 @@
        mailingLists = new Model<String>(teamModel.mailingLists == null ? ""
                : StringUtils.flattenStrings(teamModel.mailingLists, " "));
        form.add(new TextField<String>("mailingLists", mailingLists));
        form.add(repositories);
        form.add(preReceivePalette);
        form.add(new BulletListPanel("inheritedPreReceive", "inherited", GitBlit.self()
                .getPreReceiveScriptsInherited(null)));
        form.add(postReceivePalette);
        form.add(new BulletListPanel("inheritedPostReceive", "inherited", GitBlit.self()
                .getPostReceiveScriptsInherited(null)));
        form.add(new Button("save"));
        Button cancel = new Button("cancel") {