Controlling when pull requests can be merged
Overview
Teams using Bitbucket Server 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 Server supports this through Merge Request Check Plugin Modules.
Goals
This tutorial will take you through the steps required to write a plugin containing a Merge Request 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 learnt to do the following:
- Generate a Bitbucket Server plugin using Atlassian SDK
- Implement a merge request check in Java
- Declare a merge request check in your plugin's
atlassian-plugin.xml
- Use various Bitbucket Server services and the merge request check API
- Internationalize the messages your plugin emits
Steps
Generate a Bitbucket Server plugin using the Atlassian SDK
Ensure you have installed the Atlassian SDK on your system as described in this tutorial. Once installed, create a Bitbucket Server 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 |
Prepare your project and delete boilerplate code
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
andsrc/main/java/com/mycompany/bitbucket/merge/checks/impl
, with theirMyComponent
typessrc/main/resources/css
andsrc/main/resources/js
- All directories and files under
src/test/java
andsrc/test/resources
- These were unit and integration tests for
MyComponent
, which aren't valid afterMyComponent
has been deleted
- These were unit and integration tests for
In addition to removing those directories and files, also remove the <web-resource/>
block from atlassian-plugin.xml
. This example merge request doesn't include any UI components.
Now you have a basic Bitbucket Server plugin project with the following files and folders:
LICENSE
README
pom.xml
src/main/java/com/mycompany/bitbucket/merge/checks
src/main/resources/atlassian-plugin.xml
Merge request checks are part of Bitbucket Server'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<dependencies>
2...3<dependency>
4<groupId>com.atlassian.bitbucket.server</groupId>5<artifactId>bitbucket-spi</artifactId>6<version>${bitbucket.version}</version>7<scope>provided</scope>8</dependency>
9...10</dependencies>
Create a merge request check Java class
In order to implement a merge request check you will first need to create a Java class that implements the interface com.atlassian.bitbucket.scm.pull.MergeRequestCheck
. This check will ensure that whoever is merging the pull request is an administrator of the target repository so let's call it IsAdminMergeCheck
:
1package com.mycompany.bitbucket.merge.checks;23import com.atlassian.bitbucket.scm.pull.MergeRequest;4import com.atlassian.bitbucket.scm.pull.MergeRequestCheck;5import org.springframework.stereotype.Component;67import javax.annotation.Nonnull;8910public class IsAdminMergeCheck implements MergeRequestCheck {1112
13public void check( MergeRequest request) {14//TODO: implement me
15}16}
The MergeRequestCheck interface has a single method check(MergeRequest)
that you must implement. This method is called when Bitbucket Server 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 argument to this method is a MergeRequest
. A MergeRequest
encapsulates the current request to merge and allows you to retrieve the pull request via getPullRequest()
and to veto the merge via veto(String summaryMessage, String detailedMessage)
. If your check method does not call veto(...)
before it returns then Bitbucket Server will let the merge proceed (that is, as long as no other merge request check calls veto(...)
and there are no merge conflicts).
Let's go ahead and implement the check(MergeRequest)
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 Server if the current user has such permission. Bitbucket Server'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 Server at runtime when your merge request check is instantiated.
1package com.mycompany.bitbucket.merge.checks;23import com.atlassian.bitbucket.scm.pull.MergeRequest;4import com.atlassian.bitbucket.scm.pull.MergeRequestCheck;5import com.atlassian.bitbucket.permission.PermissionService;6import com.atlassain.plugin.spring.scanner.annotation.imports.ComponentImport;7import org.springframework.beans.factory.annotation.Autowired;8import org.springframework.stereotype.Component;910import javax.annotation.Nonnull;111213public class IsAdminMergeCheck implements MergeRequestCheck {1415private final PermissionService permissionService;1617
18public IsAdminMergeCheck( PermissionService permissionService) {19this.permissionService = permissionService;20}2122
23public void check( MergeRequest request) {24//TODO: implement me
25}26}
The PermissionService has methods to determine whether users have permission to access Bitbucket Server 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 MergeRequest.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:
12public void check( MergeRequest request) {3if (!permissionService.hasRepositoryPermission(request.getPullRequest().getToRef().getRepository(), Permission.REPO_ADMIN)) {4//TODO: implement me
5}6}
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 call MergeRequest.veto(String summaryMessage, String detailedMessage)
to tell Bitbucket Server 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 Server's internationalization service (I18nService) so that the localised version appropriate to the user's preferred locale is chosen. I18nService.getText(String key, String fallbackMessage)
will return you a localised 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:
1package com.mycompany.bitbucket.merge.checks;23import com.atlassian.bitbucket.i18n.I18nService;4import com.atlassian.bitbucket.permission.Permission;5import com.atlassian.bitbucket.permission.PermissionService;6import com.atlassian.bitbucket.scm.pull.MergeRequest;7import com.atlassian.bitbucket.scm.pull.MergeRequestCheck;8import com.atlassain.plugin.spring.scanner.annotation.imports.ComponentImport;9import org.springframework.beans.factory.annotation.Autowired;10import org.springframework.stereotype.Component;1112import javax.annotation.Nonnull;131415public class IsAdminMergeCheck implements MergeRequestCheck {1617private final I18nService i18nService;18private final PermissionService permissionService;1920
21public IsAdminMergeCheck( I18nService i18nService,22PermissionService permissionService) {23this.i18nService = i18nService;24this.permissionService = permissionService;25}2627
28public void check( MergeRequest request) {29if (!permissionService.hasRepositoryPermission(request.getPullRequest().getToRef().getRepository(), Permission.REPO_ADMIN)) {30//TODO: implement me
31}32}33}
Now we reference all the services we require to do our task so let's finish the implementation of the check method:
1package com.mycompany.bitbucket.merge.checks;23import com.atlassian.bitbucket.i18n.I18nService;4import com.atlassian.bitbucket.permission.Permission;5import com.atlassian.bitbucket.permission.PermissionService;6import com.atlassian.bitbucket.scm.pull.MergeRequest;7import com.atlassian.bitbucket.scm.pull.MergeRequestCheck;8import com.atlassain.plugin.spring.scanner.annotation.imports.ComponentImport;9import org.springframework.beans.factory.annotation.Autowired;10import org.springframework.stereotype.Component;1112import javax.annotation.Nonnull;131415public class IsAdminMergeCheck implements MergeRequestCheck {1617private final I18nService i18nService;18private final PermissionService permissionService;1920
21public IsAdminMergeCheck( I18nService i18nService,22PermissionService permissionService) {23this.i18nService = i18nService;24this.permissionService = permissionService;25}2627
28public void check( MergeRequest request) {29if (!permissionService.hasRepositoryPermission(request.getPullRequest().getToRef().getRepository(), Permission.REPO_ADMIN)) {30String summaryMsg = i18nService.getText("mycompany.plugin.merge.check.notrepoadmin.summary",31"Only repository administrators may merge pull requests");
32String detailedMsg = i18nService.getText("mycompany.plugin.merge.check.notrepoadmin.detailed",33"The user merging the pull request must be an administrator of the target repository");
34request.veto(summaryMsg, detailedMsg);35}36}37}
Implement atlassian-plugin.xml
After cleaning up the generated atlassian-plugin.xml
during our preparation steps, the file should look like this:
1<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2">2<plugin-info>
3<description>${project.description}</description>4<version>${project.version}</version>5<vendor name="${project.organization.name}" url="${project.organization.url}"/>6<param name="plugin-icon">images/pluginIcon.png</param>7<param name="plugin-logo">images/pluginLogo.png</param>8</plugin-info>
910<resource type="i18n" name="i18n" location="is-admin-merge-check"/>11</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, however. We need to register our component as a merge request check. This is done using the <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<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2">2<plugin-info>
3<description>${project.description}</description>4<version>${project.version}</version>5<vendor name="${project.organization.name}" url="${project.organization.url}"/>6<param name="plugin-icon">images/pluginIcon.png</param>7<param name="plugin-logo">images/pluginLogo.png</param>8</plugin-info>
910<resource type="i18n" name="i18n" location="is-admin-merge-check"/>1112<merge-check key="isAdmin" class="bean:isAdminMergeCheck"/>13</atlassian-plugin>
Create a plugin resource bundle (optional)
Bitbucket Server 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:
1mycompany.plugin.merge.check.notrepoadmin.summary=Nur Repository-Administratoren können Pull Requests akzeptieren2mycompany.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 withgetMessage
getKeyedText
calls can be replaced withcreateKeyedMessage
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.
Try it out!
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 merge request check!
Conclusion
In this tutorial you have learnt how to create a merge request check in Java, how to use various Bitbucket Server 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.