contrib: Add gen_uprev_msg.py

Add gen_uprev_msg.py to parse ChangeLog from CPCon, and generate commit
message of uprev CL (such as CL:*2351594).

BUG=none
TEST=none

Change-Id: Ic3302af90526aaa780aacc98be0e3295f1a7810c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2124038
Reviewed-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Hung-Te Lin <hungte@chromium.org>
Tested-by: Yu-Ping Wu <yupingso@chromium.org>
diff --git a/contrib/gen_uprev_msg.py b/contrib/gen_uprev_msg.py
new file mode 100755
index 0000000..20af4db
--- /dev/null
+++ b/contrib/gen_uprev_msg.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-"
+#
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Parse ChangeLog from CPCon and generate commit message.
+
+Usage:
+    1. Copy ChangeLog from CPCon (everything except the MENU bar)
+    2. xsel -b | ./gen_uprev_msg.py [-b BOARD] [--extra-repo-file FILE]
+    3. Commit message will be printed to stdout
+    4. Be aware of the warning messages in stderr
+"""
+from __future__ import print_function
+
+import argparse
+import collections
+import logging
+import re
+import sys
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger()
+
+MAX_LENGTH = 72
+CHANGES_PATTERN = r'Changes between ([0-9\.]+) and ([0-9\.]+)'
+
+DEFAULT_REPOS = (
+    'src/overlays',
+    'src/platform/bmpblk',
+    'src/platform/depthcharge',
+    'src/platform/ec',
+    'src/platform/firmware',
+    'src/platform/vboot_reference',
+    'src/third_party/arm-trusted-firmware',
+    'src/third_party/chromiumos-overlay',
+    'src/third_party/coreboot',
+    'src/third_party/coreboot/3rdparty/blobs',
+)
+
+
+CL = collections.namedtuple('CL', ['commit', 'cl', 'bug', 'title'])
+
+
+def read_extra_repos(filename):
+    """Read extra repos from |filename|."""
+    repos = set()
+    with open(filename) as f:
+        for line in f:
+            repo = line.strip()
+            if repo:
+                repos.add(repo)
+    return repos
+
+
+def parse_cl(line):
+    """Parse CL."""
+    tokens = line.split('\t')
+    if len(tokens) != 6:
+        return None
+    commit, cl, bug, _date, _author, title = tokens
+    return CL(commit, int(cl) if cl else None, int(bug) if bug else None, title)
+
+
+def main(args):
+    """Parse ChangeLog and print commit message."""
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('-b', '--board')
+    parser.add_argument('--extra-repo-file',
+                        help='File containing extra repo names')
+    args = parser.parse_args(args)
+    board = args.board or 'BOARD'
+    included_repos = set(DEFAULT_REPOS)
+    if args.extra_repo_file:
+        included_repos |= read_extra_repos(args.extra_repo_file)
+
+    changes = False
+    repos = []
+    repo = None
+    ignored_repos = []
+    skipped_lines = []
+    for line in sys.stdin:
+        line = line.strip()
+
+        # Parse "Changes between 12573.80.0 and 12573.88.0"
+        if not changes:
+            m = re.match(CHANGES_PATTERN, line)
+            if m:
+                groups = m.groups()
+                if len(groups) == 2:
+                    changes = groups
+            continue
+
+        # Parse repo
+        tokens = line.split()
+        if len(tokens) == 1 and '/' in tokens[0]:
+            repo = tokens[0]
+            if repo in included_repos:
+                cl_list = []
+                repos.append((repo, cl_list))
+            else:
+                ignored_repos.append(repo)
+                repo = None
+            continue
+
+        # Parse CL
+        if not repo:
+            continue
+
+        cl = parse_cl(line)
+        if cl:
+            cl_list.append(cl)
+        else:
+            skipped_lines.append(line)
+            continue
+
+    if not repos:
+        logger.error('No repo found from ChangeLog')
+        return 1
+
+    # Output
+    if changes:
+        title = (f'chromeos-firmware-{board}: '
+                 'Uprev firmware to {changes[1]} for {board}')
+        print(title)
+        print()
+
+    print(f'Changes between {changes[0]} and {changes[1]}:')
+
+    bugs = set()
+    for repo, cl_list in repos:
+        print()
+        print(repo)
+        private = 'private' in repo
+        for cl in cl_list:
+            if not cl.cl:
+                continue
+            cl_str = f'CL:*{cl.cl}' if private else f'CL:{cl.cl}'
+            title_max_len = MAX_LENGTH - len(cl_str) - 1 - 4
+            title = cl.title
+            while len(title) > title_max_len:
+                tokens = title.rsplit(None, 1)
+                if len(tokens) <= 1:
+                    break
+                title = tokens[0]
+            line = f' {cl_str}\t{title}'
+            print(line)
+            if cl.bug:
+                bugs.add(cl.bug)
+
+    print()
+    print('BRANCH=none')
+    bugs = [f'b:{bug}' if bug >= 1e9 else f'chromium:{bug}'
+            for bug in sorted(bugs)]
+    line_bugs = []
+    length = len('BUG=')
+    for bug in bugs:
+        if line_bugs:
+            bug = ', ' + bug
+        if length + len(bug) <= MAX_LENGTH:
+            line_bugs.append(bug)
+            length += len(bug)
+        else:
+            print('BUG=' + ''.join(line_bugs))
+            line_bugs = []
+            length = 0
+    if line_bugs:
+        print('BUG=' + ''.join(line_bugs))
+    print(f'TEST=emerge-{board} chromeos-firmware-{board}')
+
+    # Warnings
+    for repo in ignored_repos:
+        logger.warning('Ignore repo %s', repo)
+    for line in skipped_lines:
+        logger.warning('Skipping line: %s', line)
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))