Custom Pre-Receive Hooks

Samples

Writing a Custom Pre-Receive Hook

To write your own hook use Custom Pre-Hook. Enter the path to your groovy script file, or enter the script inline as usual.

The following examples will walk you through writing a custom pre-receive hook to block changes from being pushed.

Blocking Changes

A complete example of a hook that rejects everything is:

hookResponse.out().print("You cannot commit at the moment.") return false

Line 1:  Provide an "informative" message that is displayed to the user.

Line 2: Return false to block the push, true to allow it.

Another example of the same hook but rewritten using the Hooks API provided by Atlassian in Bitbucket 5 is:

def message = "You cannot commit at the moment." return resultBuilder .veto(message, message) .build()

Line 1:  Provide an "informative" message that is displayed to the user.

Line 4: Veto the result builder to block the push, omit this line to allow the push.

Line 5: Build and return the vetoed result builder to block the push.

We support both ways of writing your hook. The second one closely matches the Atlassian Hooks API introduced in Bitbucket 5.

If a single push fails for different reasons you may want to add multiple messages to inform the developer of the issues that need to be resolved.

Conditionally Blocking Changes

A very slightly more complex example, where we check the name of the repository. If the repository is test then the commit is blocked:

if (repository.name == "test") { def message = "You cannot commit at the moment to any repository named 'test'" resultBuilder.veto(message, message) } return resultBuilder.build()

Line 1: Define our condition to only veto the push is the repository is test.


It can be difficult for an end-user to pick out the reason for the failure amongst the response:


Counting objects: 5, done.
Writing objects: 100% (3/3), 256 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: You cannot commit at the moment to any repository named 'test'
To http://acme.com/bitbucket/scm/test/test.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'http://acme.com/bitbucket/scm/test/test.git'


Therefore, you can use a utility method to call out the error in the response:

import com.onresolve.scriptrunner.canned.bitbucket.util.BitbucketCannedScriptUtils if (repository.name == "test") { def message = BitbucketCannedScriptUtils.wrapHookResponse(new StringBuilder("You cannot commit at the moment to any repository named 'test'")) resultBuilder.veto(message, message) } return resultBuilder.build()

This produces a response that is easier to read:

Counting objects: 5, done.
Writing objects: 100% (3/3), 258 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote:
remote: =====================================================================
remote: You cannot commit at the moment to any repository named 'test'
remote: =====================================================================
remote:
To http://acme.com/bitbucket/scm/test/test.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'http://acme.com/bitbucket/scm/test/test.git'

It may be useful to include a link to an internal wiki article in the response, which can explain how to fix the problem.

As with all extension points, you can modify the file and repeat your push, without changing anything in the UI. The script will be automatically recompiled if it has changed, which makes the development process very fast.

When your script is successfully blocking pushes you don’t want, make sure it also allows the pushes that are OK.

Using the New Hooks API

There are a number of reasons why you may want to write your custom hook using the new Hooks API approach, which are highlighted below.

Hook Triggers

UI changes

Changes aren’t always pushed in Bitbucket Server, you can for example create branches and tags through the UI. The old approach was to have a hook to handle the pushes and an event handler to handle the UI action. This resulted in duplication of business logic.

For example if you wanted a hook that checks branch names you could apply the following triggers to it:

  • branch-create

  • repo-push

The full list of triggers are explained in more detail here.

Checking Triggers

You can respond to different triggers in different ways by checking the trigger in the hook request.

The example below checks for tag create or branch create triggers:

import com.atlassian.bitbucket.hook.repository.StandardRepositoryHookTrigger if (hookRequest.trigger == StandardRepositoryHookTrigger.BRANCH_CREATE) { // handle branch create } else if (hookRequest.trigger == StandardRepositoryHookTrigger.TAG_CREATE) { // handle tag create }

Commit Details

Previously to get a list of commits that we’re being pushed you could use refChanges.getCommits() or use the CommitService directly.

The new Hooks API helps you to get commits by providing them to you one by one. This forces you to write your hook in a more performant way when you have pushes containing a large number of commits.

This is best illustrated in the following example which shows a hook that validates commit messages are at least 5 characters in length. The part we’re interested in is the commitCallback section which provides us with the commits and checks the message.

import com.atlassian.bitbucket.hook.repository.CommitAddedDetails import com.atlassian.bitbucket.hook.repository.PreRepositoryHookCommitCallback import com.atlassian.bitbucket.hook.repository.RepositoryHookCommitFilter import com.atlassian.bitbucket.hook.repository.RepositoryHookResult import javax.annotation.Nonnull commitCallback = new PreRepositoryHookCommitCallback() { @Override boolean onCommitAdded(@Nonnull CommitAddedDetails commitDetails) { def commit = commitDetails.commit if (commit.message.length() < 5) { resultBuilder.veto("Commit message too short!", "$commit.displayId - message must be at least 5 characters long.") return false } return true } @Override RepositoryHookResult getResult() { resultBuilder.build() } } commitFilters << RepositoryHookCommitFilter.ADDED_TO_ANY_REF return RepositoryHookResult.accepted()

Line 9: Create a commit callback which will give us the commits that have been pushed.

Line 13: Get a single commit from the CommitDetails.

Line 15: Check if the commit message is less than 5 characters long.

Line 18: Veto the push with a message and return false to indicate we don’t want any more commits provided to us and move onto step <6>.

Line 21: If the commit message is 5 characters or longer then return true to be provided with the next commit.

Line 26: Build the final hook result to block/accept the push.

Line 30: We want to check commit messages whenever a commit has been added so we add ADDED_TO_ANY_REF to the commitFilters binding.

Line 32: We return accepted as a result because the validation part is handled by the commitCallback.

You may be wondering why we return accepted as a result in step 8. This is because in step 1 we do not actually run the commitCallback but we simply create it, it is only run after we have return accepted as a result in step 8.

Therefore the final decision on whether to block or accept the push is handled by the commitCallback.

Another confusing concept is the commitFilter binding. This just indicates what type of commits do I want to check. The full list of filters can be found here.

It’s recommended you use this pattern whenever you need to check commits in a hook.

Further information on getting commit details are explained here.

On this page