I’m a big believer in learning by example. When I’m about to start working with a new library or piece of technology, I first look for prior art to demonstrate the common patterns and idioms associated with its use. For Atlassian plugins, this used to involve trawling through Bitbucket for decent examples or downloading plugin source jars from maven and browsing through them.

But there is an easier way! The Atlassian SDK ships with a set of interactive scripts for creating a plugin and customising it to suit your requirements.

Step 0: Generating a plugin project

The first step is installing the Atlassian SDK or, if you already have it installed, upgrading to a recent version using the atlas-update command. Then, invoking the atlas-create-stash-plugin and following the prompts will create you a shiny new Stash plugin project, in a ready-to-build-and-deploy state.

1
2
3
4
5
6
7
8
9
10
$ atlas-create-stash-plugin
...
[INFO] [stash:create]
Define value for groupId: : com.mycompany.stash.plugin
Define value for artifactId: : my-fun-plugin
Define value for version: [1.0-SNAPSHOT]:
Define value for package: [com.mycompany.stash.plugin]:
...
$ ls my-fun-plugin/
LICENSE README pom.xml src

At this point, you can start up Stash with your new plugin installed by invoking the atlas-run command. But our plugin (though deployable) doesn’t do anything useful yet.

Step 1: Generating customized plugin modules

The SDK ships with a second set of commands that allow you to customize your plugin with the various plugin modules supported by Stash. From the newly created plugin directory, running atlas-create-stash-plugin-module will present you with a list of Stash plugin module types:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ atlas-create-stash-plugin-module
...
[INFO] [stash:create-plugin-module]
Choose Plugin Module:
1: Changeset Indexer
2: Component Import
3: Component
4: Downloadable Plugin Resource
5: Licensing API Support
6: Module Type
7: REST Plugin Module
8: Repository Hook
9: SCM Request Check
10: Servlet Context Listener
11: Servlet Context Parameter
12: Servlet Filter
13: Servlet
14: SSH Request Handler
15: Keyboard Shortcut
16: Template Context Item
17: Web Item
18: Web Panel Renderer
19: Web Resource
20: Web Resource Transformer

Choose a number (1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20):

(if you don’t see the full list above, try running atlas-update to make sure you’re on the latest version of the SDK)

For this example we’ll take a look at option 8, the Repository Hook plugin module that shipped in Stash 2.2. To make things more interesting, let’s create a hook that solves a real problem.

Atlassian has a bit of a reputation for enjoying a beer or two. Let’s create a hook that confiscates the metaphorical keys from a developer’s IDE after “Beer O’Clock”. That is, prevent pushes to a repository’s master branch after 5pm on a Friday.

Once you’ve selected option 8 (Repository Hook) you’ll be greeted with another interactive prompt that will ask you some simple questions to customize your new plugin module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Choose a number (1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20): 8

Enter New Classname [MyRepositoryHook]: BeerOClockHook
Enter Package Name [com.atlassian.stash.plugin.hook]:
Enter Hook Type (pre/post/merge) [post]: pre
Show Advanced Setup? (Y/y/N/n) [N]: N

[INFO] Adding the following items to the project:
[INFO] [class: ccom.atlassian.stash.plugin.hook.BeerOClockHook]
[INFO] [dependency: org.mockito:mockito-all]
[INFO] [module: repository-hook]
[INFO] [resource: repository-hook.soy]
[INFO] i18n strings: 2

Add Another Plugin Module? (Y/y/N/n) [N]: N

Now let’s open our project in an IDE and take a look at it:

project

Sweet! Look at all that free stuff we got for answering a few questions:

  • pom.xml – a Maven project file containing all the relevant Stash dependencies and other libraries needed for our plugin
  • atlassian-plugin.xml – an Atlassian plugin descriptor containing all the relevant module definitions for your plugin
  • BeerOClockHook.java – our customized pre-receive repoisitory hook implementation
  • MyPluginComponent.java – a simple component class and interface, which we could augment with our own business logic and inject into other plugin components as a re-usable service (or delete in this case, as we’re going to put our code directly into BeerOClockHook in this example)
  • simple unit test classes and special wired test classes for our plugin, because tests are important for any project
  • a simple icon and logo for our plugin, will be displayed in the UPM and on the Atlassian Marketplace. You can update these with your own graphics prior to release.
  • some stub CSS & JS resources bound to a new web resource context for our plugin.

If we open up our atlassian-plugin.xml plugin descriptor, we can see that a repository-hook plugin module for BeerOClockHook has already been generated and configured for us:

<repository-hook name="Beer O Clock Hook" key="beer-o-clock-hook"
i18n-name-key="beer-o-clock-hook.name" class="com.atlassian.stash.plugin.hook.BeerOClockHook">
    <description key="beer-o-clock-hook.description">The Beer O Clock Hook Plugin</description>
    <icon>icon-example.png</icon>
</repository-hook>

Now that we’ve used our handy SDK scripts to dispense with the boilerplate, all we have to do to implement our repository hook is write the business logic.

Step 2: Write your business logic

If we open up our BeerOClockHook class we’ll see some trivial example code:

public class BeerOClockHook implements PreReceiveRepositoryHook
{
    /**
     * Disables deletion of branches
     */

    @Override
    public boolean onReceive(RepositoryHookContext context, Collection refChanges, HookResponse hookResponse)
    {
        for (RefChange refChange : refChanges)
        {
            if (refChange.getType() == RefChangeType.DELETE)
            {
                hookResponse.err().println("The ref '" + refChange.getRefId() + "' cannot be deleted.");
                return false;
            }
        }
        return true;
    }
}

Let’s change it to solve our business problem – vetoing updates to master after 5pm on a Friday:

/**
 * Prevent pushes after 5pm on a Friday
 */

public class BeerOClockHook implements PreReceiveRepositoryHook
{
    @Override
    public boolean onReceive(RepositoryHookContext context, Collection refChanges, HookResponse hookResponse)
    {
        for (RefChange change : refChanges) {
            // check if master is being updated
            if ("refs/heads/master".equals(change.getRefId())) {
                // if it's after 5pm on a friday (Beer O'Clock), prevent the push
                Calendar now = Calendar.getInstance();
                if (now.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY && now.get(Calendar.HOUR_OF_DAY) >= 17) {
                    hookResponse.err().println("Pushing to master is *not allowed* after beer o'clock!");
                    return false;
                }
                break;
            }
        }
        return true;
    }
}

And that’s it! We have all the code we need for our Beer O’Clock hook plugin. Let’s test it out.

We can fire up a Stash instance with our plugin installed by invoking atlas-run from our plugin directory. Once Stash has started, we can see our hook has appeared in the pre-receive section on the repository hooks page:

Installed repository hooks

Now a quick test to prove it works. After adjusting my system date/time to be 5:01pm on a Friday, we can attempt to delete the master branch of the sample Stash repository:

1
2
3
4
5
6
$ git push http://admin:admin@localhost:7990/stash/scm/PROJECT_1/rep_1.git :master

remote: Pushing to master is *not allowed* after beer o'clock!

To http://admin:admin@localhost:7990/stash/scm/PROJECT_1/rep_1.git
! [remote rejected] master (pre-receive hook declined)

It works!

To recap, we’ve just created a brand new, functional repository hook by:

  1. Installing the SDK
  2. Running atlas-create-stash-plugin once
  3. Running atlas-create-stash-plugin-module once
  4. Modifying a generated class to solve our business problem

Plugin development has never been so easy! Remember that the Repository Hook module type we just experimented with is just one of the twenty modules that currently support code generation in Stash. The other Atlassian products have equivalent SDK commands as well.

As always, the source for this plugin is available on Bitbucket. But it’s so easy to generate it yourself, why would you need it? :)