ScriptRunner allows you to easily write scripts to handle pre-receive events, which fire when a user pushes changes but before they are accepted in to the repository. The principle purpose of this type of hook is to block the push request and return a message to the user, if certain conditions are not fulfilled.

You can use the built-in content in conjunction with conditions to enforce your workflow. Typical examples of this would be:

Adding a Pre-Receive Hook

Navigate to Admin → Script Pre Hooks. Click a heading to add a handler. Choose Custom script hook to use your own scripts to decide whether to allow the push or not.

Built-in Pre-Receive Hooks

Protect git references

A git reference is a pointer to a particular commit. In the case of branches it is a pointer to the head of a line of work. For a tag, it’s a pointer to a particular commit that remains fixed. The purpose of this hook is to block changes to refs based on arbitrary conditions.

So, this does nothing other than reject the commit with the supplied message, if the provided condition evaluates to true. So, as a blank condition evaluates to true, unless you provide a condition this will reject everything (so not very useful).

The error message will only be used if one is not provided in the condition. If you want a dynamic error message based on the changes a user has pushed then you should use diagnosable conditions.

Click the Expand examples link to show examples…​ you will probably need to modify them to suit.

For example to prevent deletion of tags that begin with REL, you would start with the existing example for blocking all tag deletion, and end up with:

import com.atlassian.bitbucket.repository.RefChangeType

refChanges.any {
   it.ref.id.startsWith("refs/tags/REL") &&
       it.type == RefChangeType.DELETE
}

To reject changes to certain subdirectories from users not in a particular authorised group, we need to combine two of the example conditions. Firstly, checking the group of the pusher, and secondly, checking if the user is in a group.

This leads us to the following condition script:

import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.auth.AuthenticationContext
import com.atlassian.bitbucket.user.UserService

def userService = ComponentLocator.getComponent(UserService)
def authContext = ComponentLocator.getComponent(AuthenticationContext)

if (pathsMatchExcludingDeletes('glob:path/to/sensitive/code/**')) {
    // return true if the user is NOT in the desired group, thus blocking the push
    return ! userService.isUserInGroup(authContext.getCurrentUser(), "authorised-devs")
}
return false
The intention of the examples is that you use them as starting points for more complex instructions, by and and or ing them together for instance.
When restricting pushes by file name, use pathsMatchExcludingDeletes rather than pathsMatch, as this will allow users to delete undesirable files that were in the repository before you started using this hook.

Exclude files tracked by Git LFS

You can exclude files tracked by Git LFS from any hook condition by using the following:

pathsMatcher
    .excludingLfsFiles() (1)
    .matches("glob:**.jar") (2)
1 tell pathsMatcher to exclude files tracked by Git LFS
2 see if any files in the push (not already by Git LFS) with the jar file extension are present
This is particularly useful if you want a condition to block binary files, but exclude the ones already tracked by Git LFS. You could then add an error message informing the developer that they need to move these files to Git LFS if they want to push them.

You can find further configuration options for pathsMatcher here.

Naming standard enforcement

This allows you to enforce a naming standard for branches and tags using regular expressions.

You may want different naming standards for different types of branches. For example for your feature and bugfix branches, you may want them to contain a JIRA issue ID. For hotfix branches you may require that they begin with a Change Request ID. Therefore you can specify multiple regular expressions for both branches and tags. If none of them match, the supplied error message (or a default one), will be returned to the user.

An example where feature branches must start with a JIRA issue key, hotfix branches must begin with a CR number:

feature/[A-Z]{2,9}-\d+.*
hotfix/CR-\d{3,}.*

To enforce tag names follow the format: 3.2.14:

\d+\.\d+\.\d+
This does not apply to branches and tags created through the Bitbucket user interface. You will need to use the separate event handler for this case.

Restrict file size

Allows you to restrict the maximum file size of an individual file. This is important because once pushed and accepted, all files will stay in the repo forever. It will be transmitted to anyone that clones the repo, increasing the amount of time for the checkout operation, and increasing their disk space requirements.

> git push
Counting objects: 5, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 258.45 KiB | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote:
remote: =====================================================================
remote: File too large (max size 1048576 bytes): largefile.bin
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'

This can be combined with a condition so that certain users are allowed to push large files.

This can take some time when pushing a new repository, as the size of every file in each commit needs to be checked. During testing we were able to push the Groovy Core repository in around 1 minute on a fresh instance of Bitbucket Server. This type of performance can be expected from version 4.3.17 onwards due to this fix: SRBITB-187.
To check the progress of a large push you can enable debug logging for com.onresolve.scriptrunner.canned.bitbucket.hooks.MaxFilesizeHook. It will show you in the logs how many commits have been processed so far for a particular repository.

Exclude files tracked by Git LFS

You can exclude files tracked by Git LFS from the restrict file size hook by selecting the Exclude LFS files checkbox.

Using the restrict file size hook in this way will indicate to the developer which files need to be moved to LFS.

Enforce trusted commit authors

Allows you to specify a policy for handling authorship of incoming commits. This function supports three different policies:

  • All authors must be Bitbucket users

  • All authors must have write access to this repository

  • Can only push own commits

Optionally, you can check the following checkboxes:

  • Allow commits from forks. This will bypass checking for those commits that have already been pushed to forked repositories of the current one. This allows for a process where you have multiple forks with different access controls, and users need to pull from other forks and push back to "their own" one.

  • Commits must match name. This will check the Author name in the pushed commits against the current Bitbucket display name when using Can only push own commits or All authors must be Bitbucket users. Note that by default Bitbucket maps users by email address, so this option is for enforcing the display name matches as well.

For Commits must match name, merged and rebased commits will be ignored, as they are already in the repository. However cherry-picked commits will be matched against committer name and email but we will still check the author is a known Bitbucket user by email.

This is designed to mitigate against the following real-world issues:

Failure to set up the gitconfig with user and email correctly

This will make it hard to trace commits back to the real user. By default, anyone can commit using git commit -m "some comment" --author "Linus Torvalds <linus@example.com>", and if you have write access to the repository, you can push that.

Commits to the repository from someone who doesn’t have write access

A user who does have write access can take a patch (git format-patch) and apply it (git am), from a user who does not have write access. If you are in a regulated environment you may have to explain how that happened, possibly years after the fact. The same thing can be done with git bundle and various other ways.

Creating malevolent commits on behalf of another user

It’s possible for a disgruntled user to create a "bad" commit with another user as the author and push that.

The first two policies prevent primarily against misconfiguration and inadvertent breaches in policy, but may not prevent someone determined to cause problems.

The third policy is more restrictive but will prevent against even a wilful attempt to breach policy. It has a caveat however - although you can still merge other people’s branches and push them, you would not be able to merge from someone elses fork (unless they had already pushed their changes to the Bitbucket repository). You could also not use a shared local repository (which is not a good practice anyway).

Signed commits would be the next level of enforcement, although this can require significant investment in training and infrastructure.

Reject force push

This is the same as the per-repo hook that is in the base product. It is reproduced here as it allows system administrators to apply it at a level above project/repository administrator, such that they cannot disable it.

A force push is a push that rewrites history on the remote server, for instance squashing multiple commits into a single commit (such as is produced by a git rebase). Generally, it’s desirable to squash commits on a feature branch so you have a clear and coherent history for your project…​ but only if the feature branch is not shared, or, it’s shared and you can ensure that the team has pushed any pending changes, and know how to reset from the rewritten branch.

Therefore you might want to combine this hook with conditions, such that you disable force pushing just to the master and release branches for instance, or allow only on feature branches.

prevent force push on master

Reject merge commits

Enforces a linear workflow policy. This means that any unnecessary merge commits will be rejected. This forces your users to use rebase rather than merge to keep a linear history. This does not affect pull requests, so branches can still be merged, however, the trivial merge commits will be avoided. You can set up your own custom message to appear as the push is rejected.

The hook will list the commit ids that were rejected so that the user knows the commits that are causing the push to be blocked.

> git push
Counting objects: 12, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (12/12), 1.06 KiB | 0 bytes/s, done.
Total 12 (delta 6), reused 0 (delta 0)
remote:
remote: =====================================================================
remote: Rejecting push due to one or more merge commits:
remote: 6378ee0e4e4
remote: ef2dc27692b
remote: Merge commits are not allowed. You should rebase rather than merge.
remote: =====================================================================
remote:
To /bitbucket/scm/project_1/repetition.git
! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to '/bitbucket/scm/project_1/repetition.git
This will NOT affect any existing merges, if a merge is already committed and pushed, the hook will not look at that commit, only at new ones.

Require commits to be associated with a JIRA issue

This requires each commit to reference a valid JIRA issue, where the supplied JQL query defines what’s valid. The issue can either be mentioned in the commit message, or be present in the branch name.

If you use a pull request workflow, there is a merge check version of this which is simpler to use.
A configured JIRA application link is required. You either have to make one of your application links primary or select one when configuring this hook, as in the image below. If you select multiple application links then each one will be queried until one returns issues which match the specified JQL clause.
jira app link select
If you remove an application link you have selected for the hook, you should also update the hook configuration. As the hook verifies the application link can be found. If you don’t the hook will fail when you push to it.
Unfortunately the Bitbucket UI will only extract issues where they are referenced by commit message, which is a shame as if you create the branch from the issue, you’d want all commits on that branch to relate to the issue.

A sample query might be:

  • The issue is "in progress" and assigned to the user pushing the changes:

status = "In Progress" and assignee = currentUser()
  • You could also require that each commit references a particular JIRA project:

project = FOO

If the hook rejects the push, the user will need to edit the commit comments (for instance using git rebase) before pushing again.

If you want to exclude merge commits from being checked then you should select the exclude merge commits checkbox when configuring the hook.

You can also build a dynamic JQL clause to query on. You can either create your own templates or use one of the samples that are included in the default installation of ScriptRunner.

Finally, if your Jira issue keys do not follow the following default regex format, resulting in for example JIR-1 being validated by the hook:

((?<!([A-Z]{1,10})-?)[A-Z]+-\d+)

You can specify your own issue key regular expression using the "Issue key regex" field.

An example, which would result in JIR.1 being validated by the hook is:

((?<!([A-Z]{1,10})\.?)[A-Z]+\.\d+)

Diagnosable conditions

Most of the built-in pre-receive hooks are applied based on an arbitrary condition. Diagnosable conditions allow you to show the user exactly why the hook was applied for their pushed changes in the hook error message.

One use of this is in the protect git references hook to show users what changes caused the hook to block their push.

For example you could use the following condition to verify all the commit authors are valid Bitbucket users and display the ones who aren’t in the hook error message:

import com.atlassian.bitbucket.user.ApplicationUser
import com.atlassian.bitbucket.user.Person

def authors = refChanges.getCommitAuthors(repository)
authors.findAll {
    if (! (it instanceof ApplicationUser)) {
        def person = it as Person
        hookMessage << "\$person.name - \$person.emailAddress is not a Bitbucket user\\n".toString()
        return true
    }

    return false
}

If you use pathsMatch and other variations of it you can use it as a diagnosable condition to automatically display the blocked paths and changes using the version here.

You could even add instructions or Git commands to the error message so the user can fix their pushed changes before pushing again.

Custom Pre-Receive Hooks

Writing a Custom Pre-Receive Hook

To write your own hook use Custom script 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.") (1)

return false (2)
1 Provide an "informative" message that is displayed to the user.
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." (1)

return resultBuilder
    .veto(message, message) (2)
    .build() (3)
1 Provide an "informative" message that is displayed to the user.
2 Veto the result builder to block the push, omit this line to allow the push.
3 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:

import com.atlassian.bitbucket.hook.repository.RepositoryHookResult

(1)
import com.atlassian.bitbucket.repository.RefChange
import com.atlassian.bitbucket.repository.Repository

Repository repository = repository (2)
Collection<RefChange> refChanges = refChanges
HookResponse hookResponse = hookResponse
RepositoryHookResult.Builder resultBuilder = resultBuilder

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

return resultBuilder.build()
1 Required imports for the objects passed in the script binding.
2 Strongly type objects passed in the binding.
3 Define our condition to only veto the push is the repository is test.

This example also redefines the objects passed in the script binding - this can be useful when working with an IDE, so that it can provide coding assistance. If you are not using an IDE, both of these sections are redundant.

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("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.

Hook Triggers

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.

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() { (1)

    @Override
    boolean onCommitAdded(@Nonnull CommitAddedDetails commitDetails) {
        def commit = commitDetails.commit (2)

        if (commit.message.length() < 5) { (3)
            resultBuilder.veto("Commit message too short!", "$commit.displayId - message must be at least 5 characters long.")

            return false (4)
        }

        return true (5)
    }

    @Override
    RepositoryHookResult getResult() {
        resultBuilder.build() (6)
    }
}

commitFilters << RepositoryHookCommitFilter.ADDED_TO_ANY_REF (7)

return RepositoryHookResult.accepted() (8)
1 Create a commit callback which will give us the commits that have been pushed.
2 Get a single commit from the CommitDetails.
3 Check if the commit message is less than 5 characters long.
4 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>.
5 If the commit message is 5 characters or longer then return true to be provided with the next commit.
6 Build the final hook result to block/accept the push.
7 We want to check commit messages whenever a commit has been added so we add ADDED_TO_ANY_REF to the commitFilters binding.
8 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.

For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.

Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.