Getting Started

Stash Extension Points

Miscellaneous

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:

  • prevent rewriting history on master or release branches

  • blocking deletion of release tags

  • prevent direct changes on a release branch, i.e. not via a merge

  • block certain users or groups of users from modifying sensitive code

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).

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.stash.repository.RefChangeType

refChanges.any {
   it.refId.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.stash.user.StashAuthenticationContext
import com.atlassian.stash.user.UserService

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

if (pathsMatch('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.

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+

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/stash/scm/test/test.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'http://acme.com/stash/scm/test/test.git'

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

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 Stash users

  • All authors must have write access to this repository

  • Can only push own commits

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 Stash 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

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.

You must have a working app link set up.
Unfortunately the Stash 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.

Working with Custom Pre-Receive Hooks

To write your own hook use Custom script hook. Enter the path to your groovy script file, or enter the script inline as usual.

A complete example of a hook that rejects everything is:

hookResponse.out().print("You cannot commit at the moment.\n") (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.

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.stash.hook.HookResponse (1)
import com.atlassian.stash.repository.RefChange
import com.atlassian.stash.repository.Repository

Repository repository = repository (2)
Collection<RefChange> refChanges = refChanges
HookResponse hookResponse = hookResponse

if (repository.name == "test") {
    hookResponse.out().print("You cannot commit at the moment to any repository named 'test'\n")
    return false
}
return true
1 Required imports for the objects passed in the script binding.
2 Strongly type objects passed in the binding.

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/stash/scm/test/test.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'http://acme.com/stash/scm/test/test.git'

Therefore, you can use a utility method to call out the error in the response:

import com.onresolve.scriptrunner.canned.stash.util.StashCannedScriptUtils

if (repository.name == "test") {
    def msg = "You cannot commit at the moment to any repository named 'test'"
    hookResponse.out().print(StashCannedScriptUtils.wrapHookResponse(new StringBuilder(msg)))
    return false
}

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/stash/scm/test/test.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'http://acme.com/stash/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.