Custom Listeners

Working with Custom Listeners

The event is contained in the script binding. For cancelable events this will implement com.atlassian.bitbucket.event.CancelableEvent.

If it is cancelable, you can prevent the operation proceeding by calling cancel on it.

There is a shorter version supplied if you do not want to worry about supplying a translated message to the user:cancel(String message).

            event.cancel('foo')

You may choose to have your listener listen for multiple different events. If you need to do different things depending on the type of event, you can check that with instanceof.

For example, in a listener that handles project modification and project creation, getting the project key depends on whether the project is being updated or modified:

ApplicationEvent event = event // <1>
def projectKey

if (event instanceof ProjectCreationRequestedEvent) {
    projectKey = event.getProject().getKey()
} else if (event instanceof ProjectModificationRequestedEvent) {
    projectKey = event.getNewValue().getKey()
}

<1>: event is passed in the binding - this line is only used to give type information when using an IDE, and has no functional impact

Samples

Controlling personal repository creation

To use, go to Admin → Listeners. In the Events field, choose either or both of RepositoryCreationRequestedEvent and RepositoryForkRequestedEvent. One or the other (not both) will be fired depending on whether it’s a new personal repo, or a fork into the user’s personal project.

Allow only 5 personal repos

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.project.ProjectType
import com.atlassian.bitbucket.repository.RepositoryService

// only allow N personal
def repositoryService = ComponentLocator.getComponent(RepositoryService)
final Integer MAX_PERSONAL = 5
def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

if (project.type == ProjectType.PERSONAL) {
    if (repositoryService.countByProject(project) >= MAX_PERSONAL) {
        event.cancel("You can only create $MAX_PERSONAL personal repositories.")
    }
}

User in named directory

Allow personal repo creation only if the user is in a certain named directory:

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.crowd.embedded.api.CrowdDirectoryService
import com.atlassian.crowd.embedded.api.CrowdService
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.project.ProjectType

def crowdService = ComponentLocator.getComponent(CrowdService)
def crowdDirectoryService = ComponentLocator.getComponent(CrowdDirectoryService)

def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

// only allow when user comes from dir X
if (project.type == ProjectType.PERSONAL && event.user) {

    def cwdUser = crowdService.getUser(event.user.name)
    def directory = crowdDirectoryService.findDirectoryById(cwdUser.directoryId)

    if (!(directory.name in ["Bitbucket Internal Directory", "Stash Internal Directory"])) { // enter directory name
        event.cancel("You don't have permissions to create a personal repository.")
    }
}

Allow 1 Gb of personal repository space

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.project.ProjectType
import com.atlassian.bitbucket.repository.Repository
import com.atlassian.bitbucket.repository.RepositoryService
import com.atlassian.bitbucket.util.Page
import com.atlassian.bitbucket.util.PageProvider
import com.atlassian.bitbucket.util.PageRequest
import com.atlassian.bitbucket.util.PagedIterable

// only allow N personal
def repositoryService = ComponentLocator.getComponent(RepositoryService)
def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

// allow only 1Gb total size per user
if (project.type == ProjectType.PERSONAL) {
    def totalUserSize = new PagedIterable<Repository>(new PageProvider<Repository>() {
        @Override
        Page<Repository> get(PageRequest pageRequest) {
            repositoryService.findByProjectKey(project.key, pageRequest) as Page<Repository>
        }
    }, 100).sum { Repository repo ->
        repositoryService.getSize(repo)
    }

    if (totalUserSize >= 1024e3) {
        event.cancel("You can only use 1Gb in personal repository space, which you have exceeded.")
    }
}

Only allow from groups

Allow only members of certain groups to create personal repos:

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.project.ProjectType
import com.atlassian.bitbucket.user.UserService

def userService = ComponentLocator.getComponent(UserService)

def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

if (project.type == ProjectType.PERSONAL) {
    if (!userService.isUserInGroup(event.getUser(), "personal-project-creators")) {
        event.cancel("You don't have permissions to create a personal repository.")
    }
}

Disallowing project creation except for members of a group

import com.atlassian.sal.api.component.ComponentLocator // <1>
import com.atlassian.bitbucket.user.UserService

    def userService = ComponentLocator.getComponent(UserService)

    if (!userService.isUserInGroup(event.getUser(), "project-creators")) {
        event.cancel("Only users in the project-creators group can create projects")
    }

<1>: imports will generally not be displayed, for clarity

This script would be attached to the ProjectCreationRequestedEvent event.

Applying listeners to specific projects/repositories

You may find in a custom listener that you need to apply it only to particular projects or repositories.

For example to apply the allow only 5 personal repos custom listener to a specific project you can use the example below.

package examples.bitbucket.handler

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.bitbucket.repository.RepositoryService
import com.atlassian.sal.api.component.ComponentLocator

def repositoryService = ComponentLocator.getComponent(RepositoryService)
final Integer MAX_PER_TEST = 5
def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

if (project.key == "TEST") {
    if (repositoryService.countByProject(project) >= MAX_PER_TEST) {
        event.cancel("You can only create $MAX_PER_TEST repositories in test projects.")
    }
}

You could also apply it to a particular set of projects:

if (project.key in ["PTEST", "PTEST1", "PTEST2"]) { ... }

If you want to apply the listener to specific repositories instead you can use the following example below:

if (event.repository.project.key == "TEST" && event.repository.slug in [ "one", "two", "three" ]) { ... }

If applying to specific repositories you need to check both the project key and repository slug as the repository slug is only unique to a project.

Post to Slack when a repository is forked

The same principle can be used to integrate with anything that provides a web service, as an example we use Slack.

Typically you would add this code to a non-canceleable event, so that the other system gets notified only if the operation is successful.

package examples.bitbucket.handler

import com.atlassian.bitbucket.event.repository.RepositoryForkedEvent
import groovy.json.JsonBuilder
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method

RepositoryForkedEvent event = event

def http = new HTTPBuilder("https://hooks.slack.com")
http.request(Method.POST, ContentType.TEXT) {
    uri.path = "/services/XXXX/YYYYY/ZZZZZ" // <1>
    body = new JsonBuilder([
        channel   : "#dev",
        username  : "webhookbot",
        text      : "New fork created on repo: *${event.repository.name}* " +
            "by *${event.user.displayName}*. Come and join our chat!",
        icon_emoji: ":ghost:",
        mrkdwn    : true,
    ]).toString()
}

<1>: Slack will give you this value

This script would be attached to the RepositoryForkedEvent event.

Update all related Jira issues when Pull Request Opened

This sample will update linked issues on the creation of a Pull Request. This may be useful for informing stakeholders on progress.

package examples.bitbucket.handler

import com.atlassian.applinks.api.ApplicationLinkResponseHandler
import com.atlassian.plugin.PluginAccessor
import com.atlassian.sal.api.UrlMode
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.sal.api.net.Request
import com.atlassian.sal.api.net.Response
import com.atlassian.bitbucket.event.pull.PullRequestOpenedEvent
import com.atlassian.bitbucket.integration.jira.JiraIssueService
import com.onresolve.scriptrunner.canned.bitbucket.util.BitbucketBaseScript
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import groovy.json.JsonBuilder
import groovy.transform.BaseScript

@BaseScript BitbucketBaseScript baseScript

PullRequestOpenedEvent event = event

def pluginAccessor = ComponentLocator.getComponent(PluginAccessor)
def jiraIssueService = ScriptRunnerImpl.getOsgiService(JiraIssueService)

def pullRequest = event.getPullRequest()
def repository = pullRequest.fromRef.repository
assert pullRequest

def keys = jiraIssueService.getIssuesForPullRequest(repository.id, pullRequest.id)*.key // <1>

def jiraLink = getJiraAppLink()
def authenticatedRequestFactory = jiraLink.createImpersonatingAuthenticatedRequestFactory()

// work out the URL to the pull request
def prUrl = "${getBaseUrl(UrlMode.ABSOLUTE)}/projects/" +
    "${repository.project.key}/repos/${repository.slug}/" +
    "pull-requests/${pullRequest.id}/overview"

def input = new JsonBuilder([
    body: "Pull Request [${pullRequest.id}|$prUrl] has been updated:\n" +
        "\n" +
        "{{${pullRequest.title}}}"
]).toString()

keys.each { String key -> // <2>
    authenticatedRequestFactory // <3>
        .createRequest(Request.MethodType.POST, "rest/api/2/issue/$key/comment?expand=renderedBody")
        .addHeader("Content-Type", "application/json")
        .setEntity(input)
        .execute([
            handle: { Response response ->
                if (response.successful) {
                    log.debug "Created comment on issue: $key."
                } else {
                    log.warn "Failed to create comment: $response.responseBodyAsStream"
                }
            }] as ApplicationLinkResponseHandler<Void>
        )
}

<1>: get all issue keys related to this PR

<2>: iterate over the issues

<3>: create a comment on each issue using the Jira REST API

For this to work you must have a working application link to Jira set up.

Block Forking Unless Unreleased Versions

Another contrived example that will block repository forking unless an unreleased Jira version exists in the project. This script should be attached to the RepositoryForkRequestedEvent event.

@BaseScript BitbucketBaseScript baseScript
RepositoryForkRequestedEvent event = event

def jiraLink = getJiraAppLink()
def jiraVersions = jiraLink.createImpersonatingAuthenticatedRequestFactory()
    .createRequest(Request.MethodType.GET, "rest/api/2/project/SD/versions") // <1>
    .addHeader("Content-Type", "application/json")
    .execute([
        handle: { Response response ->
            new JsonSlurper().parse(response.responseBodyAsStream)
        }
    ] as ApplicationLinkResponseHandler<String>
    )

if (!(jiraVersions.any { !it.released })) { // <2>
    def msg = "All JIRA versions are released for this project... " +
        "please create a new version before forking. Current versions are: ${jiraVersions*.name.join(",")}"
    event.cancel(new KeyedMessage("some.key", msg, msg))
}

<1>: Get list of versions. Note: project SD is hard-coded here

<2>: Look for unreleased versions

Further Examples

On this page