pw_presubmit: Check commit messages

Check that commit messages follow the 50/72 rule. To allow diagrams,
paths, and URLs, lines longer than 72 characters are permitted if they
contain :, /, or non-ASCII characters.

Change-Id: I50e0c84947f210b20f3a309903f987a184abe510
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/12902
Reviewed-by: Rob Mohr <mohrr@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_presubmit/py/pw_presubmit/git_repo.py b/pw_presubmit/py/pw_presubmit/git_repo.py
index 5f29ae1..8b1c8ee 100644
--- a/pw_presubmit/py/pw_presubmit/git_repo.py
+++ b/pw_presubmit/py/pw_presubmit/git_repo.py
@@ -186,3 +186,7 @@
             continue
 
     return package_dirs
+
+
+def commit_message(commit: str = 'HEAD', repo: PathOrStr = '.') -> str:
+    return git_stdout('log', '--format=%B', '-n1', commit, repo=repo)
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index 3a36ae8..dbaedf9 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -34,8 +34,8 @@
 
 from pw_presubmit import build, cli, environment, format_code, git_repo
 from pw_presubmit import python_checks
-from pw_presubmit import call, filter_paths, PresubmitContext, PresubmitFailure
-from pw_presubmit import Programs
+from pw_presubmit import call, filter_paths, plural, PresubmitContext
+from pw_presubmit import PresubmitFailure, Programs
 from pw_presubmit.install_hook import install_hook
 
 _LOG = logging.getLogger(__name__)
@@ -290,8 +290,7 @@
 
     if errors:
         _LOG.warning('%s with a missing or incorrect copyright notice:\n%s',
-                     pw_presubmit.plural(errors, 'file'),
-                     '\n'.join(str(e) for e in errors))
+                     plural(errors, 'file'), '\n'.join(str(e) for e in errors))
         raise PresubmitFailure
 
 
@@ -331,6 +330,54 @@
     call('pyoxidizer', 'build', cwd=ctx.output_dir)
 
 
+def commit_message_format(_: PresubmitContext):
+    """Checks that the top commit's message is correctly formatted."""
+    lines = git_repo.commit_message().splitlines()
+
+    if not lines:
+        _LOG.error('The commit message is too short!')
+        raise PresubmitFailure
+
+    errors = 0
+
+    if len(lines[0]) > 50:
+        _LOG.warning("The commit message's first line must be no longer than "
+                     '50 characters.')
+        _LOG.warning('The first line is %d characters:\n  %s', len(lines[0]),
+                     lines[0])
+        errors += 1
+
+    if lines[0].endswith('.'):
+        _LOG.warning(
+            "The commit message's first line must not end with a period:\n %s",
+            lines[0])
+        errors += 1
+
+    if len(lines) > 1 and lines[1]:
+        _LOG.warning("The commit message's second line must be blank.")
+        _LOG.warning('The second line has %d characters:\n  %s', len(lines[1]),
+                     lines[1])
+        errors += 1
+
+    # Check that the lines are 72 characters or less, but skip any lines that
+    # might possibly have a URL, path, or metadata in them. Also skip any lines
+    # with non-ASCII characters.
+    for i, line in enumerate(lines[2:], 3):
+        if ':' in line or '/' in line or not line.isascii():
+            continue
+
+        if len(line) > 72:
+            _LOG.warning(
+                'Commit message lines must be no longer than 72 characters.')
+            _LOG.warning('Line %d has %d characters:\n  %s', i, len(line),
+                         line)
+            errors += 1
+
+    if errors:
+        _LOG.error('Found %s in the commit message', plural(errors, 'error'))
+        raise PresubmitFailure
+
+
 #
 # Presubmit check programs
 #
@@ -350,6 +397,7 @@
 )
 
 QUICK = (
+    commit_message_format,
     init_cipd,
     init_virtualenv,
     copyright_notice,