2019-12-06 16:59:35 +00:00
#!/usr/bin/env python3
# Copyright (C) 2019-present MongoDB, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the Server Side Public License, version 1,
# as published by MongoDB, Inc.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Server Side Public License for more details.
#
# You should have received a copy of the Server Side Public License
# along with this program. If not, see
# <http://www.mongodb.com/licensing/server-side-public-license>.
#
# As a special exception, the copyright holders give permission to link the
# code of portions of this program with the OpenSSL library under certain
# conditions as described in each individual source file and distribute
# linked combinations including the program with the OpenSSL library. You
# must comply with the Server Side Public License in all respects for
# all of the code used other than as permitted herein. If you modify file(s)
# with this exception, you may extend this exception to your version of the
# file(s), but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version. If you delete this
# exception statement from all source files in the program, then also delete
# it in the license file.
#
""" Validate that the commit message is ok. """
2024-05-16 18:00:17 -04:00
2024-06-18 17:50:28 -07:00
import pathlib
2024-10-10 10:59:18 -07:00
import re
2024-06-18 17:50:28 -07:00
import subprocess
2024-10-10 10:59:18 -07:00
2025-02-27 15:24:19 -08:00
import requests
2024-10-10 10:59:18 -07:00
import structlog
2024-06-18 17:50:28 -07:00
import typer
from git import Commit , Repo
2024-10-10 10:59:18 -07:00
from typing_extensions import Annotated
2021-12-29 10:57:03 -05:00
2024-06-18 17:50:28 -07:00
LOGGER = structlog . get_logger ( __name__ )
2019-12-06 16:59:35 +00:00
2020-04-28 20:46:00 -04:00
STATUS_OK = 0
STATUS_ERROR = 1
2019-12-06 16:59:35 +00:00
2024-06-18 17:50:28 -07:00
repo_root = pathlib . Path (
subprocess . run (
" git rev-parse --show-toplevel " , shell = True , text = True , capture_output = True
) . stdout . strip ( )
)
2019-12-06 16:59:35 +00:00
2024-06-18 17:50:28 -07:00
pr_template = " "
with open ( repo_root / " .github " / " pull_request_template.md " , " r " ) as r :
pr_template = r . read ( ) . strip ( )
BANNED_STRINGS = [ " https://spruce.mongodb.com " , " https://evergreen.mongodb.com " , pr_template ]
VALID_SUMMARY = re . compile ( r ' (Revert " )?(SERVER-[0-9]+|Import wiredtiger) ' )
2021-07-13 15:55:44 -04:00
2024-05-20 22:17:04 -04:00
2024-06-18 17:50:28 -07:00
def is_valid_commit ( commit : Commit ) - > bool :
2024-05-20 22:17:04 -04:00
# Valid values look like:
# 1. SERVER-\d+
# 2. Revert "SERVER-\d+
# 3. Import wiredtiger
# 4. Revert "Import wiredtiger
2024-06-18 17:50:28 -07:00
if not VALID_SUMMARY . match ( commit . summary ) :
LOGGER . error (
2025-02-27 15:24:19 -08:00
f """ Commit did not contain a valid summary.
Please update the PR title and description to match the following regular expressions { VALID_SUMMARY } .
If you are seeing this on a PR , after changing the title / description you will need to rerun this check before being able to submit your PR .
The decision to add this check was made in SERVER - 101443 , please feel free to leave comments / feedback on that ticket . """ ,
2024-06-18 17:50:28 -07:00
commit_hexsha = commit . hexsha ,
commit_summary = commit . summary ,
)
return False
2024-07-23 12:35:07 -04:00
# Remove all whitespace from comparisons. GitHub line-wraps commit messages, which adds
# newline characters that otherwise would not match verbatim such banned strings.
stripped_message = " " . join ( commit . message . split ( ) )
2024-06-18 17:50:28 -07:00
for banned_string in BANNED_STRINGS :
2024-07-23 12:35:07 -04:00
if " " . join ( banned_string . split ( ) ) in stripped_message :
2024-06-18 17:50:28 -07:00
LOGGER . error (
2025-02-27 15:24:19 -08:00
""" Commit contains banned string (ignoring whitespace).
Please update the PR title and description to not contain the following banned string .
If you are seeing this on a PR , after changing the title / description you will need to rerun this check before being able to submit your PR .
The decision to add this check was made in SERVER - 101443 , please feel free to leave comments / feedback on that ticket . """ ,
2024-06-18 17:50:28 -07:00
banned_string = banned_string ,
commit_hexsha = commit . hexsha ,
commit_message = commit . message ,
)
return False
return True
2025-02-27 15:24:19 -08:00
def get_non_merge_queue_squashed_commits (
github_org : str ,
github_repo : str ,
pr_number : int ,
github_token : str ,
) - > list [ Commit ] :
assert github_org
assert github_repo
assert pr_number > = 0
assert github_token
pr_merge_info_query = {
" query " : f """ {{
repository ( owner : " {github_org} " , name : " {github_repo} " ) { {
pullRequest ( number : { pr_number } ) { {
viewerMergeHeadlineText ( mergeType : SQUASH )
viewerMergeBodyText ( mergeType : SQUASH )
} }
} }
} } """
}
headers = { " Authorization " : f " token { github_token } " }
LOGGER . info ( " Sending request " , request = pr_merge_info_query )
req = requests . post (
url = " https://api.github.com/graphql " ,
json = pr_merge_info_query ,
headers = headers ,
timeout = 60 , # 60s
)
resp = req . json ( )
# Response will look like
# {'data': {'repository': {'pullRequest':
# {
# 'viewerMergeHeadlineText': 'SERVER-1234 Add a ton of great support (#32823)',
# 'viewerMergeBodyText': 'This PR adds back support for a lot of things\nMany great things!'
# }}}}
LOGGER . info ( " Squashed content " , content = resp )
pr_info = resp [ " data " ] [ " repository " ] [ " pullRequest " ]
fake_repo = Repo ( )
return [
Commit (
repo = fake_repo ,
binsha = b " 00000000000000000000 " ,
message = " \n " . join ( [ pr_info [ " viewerMergeHeadlineText " ] , pr_info [ " viewerMergeBodyText " ] ] ) ,
)
]
def get_merge_queue_commits ( branch_name : str ) - > list [ Commit ] :
assert branch_name
diff_commits = subprocess . run (
[ " git " , " log " , ' --pretty=format: " % H " ' , f " { branch_name } ...HEAD " ] ,
check = True ,
capture_output = True ,
text = True ,
)
# Comes back like "hash1"\n"hash2"\n...
commit_hashs : list [ str ] = diff_commits . stdout . replace ( ' " ' , " " ) . splitlines ( )
LOGGER . info ( " Diff commit hashes " , commit_hashs = commit_hashs )
repo = Repo ( repo_root )
return [ repo . commit ( commit_hash ) for commit_hash in commit_hashs ]
2024-06-18 17:50:28 -07:00
def main (
2025-02-27 15:24:19 -08:00
github_org : Annotated [
str ,
typer . Option ( envvar = " GITHUB_ORG " , help = " Name of the github organization (e.g. 10gen) " ) ,
] = " " ,
github_repo : Annotated [
str ,
typer . Option ( envvar = " GITHUB_REPO " , help = " Name of the repo (e.g. mongo) " ) ,
] = " " ,
2024-06-18 17:50:28 -07:00
branch_name : Annotated [
str ,
typer . Option ( envvar = " BRANCH_NAME " , help = " Name of the branch to compare against HEAD " ) ,
2025-02-27 15:24:19 -08:00
] = " " ,
pr_number : Annotated [
int ,
typer . Option ( envvar = " PR_NUMBER " , help = " PR Number to compare with " ) ,
] = - 1 ,
github_token : Annotated [
str ,
typer . Option ( envvar = " GITHUB_TOKEN " , help = " Github token with pr read access " ) ,
] = " " ,
requester : Annotated [
2024-06-18 17:50:28 -07:00
str ,
typer . Option (
2025-02-27 15:24:19 -08:00
envvar = " REQUESTER " ,
help = " What is requested this task. Defined https://docs.devprod.prod.corp.mongodb.com/evergreen/Project-Configuration/Project-Configuration-Files#expansions. " ,
2024-06-18 17:50:28 -07:00
) ,
] = " " ,
) :
"""
Validate the commit message .
It validates the latest message when no arguments are provided .
"""
2025-02-27 15:24:19 -08:00
commits : list [ Commit ] = [ ]
if requester == " github_merge_queue " :
commits = get_merge_queue_commits ( branch_name )
elif requester == " github_pr " :
commits = get_non_merge_queue_squashed_commits (
github_org , github_repo , pr_number , github_token
)
else :
LOGGER . error ( " Running with an invalid requester " , requester = requester )
raise typer . Exit ( code = STATUS_ERROR )
2024-06-18 17:50:28 -07:00
2025-02-27 15:24:19 -08:00
for commit in commits :
2024-06-18 17:50:28 -07:00
if not is_valid_commit ( commit ) :
LOGGER . error ( " Found an invalid commit " , commit = commit )
raise typer . Exit ( code = STATUS_ERROR )
2024-05-20 22:17:04 -04:00
2024-06-18 17:50:28 -07:00
return
2019-12-06 16:59:35 +00:00
if __name__ == " __main__ " :
2024-06-18 17:50:28 -07:00
typer . run ( main )