Conditions
ScriptRunner allows global administrators to apply hooks, merge checks, and listeners. You can either write your own hooks and handlers in groovy, or use the provided content. Most of these can be used in conjunction with conditions
, which will give you high level of flexibility, and control.
You can apply the hooks to all repositories, or just particular repositories or projects.
Conditions
Most content (hooks, merge checks, listeners) has a condition option. Typically you will combine a condition with the built-in content, for example mail out only when a branch that matches a regex is created.
Click the Expand examples link under the Condition field to display a list of examples.
Clicking any of these will overwrite the current condition with the sample code. You will probably need to modify any string literals, such as references to project keys or groups. The result of the last line of any groovy script is returned as the result of the condition, but feel free to use the return
keyword to make it clearer.
Take as an example a contrived scenario - you want to prevent creation of tags in all publicly accessible repositories. Start with the condition that matches creation of tags:
import com.atlassian.bitbucket.repository.RefChangeType
refChanges.any { it.ref.id.startsWith("refs/tags/") &&
it.type == RefChangeType.ADD
}
Now test the condition for publicly accessible projects:
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.repository.Repository
import com.atlassian.bitbucket.permission.PermissionService
Repository repository = repository
def permissionService = ComponentLocator.getComponent(PermissionService)
permissionService.isPubliclyAccessible(repository.project)
Given that they both work, we can combine the two like so:
Repository repository = repository
Collection<RefChange> refChanges = refChanges
def permissionService = ComponentLocator.getComponent(PermissionService)
def isPublic = permissionService.isPubliclyAccessible(repository.project)
def isTagCreate = refChanges.any {
it.ref.id.startsWith("refs/tags/") &&
it.type == RefChangeType.ADD
}
isPublic && isTagCreate
Condition Methods
There are a couple of methods that can be used in conditions which simplify writing conditions. They can be used either as though they were global functions, in which case they operate on whatever is in the binding, or as if they were methods of e.g. PullRequest. They are:
pathsMatch
boolean pathsMatch(String syntaxAndPattern)
This will return true if the push, pull request, or merge check etc contains changes in the specific path. The syntaxAndPattern
argument has two possible forms: glob
and regex
.
The glob method takes ant globs (an Ant-style path matching method), for instance the following matches a push that contains java files in the directory some/path.
refChanges.pathsMatch("glob:some/path/**.java")
In a merge request, this will return true if any of the files modified are under ssh/keys:
mergeRequest.pullRequest.pathsMatch("glob:ssh/keys/**")
This will go through each commit and check if there are any changes in the specific path. Except for a pull request where only the changes from the diff are considered. This means if you rebase or merge in changes from the target branch into the source branch of a pull request to bring it up-to-date then these changes will be ignored as they are already in the branch you are merging to.
pathsMatch (against a specific commit)
The pathsMatch method is also available against a single commit. You can use it to perform some logic on a particular commit, or iterate a list of commits and check multiple criteria against each individual commit.
Used in a listener triggered by a PullRequestEvent
, the following condition will check the latest commit and return true
if any gradle files inside a `some/path/`
directory were changed without updating readme.md.
def lastCommit = event.pullRequest.commits.last()
lastCommit.pathsMatch("glob:some/path/**.gradle") &&
!lastCommit.pathsMatch("glob:readme.md")
Used in a merge check, the following script will return true
if any commit contains the “wip”
text in their message, and pom.xml was edited in the same commit.
mergeRequest.pullRequest.commits.any { commit ->
commit.pathsMatch("glob:pom.xml") && commit.message.containsIgnoreCase('wip')
}
pathsMatch (against commits for pull requests)
boolean pathsMatch(String syntaxAndPattern, Iterable<Commit> commits)
This is the same as pathsMatch
except that it will match against the supplied commits for a pull request rather than changes in the diff. This can be useful when preventing sensitive files from being merged if they exist in the commit history of the pull request using merge checks.
A commit
is an Atlassian commit.
In a merge request, this will return true if any of the files modified are under ssh/keys:
def pullRequest = mergeRequest.pullRequest
mergeRequest.pullRequest.pathsMatch("glob:ssh/keys/**", pullRequest.getCommits())
If using an event handler condition you should use:
def pullRequest = event.pullRequest
pullRequest.pathsMatch("glob:ssh/keys/**", pullRequest.getCommits())
If you click on the Expand examples
link under the condition you will see various examples of how to pathsMatch
against commits.
pathsMatch - collecting all changes that match a path (against ref changes only)
boolean pathsMatch(String syntaxAndPattern, Closure matchingChangesCollector)
This is the same as pathsMatch
except that you now can collect the changes that match the specific path. After this you can perform some logic based on these changes. You need to pass in a matchingChangesCollector
closure to pathsMatch
that contains the logic you require.
For example using them to generate a hook error message to show users specifically what changes and paths were blocked in a push using the protect git references hook.
import com.atlassian.bitbucket.content.Change
def matchingChangesCollector = { Iterable<Change> matchingChanges ->
hookMessage << "The following changes containing private ssh keys were blocked:\n"
matchingChanges.each { change ->
def message = "Id: ${shortId(change.contentId)}, Path: ${change.path.toString()}\n"
hookMessage << message
}
}
refChanges.pathsMatch("glob:id_rsa", matchingChangesCollector)
private String shortId(String contentId) {
contentId.substring(0, 11)
}
Rather than appending to the hook error message you could also log the matched changes by using the following collector:
def matchingChangesCollector = { Iterable<Change> matchingChanges ->
matchingChanges.each { change ->
def message = "Change containing private ssh key was blocked > Id: ${shortId(change.contentId)}, Path: ${change.path.toString()}\n"
log.info message
}
}
The matchingChangesCollector
closure will only be called once if the push contained at least one change which had a matching path.
You are limited by what you can do with the collector in a per repository hook due to the sandboxing on the condition which is explained here. The main use case is using the condition to build up diagnostic information which is still achievable in the sandboxed environment.
pathsMatchExcludingDeletes
boolean pathsMatchExcludingDeletes(String syntaxAndPattern)
Same as pathsMatch
, except it will ignore deleted files. This can be useful when blocking certain file names using protect git refs, or via merge checks.
This will work in the same way for a pull request as pathsMatch
but will ignore deleted files in the diff.
pathsMatchExcludingDeletes (against commits for pull requests)
boolean pathsMatchExcludingDeletes(String syntaxAndPattern, Iterable<Commit> commits)
Same as pathsMatch
(against commits for pull requests), except it will ignore deleted files in the supplied commits for the pull request. This can be useful when preventing sensitive files from being merged if they exist in the commit history of the pull request using merge checks.
If you click on the Expand examples
link under the condition you will see various examples of how to pathsMatchExcludingDeletes
against commits.
pathsMatchExcludingDeletes - collecting all changes that match a path (against ref changes only)
boolean pathsMatchExcludingDeletes(String syntaxAndPattern, Closure matchingChangesCollector)
Same as pathsMatch
(collecting all changes that match a path), except it will ignore deleted files. This can be useful when blocking certain file names using protect git refs, or via merge checks.
pathsMatcher
Currently this is only available for hook conditions.
Same as pathsMatch
and pathsMatchExcludingDeletes
, except it is more expressive about what changes are being excluded.
The exclusions are:
files that have been deleted
files that are already tracked by Git LFS
The example below shows how you can adapt your condition to use this.
def matchingChangesCollector = { Iterable<Change> matchingChanges -> }
pathsMatcher //<1>
.excludingDeletes() //<2>
.excludingLfsFiles() //<3>
.withMatchingChangesCollector(matchingChangesCollector) //<4>
.matches("glob:**.jar") //<5>
Line 3: use pathsMatcher
which is provided by the condition in the binding
Line 4: optionally exclude deletes so we get the same behavior as pathsMatchExcludingDeletes
Line 5: optionally exclude any files already tracked by Git LFS
Line 6: optionally specify a matchingChangesCollector if we have a self-diagnosing condition (the provided example above does nothing)
Line 7: specify our pattern to match on (in this case any file with a jar extension)
You can remove lines 2, 3 and 5 from the example above if they are not relevant to the changes you want to match against.
If you want to be explicit about what your including as well, there are methods available on pathsMatcher
to perform inclusions. For example below we can include both deletes and LFS file changes.
pathsMatcher
.includingDeletes()
.includingLfsFiles()
.matches("glob:**.jar")
getCommitAuthors
Similarly, you can get the commit authors for all changes in a pull request or a push using this:
Set<Person> getCommitAuthors()
For example, in an event handler where you are listening for an event related to pull requests:
event.getCommitAuthors()
This will return a java.util.Set
of Person
objects.
A Person
may not actually correspond with a registered Bitbucket user.
getPaths
List<FileChange> getPaths()
You can use the `getPaths`
method when you need to know the paths of matched files, but you don't need to know if a push, pull request, or merge check contains changes in a specific path.
The `getPaths`
method is similar to `pathsMatch`
, but instead of returning true
when at least one change has been made to a file matching the pattern, `getPaths`
will return a list of the paths that were matched. You can use this list when you need to know the actual file paths or the number of matches. Or match it against multiple patterns, if running patchMatch
multiple times affects your performance.
In a merge check, this veto will block a pull request if all the changed files are of type ‘.txt’:
if( getPaths().every { it.path.endsWith('.txt') } ) {
mergeRequest.veto("rejected", "rejected")
}
Used in a pre-hook, this condition will print out all the changed files and return true
if there is exactly one changed file.
getPaths().each {
hookMessage.append("\nFile Changed:${it.path}\n")
}
getPaths().size() == 1
Same as `pathsMatch`
,the `getPaths`
methods is available against a pull request, a pull request-related event, list of commits, or individual commit.