| #!/usr/bin/env python3 | |
| # Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file | |
| # for details. All rights reserved. Use of this source code is governed by a | |
| # BSD-style license that can be found in the LICENSE file. | |
| import subprocess | |
| import sys | |
| import re | |
| import r8_release | |
| class Branch: | |
| def __init__(self, name, first, last=None): | |
| self.name = name | |
| self.first = first | |
| self.last = last # optional last for testing purposes. | |
| def origin(self): | |
| return "origin/%s" % self.name | |
| def last_or_origin(self): | |
| return self.last if self.last else self.origin() | |
| def __str__(self): | |
| return self.name | |
| # The initial commit is the furthest back we need to search on main. | |
| # Currently, it is the merge point of main onto 4.0.23-dev | |
| MAIN = Branch('main', 'a2e203580aa00a36f85cd68d3d584b97aef34d59') | |
| OLDEST_BRANCH_VERSION = (4, 0) | |
| DEV_BRANCH_VERSION = [int(s) for s in r8_release.R8_DEV_BRANCH.split('.')] | |
| # List of change ids that should not be reported. | |
| IGNORED = ['I92d7bf3afbf609fdea21683941cfd15c90305cf2'] | |
| VERBOSE = False | |
| # Helper to call and decode a shell command. | |
| def run_cmd(cmd): | |
| if VERBOSE: | |
| print(' '.join(cmd)) | |
| return subprocess.check_output(cmd).decode('UTF-8') | |
| # Comparator on major and minor branch versions. | |
| def branch_version_less_than(b1, b2): | |
| if b1[0] < b2[0]: | |
| return True | |
| if b1[0] == b2[0] and b1[1] < b2[1]: | |
| return True | |
| return False | |
| # Find all release branches between OLDEST_BRANCH and DEV_BRANCH | |
| def get_release_branches(): | |
| # Release branches are assumed to be of the form 'origin/X.Y' | |
| out = run_cmd(['git', 'branch', '-r', '-l']) | |
| natsort = lambda s: [ | |
| int(t) if t.isdigit() else t.lower() for t in re.split(r'(\d+)', s) | |
| ] | |
| lines = sorted(out.split('\n'), key=natsort) | |
| pattern = re.compile(r'origin/(\d+).(\d+)') | |
| releases = [] | |
| for line in lines: | |
| m = pattern.search(line.strip()) | |
| if m: | |
| major = m.group(1) | |
| minor = m.group(2) | |
| if major and minor: | |
| candidate = (int(major), int(minor)) | |
| if branch_version_less_than(candidate, OLDEST_BRANCH_VERSION): | |
| continue | |
| if branch_version_less_than(candidate, DEV_BRANCH_VERSION): | |
| releases.extend(find_dev_cutoff(candidate)) | |
| return releases | |
| # Find the most recent commit hash that is for a -dev or -alpha version. | |
| # This is the starting point for the map of commits after cutoff from main. | |
| def find_dev_cutoff(branch_version): | |
| out = run_cmd([ | |
| 'git', | |
| 'log', | |
| 'origin/%d.%d' % branch_version, | |
| '--grep', | |
| r'Version .*\(-dev\|-alpha[[:digit:]]\+\)', | |
| '--pretty=oneline', | |
| ]) | |
| # Format of output is: <hash> Version <version>-(dev|alpha<N>) | |
| try: | |
| hash = out[0:out.index(' ')] | |
| return [Branch('%d.%d' % branch_version, hash)] | |
| except ValueError: | |
| throw_error("Failed to find dev cutoff for branch %d.%d" % | |
| branch_version) | |
| # Build a map from each "Change-Id" hash to the hash of its commit. | |
| def get_change_id_map(branch): | |
| out = run_cmd( | |
| ['git', 'log', | |
| '%s..%s' % (branch.first, branch.last_or_origin())]) | |
| map = {} | |
| current_commit = None | |
| for line in out.split('\n'): | |
| if line.startswith('commit '): | |
| current_commit = line[len('commit '):] | |
| assert len(current_commit) == 40 | |
| elif line.strip().startswith('Change-Id: '): | |
| change_id = line.strip()[len('Change-Id: '):] | |
| assert len(change_id) == 41 | |
| map[change_id] = current_commit | |
| return map | |
| # Check if a specific commit is present on a specific branch. | |
| def is_commit_in(commit, branch): | |
| out = run_cmd( | |
| ['git', 'branch', '-r', | |
| branch.origin(), '--contains', commit]) | |
| return out.strip() == branch.origin() | |
| def main(): | |
| found_errors = False | |
| # The main map is all commits back to the "init" point. | |
| main_map = get_change_id_map(MAIN) | |
| # Compute the release branches. | |
| release_branches = get_release_branches() | |
| # Populate the release maps with all commits after the last -dev point. | |
| release_maps = {} | |
| for branch in release_branches: | |
| release_maps[branch.name] = get_change_id_map(branch) | |
| # Each branch is then compared forwards with each subsequent branch. | |
| for i in range(len(release_branches)): | |
| branch = release_branches[i] | |
| newer_branches = release_branches[i + 1:] | |
| if (len(newer_branches) == 0): | |
| print('Last non-dev release branch is %s, nothing to check.' % | |
| branch) | |
| continue | |
| print('Checking branch %s.' % branch) | |
| changes = release_maps[branch.name] | |
| cherry_picks_count = 0 | |
| for change in changes.keys(): | |
| is_cherry_pick = False | |
| missing_from = None | |
| commit_on_main = main_map.get(change) | |
| for newer_branch in newer_branches: | |
| if change in release_maps[newer_branch.name]: | |
| is_cherry_pick = True | |
| # If the change is in the release mappings check for holes. | |
| if missing_from: | |
| found_errors |= change_error( | |
| change, 'Error: missing Change-Id %s on branch %s. ' | |
| 'Is present on %s and again on %s.' % ( | |
| change, | |
| missing_from, | |
| branch, | |
| newer_branch, | |
| )) | |
| elif commit_on_main: | |
| is_cherry_pick = True | |
| # The change is not in the non-dev part of the branch, so we need to | |
| # check that the fork from main included the change. | |
| if not is_commit_in(commit_on_main, newer_branch): | |
| found_errors |= change_error( | |
| change, 'Error: missing Change-Id %s on branch %s. ' | |
| 'Is present on %s and on main as commit %s.' % | |
| (change, newer_branch, branch, commit_on_main)) | |
| else: | |
| # The change is not on "main" so we just record for holes on releases. | |
| missing_from = newer_branch | |
| if is_cherry_pick: | |
| cherry_picks_count += 1 | |
| print('Found %d cherry-picks (out of %d commits).' % | |
| (cherry_picks_count, len(changes))) | |
| if found_errors: | |
| return 1 | |
| return 0 | |
| def change_error(change, msg): | |
| if change in IGNORED: | |
| return False | |
| error(msg) | |
| return True | |
| def throw_error(msg): | |
| raise ValueError(msg) | |
| def error(msg): | |
| print(msg, file=sys.stderr) | |
| if __name__ == '__main__': | |
| sys.exit(main()) |