Pre-Receive with configuration

Here is an example of a pre-receive hook that rejects changes to a specified branch or tag. The source is available for download from Bitbucket here.

src/main/resources/atlassian-plugin.xml

<atlassian-plugin key="com.atlassian.bitbucket.server.bitbucket-docs" name="Bitbucket Server - Documentation" plugins-version="2">
	<plugin-info>
		<description>Provides a Hook for Bitbucket Server, which allows pushes to be denied</description>
		<vendor name="Atlassian" url="http://www.atlassian.com"/>
		<version>4.7.1</version>
	</plugin-info>

	<repository-hook key="disableRefChanges" name="Disable Ref Changes" class="com.atlassian.bitbucket.repository.hook.ref.ProtectRefHook">
		<description>Disables changes to a specified reference (branch or tag) in this repository</description>
		<icon>icons/example.png</icon>
		<config-form name="Ref Hook Config" key="refHook-config">
			<view>com.atlassian.bitbucket.repository.hook.ref.formContents</view>
			<directory location="/static/"/>
		</config-form>
		<!-- Validators can be declared separately -->
		<validator>com.atlassian.bitbucket.repository.hook.ref.RefValidator</validator>
	</repository-hook>

</atlassian-plugin>

src/main/resources/META-INF/spring/atlassian-plugins-component-imports.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:osgi="http://www.springframework.org/schema/osgi"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                           http://www.springframework.org/schema/osgi
                           http://www.springframework.org/schema/osgi/spring-osgi.xsd">

    <osgi:reference id="refService" interface="com.atlassian.bitbucket.repository.RefService" />

</beans>

src/main/java/com/atlassian/bitbucket/repository/hook/ref/ProtectRefHook.java

package com.atlassian.bitbucket.repository.hook.ref;

import com.atlassian.bitbucket.hook.HookResponse;
import com.atlassian.bitbucket.hook.repository.PreReceiveRepositoryHook;
import com.atlassian.bitbucket.hook.repository.RepositoryHookContext;
import com.atlassian.bitbucket.repository.Ref;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.RefService;

import javax.annotation.Nonnull;
import java.util.Collection;

public class ProtectRefHook implements PreReceiveRepositoryHook {

    private final RefService refService;

    public ProtectRefHook(RefService refService) {
        this.refService = refService;
    }


    /**
     * Disables changes to a ref
     */
    @Override
    public boolean onReceive(@Nonnull RepositoryHookContext context,
                             @Nonnull Collection<RefChange> refChanges,
                             @Nonnull HookResponse hookResponse) {
        String refId = context.getSettings().getString("ref-id");
        Ref found = refService.resolveRef(context.getRepository(), refId);
        for (RefChange refChange : refChanges) {
            if (refChange.getRefId().equals(found.getId())) {
                hookResponse.err().println("The ref '" + refChange.getRefId() + "' cannot be altered.");
                return false;
            }
        }
        return true;
    }
}

src/main/java/com/atlassian/bitbucket/repository/hook/ref/RefValidator.java

package com.atlassian.bitbucket.repository.hook.ref;

import com.atlassian.bitbucket.repository.Ref;
import com.atlassian.bitbucket.repository.RefService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.setting.RepositorySettingsValidator;
import com.atlassian.bitbucket.setting.Settings;
import com.atlassian.bitbucket.setting.SettingsValidationErrors;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;

public class RefValidator implements RepositorySettingsValidator {

    private final RefService refService;

    public RefValidator(RefService refService) {
        this.refService = refService;
    }

    @Override
    public void validate(@Nonnull Settings settings, @Nonnull SettingsValidationErrors settingsValidationErrors, @Nonnull Repository repository) {
        String refId = settings.getString("ref-id", null);
        if (StringUtils.isEmpty(refId)) {
            settingsValidationErrors.addFieldError("ref-id", "The ref id must be specified");
        }else {
            Ref ref = refService.resolveRef(repository, refId);
            if (ref == null) {
                settingsValidationErrors.addFieldError("ref-id", "Failed to find the ref");
            }
        }
    }
}

src/main/resources/static/simple.soy

{namespace com.atlassian.bitbucket.repository.hook.ref}

/**
 * @param config
 * @param? errors
 */
{template .formContents}
    {call bitbucket.component.branchSelector.field}
             {param id: 'ref-id' /}
             {param initialValue: $config['ref-id'] /}
             {param labelText: 'Reference' /}
             {param descriptionText: 'Any pushes with changes to this reference (branch or tag) will be rejected' /}
             {param errorTexts: $errors ? $errors['ref-id'] : null/}
             {param showTags: true/}
    {/call}
{/template}

The HookResponse class has both error and output writers, both of which will be displayed to the client.