Skip to main content
Example scripts
arrow icon
To homepage
Bitbucket
Data centre icon
Data Center

Custom Bitbucket Commit Message Validation

Features
Pre hooks
Tags
Created 1 year ago, Updated 3 month(s) ago
App in script
ScriptRunner For Bitbucket
ScriptRunner For Bitbucket
by Adaptavist
Compatibility
compatibility bullet
Bitbucket (6.0 - 7.17)
compatibility bullet
ScriptRunner For Bitbucket (7.10.0)
Language |
groovy
/**
 * Copyright © 2020, StoneX/Gain Capital
 *
 * This script is released under the BSD 3-clause license: https://opensource.org/licenses/BSD-3-Clause
 *
 * Pre-hook checks if the message begins with ticket key in order to achieve better linkage of stories, builds,
 * deployments and change request tickets through automated pipelines and improved audit.
 */

boolean isSquashedCommit(String[] commitMessage) {
    // We only handle squashed commits done via Bitbucket when merging PR
    // For local squashed merge, the user is expected to add [JIRA-ID]
    def isSquashedCommit = false
    def isPrMerge = (commitMessage[0] =~ /^Merge pull request #\d+ in.*/).find()

    if (isPrMerge) {
        isSquashedCommit = (commitMessage[2] =~ /^Squashed commit of the following:$/).find()

        if (isSquashedCommit) {
            // We check commits in the squashed commit. This is just additional check to deter
            // spoofing and there should be at least one commit ID
            isSquashedCommit = false
            commitMessage.any { line ->
                // E.g. commit 3414e31e819bcff53b8c9e4387c1eaec840a294e
                // Commit hash is always length of 40
                def commitIdFound = (line =~ /^commit [A-Za-z0-9]{40,40}/).find()

                if (commitIdFound) {
                    isSquashedCommit = true
                    return true // break 'any' closure
                }
            }
        }
    }

    isSquashedCommit
}

try {
    def success = true

    refChanges.each { refChange ->
        refChange.getChangesets(repository).each { changeset ->
            def commit = changeset.toCommit
            def commitMessage = changeset.toCommit.message
            def splitMessage = commitMessage.split('\n')
            def committer = changeset.toCommit.committer.name.toLowerCase()
            def author = changeset.toCommit.author.name.toLowerCase()
            // The names of all service accounts begin with "svc."
            def isCommitterSvc = (committer =~ /^svc.*/).find()
            def isAuthorSvc = (author =~ /^svc.*/).find()
            //Merge commits have two parents. If size is greater than 0 then it means its a merge commit
            def isItMergeCommit = commit.parents.size() > 1
            def isItRevertCommit = false
            def isSquashedCommit = (isSquashedCommit(splitMessage as String[]) && commit.parents.size() == 1)

            if (commitMessage.startsWith('Revert')) {
                isItRevertCommit = (splitMessage.size() >= 3 && splitMessage[0] =~ /^Revert/).find() && (splitMessage[2] =~ /^This reverts commit/).find()
            }

            if (!commitMessage) {
                // This should not happen as we have commit message control so we simply log and ignore
                log.warn("No commit message in ${commit.displayId}")
            } else if (!isItMergeCommit && !isItRevertCommit && !isCommitterSvc && !isAuthorSvc && !isSquashedCommit) {
                // Message have to begin with [JIRA-ID] or "[EMERGENCY]" word
                def issueMatcher = commitMessage =~ /^\s*\[+[A-Z0-9]+-[0-9]+\]+|^\s*\[EMERGENCY\]/
                def isIssueInMessage = issueMatcher.find()

                if (!isIssueInMessage) {
                    hookResponse.out().println("Commit " +
                            commit.displayId +
                            " message must contain Jira ticket ID associated with this commit." +
                            " https://.../display/ABC/Commit+JIRA+id+control")
                    success = false
                }
            }
        }
    }
    return success
}
catch (Exception ex) {
    log.error("Exception: ${ex}")
    return false
}
Having an issue with this script?
Report it here