Example sources
Full sources for this tutorial can be found in the Bitbucket Example Plugins repository.
Teams using Bitbucket Data Center will often want to restrict when pull requests can be merged to a branch to align their use of pull requests with their established development process. For example, teams that want to ensure thorough review and foster knowledge sharing may want to require that a pull request has a certain number of reviewers before it can be merged. Other teams may want to enforce a quality gate on merging by requiring a green build on the source branch, or that a set of code metrics has been satisfied.
Bitbucket Data Center supports this through Repository Merge Check Plugin Modules.
This tutorial will take you through the steps required to write a plugin containing a Repository Merge Check which is designed to prevent pull requests from being merged unless the user performing the merge is an administrator for the target repository.
After completing this tutorial you will have learned to do the following:
atlassian-plugin.xml
Ensure you have installed the Atlassian SDK on your system as described in this tutorial.
Once installed, create a Bitbucket Data Center plugin project by running atlas-create-bitbucket-plugin
and supplying the
following values when prompted:
groupId | artifactId | version | package |
---|---|---|---|
com.mycompany.bitbucket | is-admin-merge-check | 1.0.0-SNAPSHOT | com.mycompany.bitbucket.merge.checks |
atlas-create-bitbucket-plugin
creates several classes you don't need, to help illustrate how to build a plugin. You
should remove all of these files:
src/main/java/com/mycompany/bitbucket/merge/checks/api
and src/main/java/com/mycompany/bitbucket/merge/checks/impl
,
with their MyComponent
typessrc/main/resources/css
and src/main/resources/js
src/test/java
and src/test/resources
MyComponent
, which aren't valid after MyComponent
has been deletedIn addition to removing those directories and files, also remove the <web-resource/>
block from
atlassian-plugin.xml
. This example merge check doesn't include any UI components.
Now you have a basic Bitbucket Data Center plugin project with the following files and folders:
LICENSE
README
pom.xml
src/main/java/com/mycompany/bitbucket/merge/checks
src/main/resources/META-INF/spring/plugin-context.xml
src/main/resources/atlassian-plugin.xml
Merge request checks are part of Bitbucket Data Center's SPI (its service provider interface).
atlas-create-bitbucket-plugin
generates pom.xml
with this dependency already in place, but for reference the
dependency looks like this:
1 2<dependencies> ... <dependency> <groupId>com.atlassian.bitbucket.server</groupId> <artifactId>bitbucket-spi</artifactId> <scope>provided</scope> </dependency> ... </dependencies>
In order to implement a repository merge check you will first need to create a Java class that implements the interface
com.atlassian.bitbucket.hook.repository.RepositoryMergeCheck
. This check will ensure that whoever is merging the
pull request is an administrator of the target repository so let's call it IsAdminMergeCheck
:
1 2package com.mycompany.bitbucket.merge.checks; import com.atlassian.bitbucket.hook.repository.*; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; @Component("isAdminMergeCheck") public class IsAdminMergeCheck implements RepositoryMergeCheck { @Nonnull @Override public RepositoryHookResult preUpdate(@Nonnull PreRepositoryHookContext context, @Nonnull PullRequestMergeHookRequest request) { // TODO: Implement me return RepositoryHookResult.accepted(); } }
The RepositoryMergeCheck
interface has a method preUpdate
that you must implement. This method is called when Bitbucket Data Center is either in the process of handling a request to
merge a pull request or it wants to display to the user whether the pull request can be merged. The second argument to
this method is a PullRequestMergeHookRequest
, which encapsulates
the current request to merge and allows you to retrieve the pull request via getPullRequest()
. The merge check can
veto the merge by returning a RepositoryHookResult
with one or more veto messages. If your preUpdate
method
returns RepositoryHookResult.accepted()
Bitbucket Data Center will let the merge proceed (that is, as long as no other
merge checks veto the request and there are no merge conflicts).
Atlassian Spring Scanner
Notice that IsAdminMergeCheck
is annotated with the standard Spring @Component
annotation.
Plugins generated by recent versions of the SDK use
Atlassian Spring Scanner
to simplify their Spring wiring. This replaces the legacy <component/>
directive in
atlassian-plugin.xml
, and <component/>
entries will no longer work.
Let's go ahead and implement the preUpdate
method.
Since our check wants to ensure only users with administrative permissions for the pull request's target repository
can merge, we will need to ask Bitbucket Data Center if the current user has such permission. Bitbucket Data Center's PermissionService
can answer this question for us so let's add that to the class: add a constructor to IsAdminMergeCheck
with a single
parameter of type PermissionService
and assign it a field permissionService
. This constructor argument will be injected
by Bitbucket Data Center at runtime when your repository merge check is instantiated.
1 2package com.mycompany.bitbucket.merge.checks; import com.atlassian.bitbucket.hook.repository.*; import com.atlassian.bitbucket.permission.PermissionService; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; @Component("isAdminMergeCheck") public class IsAdminMergeCheck implements RepositoryMergeCheck { private final PermissionService permissionService; @Autowired public IsAdminMergeCheck(@ComponentImport PermissionService permissionService) { this.permissionService = permissionService; } @Nonnull @Override public RepositoryHookResult preUpdate(@Nonnull PreRepositoryHookContext context, @Nonnull PullRequestMergeHookRequest request) { // TODO: Implement me return RepositoryHookResult.accepted(); } }
Atlassian Spring Scanner
Notice the constructor is @Autowired
, using the standard Spring annotation, and that the
PermissionService
parameter is annotated with @ComponentImport
, an annotation from
Atlassian Spring Scanner. Like
@Component
, @ComponentImport
replaces the legacy <component-import/>
directive in atlassian-plugin.xml
, and <component-import/>
entries will no longer
work.
The PermissionService has methods to determine whether users have permission to access Bitbucket Data Center objects. The
method we will use is hasRepositoryPermission(Repository, Permission)
. This takes a Repository
and a Permission
and returns true if the current user has the permission or false if not. Since we want to check if the current user is
a repository admin, the permission we should use is Permission.REPO_ADMIN
. But where do we find the target repository
for the pull request? The PullRequestMergeHookRequest.getPullRequest()
will give us the pull request being merged
and if we call getToRef().getRepository()
on this we will get the repository the pull request is being merged into.
Adding this to our method we have:
1 2@Nonnull @Override public RepositoryHookResult preUpdate(@Nonnull PreRepositoryHookContext context, @Nonnull PullRequestMergeHookRequest request) { Repository repository = request.getPullRequest().getToRef().getRepository(); if (!permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN)) { //TODO: implement me } return RepositoryHookResult.accepted(); }
The only thing left in this class is to implement what we want to do if the user is not a repository admin. In this case
we want to return RepositoryHookResult.rejected(String summaryMessage, String detailedMessage)
to tell Bitbucket
Data Center we wish to veto the merge. The summaryMessage
should be a message explaining the problem at a high level and
in as few words as possible (while still being clear). The detailedMessage
should be a longer message giving more
information and any context that may be helpful to the user in making them understand why the merge was vetoed and what
they might be able to change to fix the situation.
When generating messages that will be shown to the user, it is always good practice to use Bitbucket Data Center's
internationalization service (I18nService
) so that the localized version appropriate to the user's preferred locale
is chosen. I18nService.getText(String key, String fallbackMessage)
will return you a localized version message for
the supplied key or the fallbackMessage if no message could be found. So let's add I18nService
to our class like we
did with the PermissionService
:
1 2package com.mycompany.bitbucket.merge.checks; import com.atlassian.bitbucket.hook.repository.*; import com.atlassian.bitbucket.i18n.I18nService; import com.atlassian.bitbucket.permission.Permission; import com.atlassian.bitbucket.permission.PermissionService; import com.atlassian.bitbucket.repository.Repository; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; @Component("isAdminMergeCheck") public class IsAdminMergeCheck implements RepositoryMergeCheck { private final I18nService i18nService; private final PermissionService permissionService; @Autowired public IsAdminMergeCheck(@ComponentImport I18nService i18nService, @ComponentImport PermissionService permissionService) { this.i18nService = i18nService; this.permissionService = permissionService; } @Nonnull @Override public RepositoryHookResult preUpdate(@Nonnull PreRepositoryHookContext context, @Nonnull PullRequestMergeHookRequest request) { Repository repository = request.getPullRequest().getToRef().getRepository(); if (!permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN)) { //TODO: implement me } return RepositoryHookResult.accepted(); } }
Now we reference all the services we require to do our task so let's finish the implementation of the preUpdate
method:
1 2package com.mycompany.bitbucket.merge.checks; import com.atlassian.bitbucket.hook.repository.*; import com.atlassian.bitbucket.i18n.I18nService; import com.atlassian.bitbucket.permission.Permission; import com.atlassian.bitbucket.permission.PermissionService; import com.atlassian.bitbucket.repository.Repository; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; @Component("isAdminMergeCheck") public class IsAdminMergeCheck implements RepositoryMergeCheck { private final I18nService i18nService; private final PermissionService permissionService; @Autowired public IsAdminMergeCheck(@ComponentImport I18nService i18nService, @ComponentImport PermissionService permissionService) { this.i18nService = i18nService; this.permissionService = permissionService; } @Nonnull @Override public RepositoryHookResult preUpdate(@Nonnull PreRepositoryHookContext context, @Nonnull PullRequestMergeHookRequest request) { Repository repository = request.getPullRequest().getToRef().getRepository(); if (!permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN)) { String summaryMsg = i18nService.getText("mycompany.plugin.merge.check.notrepoadmin.summary", "Only repository administrators may merge pull requests"); String detailedMsg = i18nService.getText("mycompany.plugin.merge.check.notrepoadmin.detailed", "The user merging the pull request must be an administrator of the target repository"); return RepositoryHookResult.rejected(summaryMsg, detailedMsg); } return RepositoryHookResult.accepted(); } }
After cleaning up the generated atlassian-plugin.xml
during our preparation steps, the file should look like this:
1 2<atlassian-plugin key="\${atlassian.plugin.key}" name="\${project.name}" plugins-version="2"> <plugin-info> <description>\${project.description}</description> <version>\${project.version}</version> <vendor name="\${project.organization.name}" url="\${project.organization.url}"/> <param name="plugin-icon">images/pluginIcon.png</param> <param name="plugin-logo">images/pluginLogo.png</param> </plugin-info> <resource type="i18n" name="i18n" location="is-admin-merge-check"/> </atlassian-plugin>
In previous versions of the SDK, it would be necessary to add <component-import/>
directives for I18nService
and
PermissionService
, and to add a <component/>
directive for IsAdminMergeCheck
. Since our plugin is using Atlassian
Spring Scanner, though, these entries are not necessary and will be ignored if present. Instead, we used @Component
and @ComponentImport
directly in IsAdminMergeCheck
to import the necessary services and instantiate the check bean.
Simply instantiating an IsAdminMergeCheck
bean is not enough. We also need to register our component as a
repository merge check. This is done using the <repository-merge-check/>
element. You need to supply a key for the
<merge-check/>
tag that is unique within the plugin and you also need to reference the IsAdminMergeCheck
bean.
1 2<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2"> <plugin-info> <description>${project.description}</description> <version>${project.version}</version> <vendor name="${project.organization.name}" url="${project.organization.url}"/> <param name="plugin-icon">images/pluginIcon.png</param> <param name="plugin-logo">images/pluginLogo.png</param> </plugin-info> <resource type="i18n" name="i18n" location="is-admin-merge-check"/> <repository-merge-check key="isAdmin" class="bean:isAdminMergeCheck" configurable="false"/> </atlassian-plugin>
Using the bean:isAdminMergeCheck
syntax means IsAdminMergeCheck
will be instantiated once,
and that singleton instance will be used for all pull requests. This means IsAdminMergeCheck
can't
have any per-pull request state.
An alternative is to replace the @Component
on IsAdminMergeCheck
with @Scanned
, an
Atlassian Spring Scanner annotation, and
declare the <merge-check/>
like this:
1 2<repository-merge-check key="isAdmin" class="com.mycompany.bitbucket.merge.check.IsAdminMergeCheck" configurable="false"/>
Declared this way, a new IsAdminMergeCheck
will be instantiated every time merge checks are
run, and will only be used once. This is somewhat less efficient, as the check is continuously instantiated
and wired, but for certain merge checks it can be useful to be able to have per-pull request state.
Bitbucket Data Center will use fallback messages supplied to I18nService.getText(key, fallbackMessage)
calls, so if you
do not anticipate your plugin being used in a context other than the language your fallback messages are written in
you may choose to skip this step.
atlas-create-bitbucket-plugin
generates a <resource/>
element in atlassian-plugin.xml
, and creates an empty
is-admin-merge-check.properties
in src/main/resources
. You can define key/value pairs in this file, in standard
ResourceBundle format, to define the default
messages that will be used if messages localized for a more specific locale are not available.
For example, if you wanted to add German localizations for your plugin, you would add them in:
src/main/resources/is-admin-merge-check_de_DE.properties
Its contents would look something like:
1 2mycompany.plugin.merge.check.notrepoadmin.summary=Nur Repository-Administratoren können Pull Requests akzeptieren mycompany.plugin.merge.check.notrepoadmin.detailed=Um einen Pull Request akzeptieren zu können, muss der Benutzer ein Administrator des Ziel-Repositories sein
You do not need to provide a resource bundle for the locale and language your fallback messages are written in.
However, for large plugins with many internationalized messages, it is considered a best practice to do so. If
you have defined all of your messages in a properties file, the I18nService
offers different methods which do not
require you to duplicate your fallback messages in Java code:
getText
calls can be replaced with getMessage
getKeyedText
calls can be replaced with createKeyedMessage
If you will be defining your fallback messages in a ResourceBundle
you should prefer the method variants which
do not require fallback text, to prevent the two values from getting out of sync.
Note that starting from 5.0, Bitbucket Data Center is a Spring Boot application. You'll need the Atlassian SDK 6.3.0 or
higher to start the application. From the command line run atlas-run. Then you should connect to
http://localhost:7990/bitbucket
, logging in as admin/admin and create a project, repository, import some code with
multiple branches and create a pull request between them. To trigger your merge check's error message you should create
a new user who has contributor permissions to the repository (so that the merge button is displayed on the pull request
page for them) but who is not an administrator of the repository. When this user visits the pull request page you
should notice the merge button is greyed out and hovering over this button displays a tooltip with the detailed message
you supplied.
Congratulations on building your first repository merge check!
In this tutorial you have learned how to create a repository merge check in Java, how to use various Bitbucket Data Center services to implement your merge check, how to configure your atlassian-plugin.xml to declare your merge check and also how to provide internationalization and localization support for the messages you send to the user.
Rate this page: