Iterating changes in a Bitbucket PR from ScriptRunner

I’m working on a ScriptRunner merge hook that will enforce approval based on what changes are present in the PR.

I’ve been able to get this working well using PathsMatch and globs, however there is now a want to exclude changes in a “DataConfig” folder. I tried using the example spelled out in ScriptRunners docs but I got HazelCastCache errors when trying to iterate Changes and RefChanges.

Does Bitbucket expose a Java API that will let me iterate the changes present in a branch? I figure I can just compare the glob patterns with a list of files present.

Has anyone else done something like this via ScriptRunner and could provide guidance? I’m an admin and a Python/Bash/POSH guy first so Groovy and Java is something I’ve just been grokking through.

import java.io.File
import groovy.json.JsonSlurper
import com.atlassian.bitbucket.hook.repository.RepositoryHookResult
import com.atlassian.bitbucket.scm.pull.MergeRequest

def rulesFilePath = "/data/atlassian/merge_checks/merge_check_rules.json"

debugger("Loading pull request and target branch information.")
def pr = mergeRequest.pullRequest
def targetBranch = pr.toRef.displayId
def wasReviewRuleTriggered = true

// Wraps logging. We should keep logging turned off by default to ensure merge check is as quick as possible and to
// reduce log messages in Bitbucket. To enable, change shouldLog to true.
def debugger(msg) {
    def shouldLog = false
    if (shouldLog) {
        log.warn(msg)
    }
}

// Loads merge check rules from provided file path. Returns object.
def loadRulesFile(rulesFilePath) {
    debugger("Attempting to load JSON merge check rules file.")
    try {
        File rulesFile = new File(rulesFilePath)
        def json_parser = new JsonSlurper()
        def mergeCheckRules = json_parser.parse(rulesFile)
        debugger("Successfully loaded JSON rules check file from ${rulesFile}")
        return mergeCheckRules
    } catch (Exception e) {
        log.error("Failed to load rule file. Exception: ${e}")
        vetoMerge("Merge hook encountered an error while trying to parse component JSON file.",
                "Merge hook encountered an error while trying to parse component JSON file.")

    }
}

// Creates a list of users who have approved this pull request. Returns collection.
def getApprovalList() {
    debugger("Attempting to fetch all users who have approved this pull request.")
    def approvalUsers = []
    for ( user in mergeRequest.pullRequest.reviewers )
    {
        if ( user.approved )
            approvalUsers.add(user.user.name)
    }
    debugger("List of users who have approved: ${approvalUsers}")
    return approvalUsers
}

// Loops through provided file patterns until a positive match is found or all patterns are tested. Returns bool.
def doesPullRequestContainRelevantChanges(globPatterns, pr, commits) {
    debugger("Testing changes in this pull request against the following patterns: ${globPatterns}")
    for ( pattern in globPatterns )
    {
        def globSearch = "glob:${pattern}"
        if ( pr.pathsMatch(globSearch, commits) ) {
            debugger("Found the following pattern - ${globSearch} - returning true.")
            return true
        }
    }
    debugger("No patterns matched. Returning false.")
    return false
}

// Veto a Pull Request.
def vetoMerge(loggedMessage,vetoMessage) {
    debugger("VETOING PR with the following veto message: ${vetoMessage}")
    RepositoryHookResult.rejected(loggedMessage, vetoMessage)
    mergeRequest.veto(loggedMessage, vetoMessage)
}

//
def checkComponentRulesAgainstPullRequest(pullRequestRule, pr, commits, pullRequestApprovals) {
    def ruleTriggered = false
    debugger("Checking the following component rules: ${pullRequestRule.component_rule_name}.")
    if ( doesPullRequestContainRelevantChanges(pullRequestRule.glob_patterns, pr, commits) ) {
        ruleTriggered = true
        if ( pullRequestRule.component_owners.intersect(pullRequestApprovals).size < pullRequestRule.approval_count ) {
            vetoMerge("Pull Request needs additional targeted approvals based on component rule: " +
                    "${pullRequestRule.component_rule_name}", pullRequestRule.veto_message)
        } else {
            debugger("This pull request has sufficient approvals for ${pullRequestRule.component_rule_name}.")
        }
    }
    return ruleTriggered
}

// Go through each team in the JSON config file and check to see if all approvals needed are present. If they are not,
// veto the pull request. NOTE: A pull request can be vetoed multiple times depending on the number of failed checks.
if ( targetBranch == 'master' ) {
    debugger("Loading commit and rules JSON.")
    def commits = pr.getCommits()
    def mergeCheckRules = loadRulesFile(rulesFilePath)
    def pullRequestApprovals = getApprovalList()
    for ( pullRequestRule in mergeCheckRules.rules ) {
        def ruleTriggered = checkComponentRulesAgainstPullRequest(pullRequestRule, pr, commits, pullRequestApprovals)
        if ( ruleTriggered ) {
            wasReviewRuleTriggered = true
        }
    }
    if ( wasReviewRuleTriggered == false ) {
        vetoMerge("Vetoing merge because no review rules exist for this pull request.",
                "Based on the changes present in this pull request, no component review rules were detected. " +
                        "PR merge has been vetoed until relevant review rules about these components are created. " +
                        "Please reach out to USER for concerns.")
    }
} else {
    debugger("Not enforcing review checks because this pull request is targeting ${targetBranch}.")
}

Example of the input json:

{
    {
      "component_rule_name": "Component Name",
      "project_key": "JIRA_PROJECT_KEY",
      "veto_message": "Message displayed in Bamboo if merge check rule is violated. Be respectful with message length.",
      "component_owners": ["aowner", "bowner"],
      "approval_count": 1,
      "glob_patterns": ["Folder/**", "*.exe"]
    }
}