blob: 3b27b082f5497bad5f929ffd743f2d2497a51ec8 [file] [log] [blame]
#!/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'])
pattern = re.compile('origin/(\d+).(\d+)')
releases = []
for line in out.split('\n'):
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 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',
'Version .*-dev',
'--pretty=oneline',
])
# Format of output is: <hash> Version <version>-dev
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())