diff --git a/tools/bin/mbedtls-compare-merges b/tools/bin/mbedtls-compare-merges new file mode 100755 index 00000000..db6ab5b6 --- /dev/null +++ b/tools/bin/mbedtls-compare-merges @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Compare merges between two TF-PSA-Crypto or Mbed TLS branches. + +Invoke this script from a git checkout. +""" + +import argparse +import os +import re +import subprocess +import sys +from typing import Dict, Iterator, List, NamedTuple + + +class MergedPR(NamedTuple): + sha: str + pull: int + description: str + + +Options = argparse.Namespace + + +def run_git(options: Options, cmd: List[str]) -> str: + """Run git and return the text output. + + Remove newline or null terminators. Assume UTF-8 encoding. + """ + raw = subprocess.check_output(['git'] + cmd, + cwd=options.directory, + encoding='utf-8') + return raw.rstrip('\0\n') + + +def get_base(options: Options) -> str: + """Get the base commit (merge base of the reference branch and backport branch).""" + output = run_git(options, + ['merge-base', options.backport_branch, options.reference_branch]) + return output + + +def parse_commit_info(info: str) -> MergedPR: + sha, subject, body = info.splitlines() + m = re.match(r'Merge pull request #([0-9]+)', subject) + pull = int(m.group(1)) + description = re.sub(r'(\[backport[^][]*\]|\[[0-9.]+\]:? *|backport +[0-9.]+)\W', '', body, flags=re.I).strip() + return MergedPR(sha, pull, description) + +def get_merges(options: Options, base: str, head: str) -> List[MergedPR]: + """Get pull request merged on the given branch in the given interval.""" + cmd = ['log', '--merges', '--grep=^Merge pull request '] + cmd += ['--format=%h\n%s\n%b', '-z'] + if options.since: + cmd.append('--since=options.since') + cmd.append(base + '..' + head) + output = run_git(options, cmd) + commits = output.split('\0') + return [parse_commit_info(info) for info in commits] + + +def missing_merges(dict1: Dict[str, MergedPR], + dict2: Dict[str, MergedPR]) -> Iterator[MergedPR]: + """Find merges in the first list that are missing from the second list.""" + for description, pr in dict1.items(): + if description not in dict2: + yield pr + +def report_missing(title: str, pr_list: List[MergedPR]) -> None: + """Report the given pull requests. + + Produce no output if the list is empty + """ + if not pr_list: + return + print(title) + for pr in sorted(pr_list, key=lambda pr: pr.pull): + print(f'#{pr.pull}\t{pr.description}') + +def compare_merges(reference_list: List[MergedPR], + backport_list: List[MergedPR]) -> None: + """Compare merges in the two list.""" + reference_dict = {pr.description: pr for pr in reference_list} + backport_dict = {pr.description: pr for pr in backport_list} + not_backported = list(missing_merges(reference_dict, backport_dict)) + report_missing('No backport:', not_backported) + backport_only = list(missing_merges(backport_dict, reference_dict)) + report_missing('Only in LTS:', backport_only) + + +def main(argv: List[str]) -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--backport-branch', '-b', + metavar='BRANCH', + required=True, + help='Backport branch') + parser.add_argument('--directory', '-C', + metavar='DIR', + default=os.curdir, + help='Use this directory as the git worktree') + parser.add_argument('--reference-branch', '-a', + metavar='BRANCH', + default='development', + help='Reference branch') + parser.add_argument('--since', '-s', + metavar='DATE', + help='Only consider commits since this date') + options = parser.parse_args() + base = get_base(options) + reference = get_merges(options, base, options.reference_branch) + backport = get_merges(options, base, options.backport_branch) + compare_merges(reference, backport) + +if __name__ == '__main__': + main(sys.argv[1:])