James Moger
2011-12-19 6cc1d4cf3605719af3f9a37ee09cb02519be3571
Server-side hook script selection. Documentation.
9 files modified
183 ■■■■■ changed files
distrib/gitblit.properties 9 ●●●●● patch | view | raw | blame | history
docs/01_setup.mkd 28 ●●●● patch | view | raw | blame | history
resources/bootstrap.gb.css 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 55 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitServlet.java 26 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 5 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.html 10 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.java 41 ●●●●● patch | view | raw | blame | history
test-gitblit.properties 4 ●●●● patch | view | raw | blame | history
distrib/gitblit.properties
@@ -36,6 +36,7 @@
# Use forward slashes even on Windows!!
# e.g. c:/groovy
#
# RESTART REQUIRED
# SINCE 0.8.0
groovy.scriptsFolder = groovy
@@ -46,6 +47,9 @@
# push in this script based on the repository and branch the push is attempting
# to change.
#
# Script names are case-sensitive on case-sensitive file systems.  You may omit
# the traditional ".groovy" from this list if your file extension is ".groovy"
#
# NOTE:
# These scripts are only executed when pushing to *Gitblit*, not to other Git
# tooling you may be using.  Also note that these scripts are shared between
@@ -54,6 +58,7 @@
# *repository* variable.
#
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 0.8.0
groovy.preReceiveScripts =
@@ -61,6 +66,9 @@
#
# These scripts execute AFTER an incoming push has been applied to a repository.
# You might trigger a continuous-integration build here or send a notification.
#
# Script names are case-sensitive on case-sensitive file systems.  You may omit
# the traditional ".groovy" from this list if your file extension is ".groovy"
#
# NOTE:
# These scripts are only executed when pushing to *Gitblit*, not to other Git
@@ -70,6 +78,7 @@
# *repository* variable.
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 0.8.0
groovy.postReceiveScripts =
docs/01_setup.mkd
@@ -232,13 +232,33 @@
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.
Your Groovy scripts should be stored in the *groovy.scriptsFolder* as specified in `gitblit.properties` or `web.xml`.
### Rules & Requirements
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`.
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.
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.
Scripts must be explicitly specified to be executed.  A script can be run on *all repositories* by adding the script file name to *groovy.preReceiveScripts* or *groovy.postReceiveScripts* in `gitblit.properties` or `web.xml`. Alternatively, you may specify per-repository scripts in the repository settings.  Global/shared scripts are executed first in their listed order, followed by per-repository scripts in their listed order.
Some primitive sample scripts are included in the GO and WAR distributions to show you how you can tap into Gitblit with the provided bound variables.
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
Pre-Receive scripts execute after the pushed objects have all been written to the Git repository but before the refs have been updated to point to these new objects.
This is the appropriate point to block a push and is how many Git tools implement branch-write permissions.
### Post-Receive
Post-Receive scripts execute after all refs have been updated.
This is the appropriate point to trigger continuous integration builds or send email notifications, etc.
## Client Setup and Configuration
### Https with Self-Signed Certificates
You must tell Git/JGit not to verify the self-signed certificate in order to perform any remote Git operations.
resources/bootstrap.gb.css
@@ -11,6 +11,11 @@
    margin-bottom: 10px;
}
.settings h3 {
    margin-bottom: 0.5em;
    border-bottom: 2px solid #000080 !important;
}
.page-header h1, .page-header h2 {
    color: #0069D6;
}
src/com/gitblit/GitBlit.java
@@ -27,9 +27,11 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -293,6 +295,16 @@
     */
    public static File getProposalsFolder() {
        return getFileOrFolder(Keys.federation.proposalsFolder, "proposals");
    }
    /**
     * Returns the path of the Groovy folder. This method checks to see if
     * Gitblit is running on a cloud service and may return an adjusted path.
     *
     * @return the Groovy scripts folder path
     */
    public static File getGroovyScriptsFolder() {
        return getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
    }
    /**
@@ -1426,6 +1438,48 @@
    }
    /**
     * Returns the list of all available Groovy push hook scripts that are not
     * already specified globally for all repositories. Script files must have
     * .groovy extension
     *
     * @return list of available hook scripts
     */
    public List<String> getAvailableScripts() {
        File groovyFolder = getGroovyScriptsFolder();
        File[] files = groovyFolder.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                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);
                }
            }
        }
        return scripts;
    }
    /**
     * Notify the administrators by email.
     * 
     * @param subject
@@ -1488,6 +1542,7 @@
                setting.currentValue = settings.getString(key, "");
            }
        }
        settingsModel.pushScripts = getAvailableScripts();
        return settingsModel;
    }
src/com/gitblit/GitServlet.java
@@ -26,7 +26,8 @@
import java.io.OutputStreamWriter;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
@@ -68,6 +69,8 @@
    private GroovyScriptEngine gse;
    private File groovyDir;
    /**
     * Configure the servlet from Gitblit's configuration.
     */
@@ -83,9 +86,9 @@
    @Override
    public void init(ServletConfig config) throws ServletException {
        String groovyRoot = GitBlit.getString(Keys.groovy.scriptsFolder, "groovy");
        groovyDir = GitBlit.getGroovyScriptsFolder();
        try {
            gse = new GroovyScriptEngine(groovyRoot);
            gse = new GroovyScriptEngine(groovyDir.getAbsolutePath());
        } catch (IOException e) {
            throw new ServletException("Failed to instantiate Groovy Script Engine!", e);
        }
@@ -127,7 +130,8 @@
         */
        @Override
        public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
            List<String> scripts = GitBlit.getStrings(Keys.groovy.preReceiveScripts);
            Set<String> scripts = new LinkedHashSet<String>();
            scripts.addAll(GitBlit.getStrings(Keys.groovy.preReceiveScripts));
            RepositoryModel repository = getRepositoryModel(rp);
            scripts.addAll(repository.preReceiveScripts);
            UserModel user = getUserModel(rp);
@@ -154,7 +158,8 @@
                logger.info("skipping post-receive hooks, no refs created, updated, or removed");
                return;
            }
            List<String> scripts = GitBlit.getStrings(Keys.groovy.postReceiveScripts);
            Set<String> scripts = new LinkedHashSet<String>();
            scripts.addAll(GitBlit.getStrings(Keys.groovy.postReceiveScripts));
            RepositoryModel repository = getRepositoryModel(rp);
            scripts.addAll(repository.postReceiveScripts);
            UserModel user = getUserModel(rp);
@@ -204,7 +209,7 @@
         * @param scripts
         */
        protected void runGroovy(RepositoryModel repository, UserModel user,
                Collection<ReceiveCommand> commands, List<String> scripts) {
                Collection<ReceiveCommand> commands, Set<String> scripts) {
            if (scripts == null || scripts.size() == 0) {
                // no Groovy scripts to execute
                return;
@@ -221,6 +226,15 @@
                if (StringUtils.isEmpty(script)) {
                    continue;
                }
                // allow script to be specified without .groovy extension
                // this is easier to read in the settings
                File file = new File(groovyDir, script);
                if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) {
                    file = new File(groovyDir, script + ".groovy");
                    if (file.exists()) {
                        script = file.getName();
                    }
                }
                try {
                    Object result = gse.run(script, binding);
                    if (result instanceof Boolean) {
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -197,4 +197,7 @@
gb.emptyRepository = empty repository
gb.repositoryUrl = repository url
gb.mailRecipients = mail recipients
gb.mailRecipientsDescription = space-delimited, used by sendemail Groovy hook
gb.mailRecipientsDescription = space-delimited, used by sendemail Groovy hook
gb.preReceiveScripts = pre-receive scripts
gb.postReceiveScripts = post-receive scripts
gb.groovyHookScripts = hook scripts
src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -9,7 +9,8 @@
    <!-- Repository Table -->
    <form style="padding-top:5px;" wicket:id="editForm">
        <table class="plain">
            <tbody>
            <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.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>
@@ -22,13 +23,16 @@
                <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><td colspan="2"><hr></hr></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>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.permittedTeams"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
                <tr><td colspan="2"><hr></hr></td></tr>
                <tr><td colspan="2"><h3><wicket:message key="gb.federation"></wicket:message></h3></td></tr>
                <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span6" wicket:id="federationStrategy" tabindex="14" /></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>
                <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>
            </tbody>
        </table>
src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -80,6 +80,9 @@
        List<String> federationSets = new ArrayList<String>();
        List<String> repositoryUsers = new ArrayList<String>();
        List<String> repositoryTeams = new ArrayList<String>();
        List<String> preReceiveScripts = new ArrayList<String>();
        List<String> postReceiveScripts = new ArrayList<String>();
        if (isCreate) {
            super.setupPage(getString("gb.newRepository"), "");
        } else {
@@ -101,13 +104,29 @@
        // teams palette
        final Palette<String> teamsPalette = new Palette<String>("teams", new ListModel<String>(
                repositoryTeams), new CollectionModel<String>(GitBlit.self().getAllTeamnames()),
                new ChoiceRenderer<String>("", ""), 10, false);
                new ChoiceRenderer<String>("", ""), 5, false);
        // federation sets palette
        List<String> sets = GitBlit.getStrings(Keys.federation.sets);
        final Palette<String> federationSetsPalette = new Palette<String>("federationSets",
                new ListModel<String>(federationSets), new CollectionModel<String>(sets),
                new ChoiceRenderer<String>("", ""), 10, false);
                new ChoiceRenderer<String>("", ""), 5, false);
        // pre-receive palette
        if (repositoryModel.preReceiveScripts != null) {
            preReceiveScripts.addAll(repositoryModel.preReceiveScripts);
        }
        final Palette<String> preReceivePalette = new Palette<String>("preReceiveScripts",
                new ListModel<String>(preReceiveScripts), new CollectionModel<String>(GitBlit
                        .self().getAvailableScripts()), new ChoiceRenderer<String>("", ""), 12, true);
        // post-receive palette
        if (repositoryModel.postReceiveScripts != null) {
            postReceiveScripts.addAll(repositoryModel.postReceiveScripts);
        }
        final Palette<String> postReceivePalette = new Palette<String>("postReceiveScripts",
                new ListModel<String>(postReceiveScripts), new CollectionModel<String>(GitBlit
                        .self().getAvailableScripts()), new ChoiceRenderer<String>("", ""), 12, true);
        CompoundPropertyModel<RepositoryModel> model = new CompoundPropertyModel<RepositoryModel>(
                repositoryModel);
@@ -179,6 +198,22 @@
                        repositoryModel.mailRecipients = list;
                    }
                    // pre-receive scripts
                    List<String> preReceiveScripts = new ArrayList<String>();
                    Iterator<String> pres = preReceivePalette.getSelectedChoices();
                    while (pres.hasNext()) {
                        preReceiveScripts.add(pres.next());
                    }
                    repositoryModel.preReceiveScripts = preReceiveScripts;
                    // post-receive scripts
                    List<String> postReceiveScripts = new ArrayList<String>();
                    Iterator<String> post = postReceivePalette.getSelectedChoices();
                    while (post.hasNext()) {
                        postReceiveScripts.add(post.next());
                    }
                    repositoryModel.postReceiveScripts = postReceiveScripts;
                    // save the repository
                    GitBlit.self().updateRepositoryModel(oldName, repositoryModel, isCreate);
@@ -246,6 +281,8 @@
        form.add(usersPalette);
        form.add(teamsPalette);
        form.add(federationSetsPalette);
        form.add(preReceivePalette);
        form.add(postReceivePalette);
        form.add(new Button("save"));
        Button cancel = new Button("cancel") {
test-gitblit.properties
@@ -6,8 +6,8 @@
git.searchRepositoriesSubfolders = true
git.enableGitServlet = true
groovy.scriptsFolder = groovy
groovy.preReceiveScripts = blockpush.groovy
groovy.postReceiveScripts = sendemail.groovy jenkins.groovy
groovy.preReceiveScripts = blockpush
groovy.postReceiveScripts = sendemail jenkins
web.authenticateViewPages = false
web.authenticateAdminPages = true
web.allowCookieAuthentication = true