From 40cd8408d8dfc0db961f4a9768a0c03fcf3705af Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Fri, 10 Sep 2021 08:24:18 +0900 Subject: [PATCH] CI: Check commit message compliance This commit adds these checks. - Title consists of module name(s) and subject or just subject. - If there are two or more module names, the module names has to be separated by a comma followed by an optional space. - First word of the subject is starting from an upper case letter and remaining characters in the first word are lower case letters. "Don't" and a word with a hyphen at the middle are also acceptable. - The title is 72 characters at max including module prefix. - If the subject exceed 50 characters excluding the module prefix, display a warning message. - Titles for commits of revert and merge are ignored. - Full description lines are 72 columns max. --- .github/workflows/commit-msg.yml | 24 ++++++ CI/check-log-msg.py | 132 +++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 .github/workflows/commit-msg.yml create mode 100755 CI/check-log-msg.py diff --git a/.github/workflows/commit-msg.yml b/.github/workflows/commit-msg.yml new file mode 100644 index 00000000000000..3dc7c26bd05953 --- /dev/null +++ b/.github/workflows/commit-msg.yml @@ -0,0 +1,24 @@ +name: Commit Message Check + +on: [pull_request] + +jobs: + ubuntu64: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup environment + run: | + pip3 install -U gitpython + + - name: Fetch OBS Studio master + run: | + git fetch origin + + - name: Check the log messages + run: | + ./CI/check-log-msg.py origin/master.. diff --git a/CI/check-log-msg.py b/CI/check-log-msg.py new file mode 100755 index 00000000000000..cfde0daf7a9922 --- /dev/null +++ b/CI/check-log-msg.py @@ -0,0 +1,132 @@ +#! /bin/env python3 + +import git +import re + +has_error = False + +LOG_ERROR = 100 +LOG_WARNING = 200 +LOG_INFO = 300 +current_commit = None + +def blog(level, txt): + if level == LOG_ERROR: + global has_error + has_error = True + s_level = "Error: " + elif level == LOG_WARNING: + s_level = "Warning: " + elif level == LOG_INFO: + s_level = "Info: " + print('{level}commit {hexsha}: {txt}'.format(level = s_level, hexsha = current_commit.hexsha[:9], txt = txt)) + +def find_directory_name(name, tree, max_depth): + for t in tree.trees: + if name == t.name: + return True + if max_depth > 1: + for t in tree.trees: + if find_directory_name(name, t, max_depth - 1): + return True + for b in tree.blobs: + if name == b.name: + return True + return False + +def check_path(path, tree): + paths = path.split('/', 1) + name = paths[0] + if len(paths) == 1: + return find_directory_name(name, tree, 1) + else: + for t in tree.trees: + if name == t.name: + return check_path(paths[1], t) + return False + +def find_submodule_name(name, submodules): + for s in submodules: + sname = s.name.split('/')[-1] + if name == sname: + return True + +def check_module_names(names): + for name in names: + if name.find('/') >= 0: + if check_path(name, current_commit.tree): + return True + else: + if find_directory_name(name, current_commit.tree, 3): + return True + # TODO: Cannot handle removed submodules. Implement to parse .gitmodules file. + if find_submodule_name(name, current_commit.repo.submodules): + return True + blog(LOG_ERROR, "unknown module name '%s'" % name) + +def check_message_title(title): + global has_error + title_split = title.split(': ', 1) + if len(title_split) == 2: + check_module_names(re.split(r', *', title_split[0])) + title_text = title_split[1] + else: + title_text = title_split[0] + + if len(title) > 72: + blog(LOG_ERROR, 'Too long title: %s' % title) + + if len(title_text) > 50: + blog(LOG_WARNING, 'Too long title excluding module name: %s' % title_text) + + if not re.match(r"([A-Z][a-z]*(-[a-z]+)*|Don't) ", title_text): + blog(LOG_ERROR, 'Invalid first word: %s' % title_text.split(' ', 1)[0]) + has_error = True + +def check_message_body(body): + for line in body.split('\n'): + if len(line) > 72 and line.find(' ') > 0: + blog(LOG_ERROR, 'Too long description in a line: %s' % line) + pass + +def check_message(c): + msg = c.message.split('\n', 2) + if len(msg) == 0: + blog(LOG_ERROR, 'Commit message is empty.') + return True + + if re.match(r'Revert ', msg[0]): + return False + if re.match(r'Merge [0-9a-f]{40} into [0-9a-f]{40}$', msg[0]): + return False + if re.match(r'Merge pull request', msg[0]): + return False + + if len(msg) > 0: + check_message_title(msg[0]) + if len(msg) > 1: + if len(msg[1]): + blog(LOG_ERROR, '2nd line is not empty.') + if len(msg) > 2: + check_message_body(msg[2]) + return has_error + +def main(): + import sys + import argparse + parser = argparse.ArgumentParser(prog='check-log-msg.py', description='Log message compliance checker') + parser.add_argument('commits') + args = parser.parse_args() + + repo = git.Repo('.') + global has_error + for c in repo.iter_commits(args.commits): # '27.1.0-rc2..27.1.0'): + global current_commit + current_commit = c + blog(LOG_INFO, "Checking commit '%s'" % c.message.split('\n', 1)[0]) + check_message(c) + if has_error: + sys.exit(1) + +if __name__ == '__main__': + ret = main()