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
}
CODE

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() 
CODE

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.