From: James Moger Date: Sat, 17 Dec 2011 02:14:48 +0000 (-0500) Subject: Groovy push hooks X-Git-Tag: v0.8.0~75 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=fa54bec1d90ff0baa8a509bc68acb6a92bb817a8;p=gitblit.git Groovy push hooks --- diff --git a/.classpath b/.classpath index 32f4ced1..e6b336c6 100644 --- a/.classpath +++ b/.classpath @@ -97,5 +97,10 @@ + + + + + diff --git a/build.xml b/build.xml index 8daebf16..bd25e6c4 100644 --- a/build.xml +++ b/build.xml @@ -186,6 +186,14 @@ + + + + + + + + @@ -343,7 +351,15 @@ - + + + + + + + + + @@ -922,4 +938,4 @@ - + diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index 537f9b67..18566d8a 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -28,6 +28,51 @@ git.searchRepositoriesSubfolders = true # SINCE 0.5.0 git.enableGitServlet = true +# +# Groovy Integration +# + +# Location of Groovy scripts to use for Pre and Post receive hooks. +# Use forward slashes even on Windows!! +# e.g. c:/groovy +# +# SINCE 0.8.0 +groovy.scriptsFolder = groovy + +# Scripts to execute on Pre-Receive. +# +# These scripts execute after an incoming push has been parsed and validated +# but BEFORE the changes are applied to the repository. You might reject a +# push in this script based on the repository and branch the push is attempting +# to change. +# +# 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 +# repositories. These are NOT repository-specific scripts! Within the script +# you may customize the control-flow for a specific repository by checking the +# *repository* variable. +# +# SPACE-DELIMITED +# SINCE 0.8.0 +groovy.preReceiveScripts = + +# Scripts to execute on Post-Receive. +# +# 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. +# +# 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 +# repositories. These are NOT repository-specific scripts! Within the script +# you may customize the control-flow for a specific repository by checking the +# *repository* variable. +# +# SPACE-DELIMITED +# SINCE 0.8.0 +groovy.postReceiveScripts = + # # Authentication Settings # @@ -385,6 +430,17 @@ mail.fromAddress = # SINCE 0.6.0 mail.adminAddresses = +# List of email addresses for sending commit email notifications. +# +# This key currently requires use of the sendemail.groovy hook script. +# If you set sendemail.groovy in *groovy.postReceiveScripts* then email +# notifications for all repositories (regardless of access restrictions) +# will be sent to these addresses. +# +# SPACE-DELIMITED +# SINCE 0.8.0 +mail.mailingLists = + # # Federation Settings # SINCE 0.6.0 diff --git a/docs/00_index.mkd b/docs/00_index.mkd index 11d571d8..d1c08fe6 100644 --- a/docs/00_index.mkd +++ b/docs/00_index.mkd @@ -66,6 +66,7 @@ Administrators can create and manage all repositories, user accounts, and teams ### Integration with Your Infrastructure +- Groovy push hook scripts - Pluggable user service mechanism for custom authentication, authorization, and user management - Rich RSS feeds - JSON-based RPC mechanism diff --git a/docs/01_features.mkd b/docs/01_features.mkd index 4172f4e1..39fbfaac 100644 --- a/docs/01_features.mkd +++ b/docs/01_features.mkd @@ -11,6 +11,7 @@ - RSS/JSON RPC interface - Java/Swing Gitblit Manager tool - Gitweb inspired web UI +- Groovy pre- and post- push hook scripts, per-repository or globally for all repositories - Administrators may create, edit, rename, or delete repositories through the web UI or RPC interface - Administrators may create, edit, rename, or delete users through the web UI or RPC interface - Administrators may create, edit, rename, or delete teams through the web UI or RPC interface @@ -40,10 +41,10 @@ ## Limitations - HTTP/HTTPS are the only supported Git protocols -- Access controls are not path-based, they are repository-based +- Built-in access controls are not path-based, they are repository-based. - Only Administrators can create, rename or delete repositories - Only Administrators can create, modify or delete users -- Commit hooks are not supported +- Only Administrators can create, modify or delete teams - Native Git may be needed to periodically run git-gc as [JGit][jgit] does not fully support the git-gc featureset. ### Caveats diff --git a/docs/01_setup.mkd b/docs/01_setup.mkd index 3256d1b3..27a4e6af 100644 --- a/docs/01_setup.mkd +++ b/docs/01_setup.mkd @@ -3,9 +3,11 @@ 1. Download [Gitblit WAR %VERSION%](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%) to the webapps folder of your servlet container. 2. You may have to manually extract the WAR (zip file) to a folder within your webapps folder. 3. Copy the `WEB-INF/users.conf` file to a location outside the webapps folder that is accessible by your servlet container. +Optionally copy the example hook scripts in `WEB-INF/groovy` to a location outside the webapps folder that is accesible by your servlet container. 4. The Gitblit webapp is configured through its `web.xml` file. Open `web.xml` in your favorite text editor and make sure to review and set: - <context-parameter> *git.repositoryFolder* (set the full path to your repositories folder) + - <context-parameter> *groovy.scriptsFolder* (set the full path to your Groovy hook scripts folder) - <context-parameter> *realm.userService* (set the full path to `users.conf`) 5. You may have to restart your servlet container. 6. Open your browser to or whatever the url should be. @@ -19,6 +21,7 @@ Open `web.xml` in your favorite text editor and make sure to review and set: 2. The server itself is configured through a simple text file. Open `gitblit.properties` in your favorite text editor and make sure to review and set: - *git.repositoryFolder* (path may be relative or absolute) + - *groovy.scriptsFolder* (path may be relative or absolute) - *server.tempFolder* (path may be relative or absolute) - *server.httpPort* and *server.httpsPort* - *server.httpBindInterface* and *server.httpsBindInterface* @@ -115,12 +118,13 @@ Backup your `web.properties` file (if you have one, these are the setting overri 2. Backup your `users.properties` file *(if it is located in the Gitblit GO folder)* OR Backup your `users.conf` file *(if it is located in the Gitblit GO folder)* -3. Unzip Gitblit GO to a new folder -4. Overwrite the `gitblit.properties` file with your backup -5. Overwrite the `users.properties` file with your backup *(if it was located in the Gitblit GO folder)* +3. Backup your Groovy hook scripts +4. Unzip Gitblit GO to a new folder +5. Overwrite the `gitblit.properties` file with your backup +6. Overwrite the `users.properties` file with your backup *(if it was located in the Gitblit GO folder)* OR Overwrite the `users.conf` file with your backup *(if it was located in the Gitblit GO folder)* -6. Review and optionally apply any new settings as indicated in the [release log](releases.html). +7. Review and optionally apply any new settings as indicated in the [release log](releases.html). #### Upgrading Windows Service You may need to delete your old service definition and install a new one depending on what has changed in the release. @@ -197,10 +201,10 @@ The `users.conf` file allows flexibility for adding new fields to a UserModel ob ### Administering Users (users.properties, Gitblit v0.5.0 - v0.7.0) All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`. Your file extension must be *.properties* in order to use this user service. -The format of `users.properties` follows Jetty's convention for HashRealms: +The format of `users.properties` loosely follows Jetty's convention for HashRealms: - username,password,role1,role2,role3... - @teamname,!username1,!username2,!username3,repository1,repository2,repository3... + username=password,role1,role2,role3... + @teamname=!username1,!username2,!username3,repository1,repository2,repository3... #### Usernames Usernames must be unique and are case-insensitive. @@ -220,6 +224,21 @@ You may use your own custom *com.gitblit.IUserService* implementation by specify Your user service class must be on Gitblit's classpath and must have a public default constructor. Please see the following interface definition [com.gitblit.IUserService](https://github.com/gitblit/gitblit/blob/master/src/com/gitblit/IUserService.java). +## Groovy Hook Scripts + +*SINCE 0.8.0* + +The preferred hook mechanism is Groovy. This mechanism only executes when pushing to Gitblit, not when pushing to some other Git tooling in your stack. + +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`. + +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. +Hook contributions and improvements are welcome. + ## 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. diff --git a/docs/04_design.mkd b/docs/04_design.mkd index 3fd13d47..e16a9b11 100644 --- a/docs/04_design.mkd +++ b/docs/04_design.mkd @@ -35,6 +35,7 @@ The following dependencies are automatically downloaded by Gitblit GO (or alread - [jdom](http://www.jdom.org) (Apache-style JDOM license) - [google-gson](http://code.google.com/google-gson) (Apache 2.0) - [javamail](http://kenai.com/projects/javamail) (CDDL-1.0, BSD, GPL-2.0, GNU-Classpath) +- [Groovy](http://groovy.codehaus.org) (Apache 2.0) ### Other Build Dependencies - [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed) diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd index 9a4e4a83..75ce1459 100644 --- a/docs/04_releases.mkd +++ b/docs/04_releases.mkd @@ -3,6 +3,13 @@ ### Current Release **%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%* +- 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. + **New:** *mail.mailingLists =* - 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. **New:** *realm.userService = users.conf* @@ -21,7 +28,6 @@ This user service implementation allows for serialization and deserialization of - 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: 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 **0.7.0** ([go](http://code.google.com/p/gitblit/downloads/detail?name=gitblit-0.7.0.zip) | [war](http://code.google.com/p/gitblit/downloads/detail?name=gitblit-0.7.0.war) | [fedclient](http://code.google.com/p/gitblit/downloads/detail?name=fedclient-0.7.0.zip) | [manager](http://code.google.com/p/gitblit/downloads/detail?name=manager-0.7.0.zip) | [api](http://code.google.com/p/gitblit/downloads/detail?name=gbapi-0.7.0.zip)) based on [JGit 1.1.0 (201109151100-r)][jgit]   *released 2011-11-11* diff --git a/docs/05_roadmap.mkd b/docs/05_roadmap.mkd index e9d2dcbe..9be6372e 100644 --- a/docs/05_roadmap.mkd +++ b/docs/05_roadmap.mkd @@ -37,7 +37,10 @@ This list is volatile. ### IDEAS -* Gitblit: consider user-subscribed email notifications for a repository branch +* Gitblit: implement branch permission controls as Groovy pre-receive script. +*Maintain permissions text file similar to a gitolite configuration file or svn authz file.* +* Gitblit: consider user-subscribed email notifications for a repository branch as a built-in feature. +*There is a sample Groovy post-receive hook script which can send emails.* * Gitblit: aggregate RSS feeds by tag or subfolder * Gitblit: Consider creating more Git model objects and exposing them via the JSON RPC interface to allow inspection/retrieval of Git commits, Git trees, etc from Gitblit. * Gitblit: Stronger ticgit integration (issue 8) diff --git a/groovy/blockpush.groovy b/groovy/blockpush.groovy new file mode 100644 index 00000000..eac7d3f5 --- /dev/null +++ b/groovy/blockpush.groovy @@ -0,0 +1,88 @@ +/* + * 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. + */ +import java.text.MessageFormat; + +import com.gitblit.GitBlit +import com.gitblit.models.RepositoryModel +import com.gitblit.models.UserModel +import org.eclipse.jgit.transport.ReceiveCommand +import org.eclipse.jgit.transport.ReceiveCommand.Result +import org.slf4j.Logger + +/** + * Sample Gitblit Pre-Receive Hook: blockpush + * + * This script could and perhaps should be further developed to provide + * a full repository-branch permissions system similar to gitolite or gitosis. + * + * The Pre-Receive hook is executed after an incoming push has been parsed, + * validated, and objects have been written but BEFORE the refs are updated. + * This is the appropriate point to block a push for some reason. + * + * This script is only executed when pushing to *Gitblit*, not to other Git + * tooling you may be using. + * + * If this script is specified in *groovy.preReceiveScripts* of gitblit.properties + * or web.xml then it will be executed by any repository when it receives a + * push. If you choose to share your script then you may have to consider + * tailoring control-flow based on repository access restrictions. + * + * Scripts may also be specified per-repository in the repository settings page. + * Shared scripts will be excluded from this list of available scripts. + * + * This script is dynamically reloaded and it is executed within it's own + * exception handler so it will not crash another script nor crash Gitblit. + * + * If you want this hook script to fail and abort all subsequent scripts in the + * chain, "return false" at the appropriate failure points. + * + * Bound Variables: + * gitblit Gitblit Server com.gitblit.GitBlit + * repository Gitblit Repository com.gitblit.models.RepositoryModel + * user Gitblit User com.gitblit.models.UserModel + * commands JGit commands Collection + * url Base url for Gitblit String + * log Logger instance org.slf4j.Logger + * + */ + +// Indicate we have started the script +logger.info("blockpush hook triggered by ${user.username} for ${repository.name}: checking ${commands.size} commands") + +/* + * Example rejection of pushes to the master branch of example.git + */ +def blocked = false +switch (repository.name) { + case "ex@mple.git": + for (ReceiveCommand command : commands) { + def updatedRef = command.refName + if (updatedRef.equals("refs/heads/master")) { + // to reject a command set it's result to anything other than Result.NOT_ATTEMPTED + command.setResult(Result.REJECTED_OTHER_REASON, "You are not permitted to write to ${repository.name}:${updatedRef}") + blocked = true + } + } + break + + default: + break +} + +if (blocked) { + // return false to break the push hook chain + return false +} \ No newline at end of file diff --git a/groovy/jenkins.groovy b/groovy/jenkins.groovy new file mode 100644 index 00000000..df4e588f --- /dev/null +++ b/groovy/jenkins.groovy @@ -0,0 +1,71 @@ +/* + * 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. + */ +import com.gitblit.GitBlit +import com.gitblit.Keys +import com.gitblit.models.RepositoryModel +import com.gitblit.models.UserModel +import com.gitblit.utils.JGitUtils +import org.eclipse.jgit.lib.Repository +import org.eclipse.jgit.revwalk.RevCommit +import org.eclipse.jgit.transport.ReceiveCommand +import org.eclipse.jgit.transport.ReceiveCommand.Result +import org.slf4j.Logger + +/** + * Sample Gitblit Post-Receive Hook: jenkins + * + * 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 + * integration build or to send a notification. + * + * This script is only executed when pushing to *Gitblit*, not to other Git + * tooling you may be using. + * + * If this script is specified in *groovy.postReceiveScripts* of gitblit.properties + * or web.xml then it will be executed by any repository when it receives a + * push. If you choose to share your script then you may have to consider + * tailoring control-flow based on repository access restrictions. + * + * Scripts may also be specified per-repository in the repository settings page. + * Shared scripts will be excluded from this list of available scripts. + * + * This script is dynamically reloaded and it is executed within it's own + * exception handler so it will not crash another script nor crash Gitblit. + * + * Bound Variables: + * gitblit Gitblit Server com.gitblit.GitBlit + * repository Gitblit Repository com.gitblit.models.RepositoryModel + * user Gitblit User com.gitblit.models.UserModel + * commands JGit commands Collection + * url Base url for Gitblit String + * logger Logger instance org.slf4j.Logger + * + */ +// Indicate we have started the script +logger.info("jenkins hook triggered by ${user.username} for ${repository.name}") + +// This script requires Jenkins Git plugin 1.1.14 or later +// http://kohsuke.org/2011/12/01/polling-must-die-triggering-jenkins-builds-from-a-git-hook/ + +// define your jenkins url here or set groovy.jenkinsServer in +// gitblit.properties or web.xml +def jenkinsUrl = gitblit.getString("groovy.jenkinsServer", "http://yourserver/jenkins") + +// define the trigger url +def triggerUrl = jenkinsUrl + "/git/notifyCommit?url=" + url + "/git/" + repository.name + +// trigger the build +new URL(triggerUrl).getContent() diff --git a/groovy/sendemail.groovy b/groovy/sendemail.groovy new file mode 100644 index 00000000..1ba72a8a --- /dev/null +++ b/groovy/sendemail.groovy @@ -0,0 +1,134 @@ +/* + * 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. + */ +import com.gitblit.GitBlit +import com.gitblit.Keys +import com.gitblit.models.RepositoryModel +import com.gitblit.models.UserModel +import com.gitblit.utils.JGitUtils +import org.eclipse.jgit.lib.Repository +import org.eclipse.jgit.lib.Config +import org.eclipse.jgit.revwalk.RevCommit +import org.eclipse.jgit.transport.ReceiveCommand +import org.eclipse.jgit.transport.ReceiveCommand.Result +import org.slf4j.Logger + +/** + * Sample Gitblit Post-Receive Hook: sendemail + * + * 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 + * integration build or to send a notification. + * + * This script is only executed when pushing to *Gitblit*, not to other Git + * tooling you may be using. + * + * If this script is specified in *groovy.postReceiveScripts* of gitblit.properties + * or web.xml then it will be executed by any repository when it receives a + * push. If you choose to share your script then you may have to consider + * tailoring control-flow based on repository access restrictions. + * + * Scripts may also be specified per-repository in the repository settings page. + * Shared scripts will be excluded from this list of available scripts. + * + * This script is dynamically reloaded and it is executed within it's own + * exception handler so it will not crash another script nor crash Gitblit. + * + * If you want this hook script to fail and abort all subsequent scripts in the + * chain, "return false" at the appropriate failure points. + * + * Bound Variables: + * gitblit Gitblit Server com.gitblit.GitBlit + * repository Gitblit Repository com.gitblit.models.RepositoryModel + * user Gitblit User com.gitblit.models.UserModel + * commands JGit commands Collection + * url Base url for Gitblit String + * logger Logger instance org.slf4j.Logger + * + */ + +// Indicate we have started the script +logger.info("sendemail hook triggered by ${user.username} for ${repository.name}") + +/* + * Primitive example email notification with example repository-specific checks. + * 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 +Config config = r.getConfig() +def mailinglist = config.getString("hooks", null, "mailinglist") +def emailprefix = config.getString("hooks", null, "emailprefix") + +// set default values +def toAddresses = [] +if (emailprefix == null) + emailprefix = "[Gitblit]" + +if (mailinglist != null) { + 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)) + +// special custom cases +switch(repository.name) { + case "ex@mple.git": + toAddresses.add "dev-team@somewhere.com" + toAddresses.add "qa-team@somewhere.com" + break + default: + break +} + +// get the create/update commits from the repository to build message content +def commits = [] +for (ReceiveCommand command:commands) { + switch (command.type) { + case ReceiveCommand.Type.UPDATE: + case ReceiveCommand.Type.CREATE: + RevCommit commit = JGitUtils.getCommit(r, command.newId.name) + commits.add(commit) + break + + default: + break + } +} +// close the repository reference +r.close() + +// build a link to the summary page, either mounted or parameterized +def summaryUrl +if (gitblit.getBoolean(Keys.web.mountParameters, true)) + summaryUrl = url + "/summary/" + repository.name.replace("/", gitblit.getString(Keys.web.forwardSlashCharacter, "/")) +else + summaryUrl = url + "/summary?r=" + repository.name + +// create a simple commits table +def table = commits.collect { it.id.name[0..8] + " " + it.authorIdent.name.padRight(20, " ") + it.shortMessage }.join("\n") + +// create the message body +def msg = """${user.username} pushed ${commits.size} commits to ${repository.name} +${summaryUrl} + +${table}""" + +// tell Gitblit to send the message (Gitblit filters duplicate addresses) +gitblit.notifyUsers("${emailprefix} ${user.username} pushed ${commits.size} commits => ${repository.name}", msg, toAddresses) \ No newline at end of file diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index 13dc3fa5..11454f30 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -557,6 +557,7 @@ public class GitBlit implements ServletContextListener { public boolean setRepositoryTeams(RepositoryModel repository, List repositoryTeams) { return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams); } + /** * Updates the TeamModel object for the specified name. * @@ -564,7 +565,8 @@ public class GitBlit implements ServletContextListener { * @param team * @param isCreate */ - public void updateTeamModel(String teamname, TeamModel team, boolean isCreate) throws GitBlitException { + public void updateTeamModel(String teamname, TeamModel team, boolean isCreate) + throws GitBlitException { if (!teamname.equalsIgnoreCase(team.name)) { if (userService.getTeamModel(team.name) != null) { throw new GitBlitException(MessageFormat.format( @@ -576,7 +578,7 @@ public class GitBlit implements ServletContextListener { throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!"); } } - + /** * Delete the team object with the specified teamname * @@ -725,6 +727,10 @@ public class GitBlit implements ServletContextListener { "gitblit", null, "federationSets"))); model.isFederated = getConfig(config, "isFederated", false); model.origin = config.getString("remote", "origin", "url"); + model.preReceiveScripts = new ArrayList(Arrays.asList(config.getStringList( + "gitblit", null, "preReceiveScript"))); + model.postReceiveScripts = new ArrayList(Arrays.asList(config.getStringList( + "gitblit", null, "postReceiveScript"))); } r.close(); return model; @@ -944,6 +950,8 @@ public class GitBlit implements ServletContextListener { config.setString("gitblit", null, "federationStrategy", repository.federationStrategy.name()); config.setBoolean("gitblit", null, "isFederated", repository.isFederated); + config.setStringList("gitblit", null, "preReceiveScript", repository.preReceiveScripts); + config.setStringList("gitblit", null, "postReceiveScript", repository.postReceiveScripts); try { config.save(); } catch (IOException e) { @@ -1426,6 +1434,37 @@ public class GitBlit implements ServletContextListener { } } + /** + * Notify users by email of something. + * + * @param subject + * @param message + * @param toAddresses + */ + public void notifyUsers(String subject, String message, ArrayList toAddresses) { + this.notifyUsers(subject, message, toAddresses.toArray(new String[0])); + } + + /** + * Notify users by email of something. + * + * @param subject + * @param message + * @param toAddresses + */ + public void notifyUsers(String subject, String message, String... toAddresses) { + try { + Message mail = mailExecutor.createMessage(toAddresses); + if (mail != null) { + mail.setSubject(subject); + mail.setText(message); + mailExecutor.queue(mail); + } + } catch (MessagingException e) { + logger.error("Messaging error", e); + } + } + /** * Returns the descriptions/comments of the Gitblit config settings. * diff --git a/src/com/gitblit/GitServlet.java b/src/com/gitblit/GitServlet.java index b928d836..b2ee1c79 100644 --- a/src/com/gitblit/GitServlet.java +++ b/src/com/gitblit/GitServlet.java @@ -15,11 +15,48 @@ */ package com.gitblit; +import groovy.lang.Binding; +import groovy.util.GroovyScriptEngine; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.List; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.PostReceiveHook; +import org.eclipse.jgit.transport.PreReceiveHook; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceiveCommand.Result; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.HttpUtils; +import com.gitblit.utils.StringUtils; + /** * The GitServlet exists to force configuration of the JGit GitServlet based on * the Gitblit settings from either gitblit.properties or from context * parameters in the web.xml file. * + * It also implements and registers the Groovy hook mechanism. + * * Access to this servlet is protected by the GitFilter. * * @author James Moger @@ -29,6 +66,8 @@ public class GitServlet extends org.eclipse.jgit.http.server.GitServlet { private static final long serialVersionUID = 1L; + private GroovyScriptEngine gse; + /** * Configure the servlet from Gitblit's configuration. */ @@ -41,4 +80,232 @@ public class GitServlet extends org.eclipse.jgit.http.server.GitServlet { } return super.getInitParameter(name); } + + @Override + public void init(ServletConfig config) throws ServletException { + String groovyRoot = GitBlit.getString(Keys.groovy.scriptsFolder, "groovy"); + try { + gse = new GroovyScriptEngine(groovyRoot); + } catch (IOException e) { + throw new ServletException("Failed to instantiate Groovy Script Engine!", e); + } + + // set the Gitblit receive hook + setReceivePackFactory(new DefaultReceivePackFactory() { + @Override + public ReceivePack create(HttpServletRequest req, Repository db) + throws ServiceNotEnabledException, ServiceNotAuthorizedException { + ReceivePack rp = super.create(req, db); + GitblitReceiveHook hook = new GitblitReceiveHook(); + hook.gitblitUrl = HttpUtils.getGitblitURL(req); + rp.setPreReceiveHook(hook); + rp.setPostReceiveHook(hook); + return rp; + } + }); + super.init(config); + } + + /** + * The Gitblit receive hook allows for special processing on push events. + * That might include rejecting writes to specific branches or executing a + * script. + * + * @author James Moger + * + */ + private class GitblitReceiveHook implements PreReceiveHook, PostReceiveHook { + + protected final Logger logger = LoggerFactory.getLogger(GitblitReceiveHook.class); + + protected String gitblitUrl; + + /** + * Instrumentation point where the incoming push event has been parsed, + * validated, objects created BUT refs have not been updated. You might + * use this to enforce a branch-write permissions model. + */ + @Override + public void onPreReceive(ReceivePack rp, Collection commands) { + List scripts = GitBlit.getStrings(Keys.groovy.preReceiveScripts); + RepositoryModel repository = getRepositoryModel(rp); + scripts.addAll(repository.preReceiveScripts); + UserModel user = getUserModel(rp); + runGroovy(repository, user, commands, scripts); + for (ReceiveCommand cmd : commands) { + if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) { + logger.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId() + .getName(), cmd.getResult(), cmd.getMessage())); + } + } + + // Experimental + // runNativeScript(rp, "hooks/pre-receive", commands); + } + + /** + * Instrumentation point where the incoming push has been applied to the + * repository. This is the point where we would trigger a Jenkins build + * or send an email. + */ + @Override + public void onPostReceive(ReceivePack rp, Collection commands) { + if (commands.size() == 0) { + logger.info("skipping post-receive hooks, no refs created, updated, or removed"); + return; + } + List scripts = GitBlit.getStrings(Keys.groovy.postReceiveScripts); + RepositoryModel repository = getRepositoryModel(rp); + scripts.addAll(repository.postReceiveScripts); + UserModel user = getUserModel(rp); + runGroovy(repository, user, commands, scripts); + + // Experimental + // runNativeScript(rp, "hooks/post-receive", commands); + } + + /** + * Returns the RepositoryModel for the repository we are pushing into. + * + * @param rp + * @return a RepositoryModel + */ + protected RepositoryModel getRepositoryModel(ReceivePack rp) { + Repository repository = rp.getRepository(); + String rootPath = GitBlit.getRepositoriesFolder().getAbsolutePath(); + String repositoryName = repository.getDirectory().getAbsolutePath(); + repositoryName = repositoryName.substring(rootPath.length() + 1); + RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName); + return model; + } + + /** + * Returns the UserModel for the user pushing the changes. + * + * @param rp + * @return a UserModel + */ + protected UserModel getUserModel(ReceivePack rp) { + PersonIdent person = rp.getRefLogIdent(); + UserModel user = GitBlit.self().getUserModel(person.getName()); + if (user == null) { + // anonymous push, create a temporary usermodel + user = new UserModel(person.getName()); + } + return user; + } + + /** + * Runs the specified Groovy hook scripts. + * + * @param repository + * @param user + * @param commands + * @param scripts + */ + protected void runGroovy(RepositoryModel repository, UserModel user, + Collection commands, List scripts) { + if (scripts == null || scripts.size() == 0) { + // no Groovy scripts to execute + return; + } + + Binding binding = new Binding(); + binding.setVariable("gitblit", GitBlit.self()); + binding.setVariable("repository", repository); + binding.setVariable("user", user); + binding.setVariable("commands", commands); + binding.setVariable("url", gitblitUrl); + binding.setVariable("logger", logger); + for (String script : scripts) { + if (StringUtils.isEmpty(script)) { + continue; + } + try { + Object result = gse.run(script, binding); + if (result instanceof Boolean) { + if (!((Boolean) result)) { + logger.error(MessageFormat.format( + "Groovy script {0} has failed! Hook scripts aborted.", script)); + break; + } + } + } catch (Exception e) { + logger.error( + MessageFormat.format("Failed to execute Groovy script {0}", script), e); + } + } + } + + /** + * Runs the native push hook script. + * + * http://book.git-scm.com/5_git_hooks.html + * http://longair.net/blog/2011/04/09/missing-git-hooks-documentation/ + * + * @param rp + * @param script + * @param commands + */ + @SuppressWarnings("unused") + protected void runNativeScript(ReceivePack rp, String script, + Collection commands) { + + Repository repository = rp.getRepository(); + File scriptFile = new File(repository.getDirectory(), script); + + int resultCode = 0; + if (scriptFile.exists()) { + try { + logger.debug("executing " + scriptFile); + Process process = Runtime.getRuntime().exec(scriptFile.getAbsolutePath(), null, + repository.getDirectory()); + BufferedReader reader = new BufferedReader(new InputStreamReader( + process.getInputStream())); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( + process.getOutputStream())); + for (ReceiveCommand command : commands) { + switch (command.getType()) { + case UPDATE: + // updating a ref + writer.append(MessageFormat.format("{0} {1} {2}\n", command.getOldId() + .getName(), command.getNewId().getName(), command.getRefName())); + break; + case CREATE: + // new ref + // oldrev hard-coded to 40? weird. + writer.append(MessageFormat.format("40 {0} {1}\n", command.getNewId() + .getName(), command.getRefName())); + break; + } + } + resultCode = process.waitFor(); + + // read and buffer stdin + // this is supposed to be piped back to the git client. + // not sure how to do that right now. + StringBuilder sb = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + logger.debug(sb.toString()); + } catch (Throwable e) { + resultCode = -1; + logger.error( + MessageFormat.format("Failed to execute {0}", + scriptFile.getAbsolutePath()), e); + } + } + + // reject push + if (resultCode != 0) { + for (ReceiveCommand command : commands) { + command.setResult(Result.REJECTED_OTHER_REASON, MessageFormat.format( + "Native script {0} rejected push or failed", + scriptFile.getAbsolutePath())); + } + } + } + } } diff --git a/src/com/gitblit/MailExecutor.java b/src/com/gitblit/MailExecutor.java index bfe2232f..56a4ab58 100644 --- a/src/com/gitblit/MailExecutor.java +++ b/src/com/gitblit/MailExecutor.java @@ -15,6 +15,7 @@ */ package com.gitblit; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -24,6 +25,7 @@ import java.util.Properties; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.regex.Pattern; import javax.mail.Authenticator; import javax.mail.Message; @@ -152,11 +154,23 @@ public class MailExecutor implements Runnable { InternetAddress from = new InternetAddress(fromAddress, "Gitblit"); message.setFrom(from); - InternetAddress[] tos = new InternetAddress[toAddresses.size()]; - for (int i = 0; i < toAddresses.size(); i++) { - tos[i] = new InternetAddress(toAddresses.get(i)); + Set uniques = new HashSet(toAddresses); + Pattern validEmail = Pattern + .compile("^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$"); + List tos = new ArrayList(); + for (String address : uniques) { + if (StringUtils.isEmpty(address)) { + continue; + } + if (validEmail.matcher(address).find()) { + try { + tos.add(new InternetAddress(address)); + } catch (Throwable t) { + } + } } - message.setRecipients(Message.RecipientType.TO, tos); + message.setRecipients(Message.RecipientType.TO, + tos.toArray(new InternetAddress[tos.size()])); message.setSentDate(new Date()); } catch (Exception e) { logger.error("Failed to properly create message", e); diff --git a/src/com/gitblit/build/Build.java b/src/com/gitblit/build/Build.java index 43cd26f7..8ca52c32 100644 --- a/src/com/gitblit/build/Build.java +++ b/src/com/gitblit/build/Build.java @@ -89,6 +89,7 @@ public class Build { downloadFromApache(MavenObject.JDOM, BuildType.RUNTIME); downloadFromApache(MavenObject.GSON, BuildType.RUNTIME); downloadFromApache(MavenObject.MAIL, BuildType.RUNTIME); + downloadFromApache(MavenObject.GROOVY, BuildType.RUNTIME); downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME); downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.RUNTIME); @@ -114,7 +115,8 @@ public class Build { downloadFromApache(MavenObject.JDOM, BuildType.COMPILETIME); downloadFromApache(MavenObject.GSON, BuildType.COMPILETIME); downloadFromApache(MavenObject.MAIL, BuildType.COMPILETIME); - + downloadFromApache(MavenObject.GROOVY, BuildType.COMPILETIME); + downloadFromEclipse(MavenObject.JGIT, BuildType.COMPILETIME); downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.COMPILETIME); @@ -495,6 +497,10 @@ public class Build { "1.4.3", 462000, 642000, 0, "8154bf8d666e6db154c548dc31a8d512c273f5ee", "5875e2729de83a4e46391f8f979ec8bd03810c10", null); + public static final MavenObject GROOVY = new MavenObject("groovy", "org/codehaus/groovy", "groovy-all", + "1.8.4", 6143000, 2290000, 4608000, "b5e7c2a5e6af43ccccf643ad656d6fe4b773be2b", + "a527e83e5c715540108d8f2b86ca19a3c9c78ac1", "8e5da5584fff57b14adbb4ee25cca09f5a00b013"); + public final String name; public final String group; public final String artifact; diff --git a/src/com/gitblit/models/RepositoryModel.java b/src/com/gitblit/models/RepositoryModel.java index 9a774fbd..c5423f03 100644 --- a/src/com/gitblit/models/RepositoryModel.java +++ b/src/com/gitblit/models/RepositoryModel.java @@ -55,6 +55,8 @@ public class RepositoryModel implements Serializable, Comparable preReceiveScripts; + public List postReceiveScripts; public RepositoryModel() { this("", "", "", new Date(0)); diff --git a/test-gitblit.properties b/test-gitblit.properties index 22556a2f..72a5db41 100644 --- a/test-gitblit.properties +++ b/test-gitblit.properties @@ -5,6 +5,9 @@ git.repositoriesFolder = git git.searchRepositoriesSubfolders = true git.enableGitServlet = true +groovy.scriptsFolder = groovy +groovy.preReceiveScripts = blockpush.groovy +groovy.postReceiveScripts = sendemail.groovy jenkins.groovy web.authenticateViewPages = false web.authenticateAdminPages = true web.allowCookieAuthentication = true @@ -59,6 +62,7 @@ mail.username = mail.password = mail.fromAddress = mail.adminAddresses = +mail.mailingLists = x@test.com y@test.com z@test.com federation.name = Unit Test federation.passphrase = Unit Testing federation.allowProposals = false