1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
|
#!/usr/bin/env python
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import ast
import logging
import re
import sys
# http://noyobo.com/2015/11/13/ANSI-escape-code.html
# Reset all to default: 0
# Bold or increased intensity: 1
# Fraktur (Gothic): 20
# red: 31
# green: 32
# yellow: 33
# grey: 38
reset = "\x1b[0m"
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
green = "\x1b[32;20m"
yellow = "\x1b[33;20m"
grey = "\x1b[38;20m"
format = "%(message)s"
TITLE = sys.argv[1]
BODY = sys.argv[2]
words_to_check = {
'Add': r'\b(added|adding|adds)\b',
'Allow': r'\b(allowed|allowing|allows)\b',
'Change': r'\b(changed|changing|changes)\b',
'Deprecate': r'\b(deprecated|deprecating|deprecates)\b',
'Disable': r'\b(disabled|disabling|disables)\b',
'Enable': r'\b(enabled|enabling|enables)\b',
'Fix': r'\b(fixed|fixing|fixes)\b',
'Improve': r'\b(improved|improving|improves)\b',
'Make': r'\b(made|making|makes)\b',
'Move': r'\b(moved|moving|moves)\b',
'Rename': r'\b(renamed|renaming|renames)\b',
'Replace': r'\b(replaced|replacing|replaces)\b',
'Remove': r'\b(removed|removing|removes)\b',
'Support': r'\b(supported|supporting|supports)\b',
'Update': r'\b(updated|updating|updates)\b',
'Upgrade': r'\b(upgraded|upgrading|upgrades)\b',
}
class CustomFormatter(logging.Formatter):
# logging with color
FORMATS = {
logging.DEBUG: grey + format + reset,
logging.INFO: grey + format + reset,
logging.WARNING: yellow + format + reset,
logging.ERROR: red + format + reset,
logging.CRITICAL: bold_red + format + reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(CustomFormatter())
logger.addHandler(ch)
def main():
logger.info("Start check pull request ...\n")
title = TITLE
body = BODY.split('\r\n')
sys.exit(1) if check_pull_request(title, body) else sys.exit(0)
def get_list(line):
# exclude list
# ['app']
# ['-c', 'a b', 'a b']
# ["-c"]
# [1]
# ['-c', 'System.Object[] System.Object[]', 'System.Object[] System.Object[]']
ref = re.findall(r'([\[].*[\]])', line)
try:
if isinstance(ast.literal_eval(ref[0]), list):
return True
except:
return False
def check_pull_request(title, body):
if title.startswith('[') or title.startswith('{'):
error_flag = False
# only check title which start with `[`
if title.startswith('['):
error_flag = check_line(title)
if '**History Notes**' in body:
is_edit_history_note = False
history_note_error_flag = False
for line in body:
if line.startswith('['):
# get component name in []
ref = re.findall(r'[\[](.*?)[\]]', line)
if ref and ref[0] not in ['Component Name 1', 'Component Name 2'] \
and not get_list(line) \
and not re.findall(r'[\[](.*?)[\]]\((.*)\)', line): # exclude [Markdown](url)
is_edit_history_note = True
history_note_error_flag = check_line(line) or history_note_error_flag
# If the `History Notes` is edited:
# Use the history notes check result (history_note_error_flag), ignore the title check result (error_flag).
error_flag = error_flag if not is_edit_history_note else history_note_error_flag
else:
logger.error('Pull Request title should start with [ or { , Please follow https://aka.ms/submitAzPR')
error_flag = True
return error_flag
def check_line(line):
# check every line
error_flag = False
# Check Fix #number in title, just give a warning here, because it is not necessarily.
if 'Fix' in line:
sub_pattern = r'#\d'
ref = re.findall(sub_pattern, line)
if not ref:
logger.warning('If it\'s related to fixing an issue, put Fix #number in title\n')
for idx, i in enumerate(line):
# ] } : must be followed by a space
if i in [']', '}', ':']:
try:
assert line[idx + 1] == ' '
except:
logger.info('%s%s: missing space after %s', line, yellow, i)
logger.error(' ' * idx + '↑')
error_flag = True
# az xxx commands must be enclosed in `, e.g., `az vm`
if idx + 2 < len(line) and i == 'a' and line[idx + 1] == 'z' and line[idx + 2] == ' ':
command = 'az '
index = idx + 3
while index < len(line) and line[index] != ':':
command += line[index]
index += 1
try:
assert line[idx - 1] == '`'
except:
logger.info('%s%s: missing ` around %s', line, yellow, command)
logger.error(' ' * idx + '↑' + ' ' * (index - idx - 2) + '↑')
error_flag = True
if i == ':':
# check extra space character before colon
if idx - 1 >= 0 and line[idx - 1] == ' ':
logger.info('%s%s: please delete extra space character before :', line, yellow)
logger.error(' ' * (idx - 1) + '↑')
error_flag = True
# First word after the colon must be capitalized
index = 0
if line[idx + 1] == ' ' and line[idx + 2].islower() and line[idx + 2: idx + 5] != 'az ':
index = idx + 2
elif line[idx + 1] != ' ' and line[idx + 1].islower() and line[idx + 1: idx + 4] != 'az ':
index = idx + 1
if index:
logger.info('%s%s: should use capital letters after :', line, yellow)
logger.error(' ' * index + '↑')
error_flag = True
# --xxx parameters must be enclosed in `, e.g., `--size`
# Add check to ensure parameters use --x-x-x format
if i == '-' and line[idx + 1] == '-':
param = '--'
index = idx + 2
while index < len(line) and line[index] != ' ':
if line[index] == '_':
logger.info('%s%s: parameter should use --x-x-x format, please replace _ with -', line, yellow)
logger.error(' ' * index + '↑')
error_flag = True
param += line[index]
index += 1
try:
assert line[idx - 1] == '`'
except:
logger.info('%s%s: missing ` around %s', line, yellow, param)
logger.error(' ' * idx + '↑' + ' ' * (index - idx - 2) + '↑')
error_flag = True
# verb check: only check the first word after ] or :
if i in [']', ':']:
word = ''
c = index = idx + 1 if line[idx + 1] != ' ' else idx + 2
while index < len(line) and line[index] != ' ':
word += line[index]
index += 1
for k, v in words_to_check.items():
if re.findall(v, word, re.IGNORECASE):
logger.info(line)
logger.error(' ' * c + '↑')
logger.warning(
'Please use the right verb of%s %s %swith %s(%s)%s simple present tense in base form '
'and capitalized first letter to describe what is done, '
'follow https://aka.ms/submitAzPR\n', red, word, yellow, green, k, yellow)
error_flag = True
break
# check extra consecutive spaces
if i == ' ' and (idx + 1) < len(line) and line[idx + 1] == ' ':
logger.info('%s%s: please delete the extra space character', line, yellow)
logger.error(' ' * (idx + 1) + '↑')
error_flag = True
# last character check
if line[-1] in ['.', ',', ' ']:
logger.info('%s%s: please delete the last character', line, yellow)
logger.error(' ' * idx + '↑')
error_flag = True
return error_flag
if __name__ == '__main__':
main()
|