blob: adf763cc9b60bc7f691e63c9d4471776891de37f [file] [log] [blame]
Ian Zernybbd36a02023-06-09 12:37:50 +02001#!/usr/bin/env python3
2# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5
6import subprocess
7import sys
8import re
9import r8_release
10
11class Branch:
12 def __init__(self, name, first, last=None):
13 self.name = name
14 self.first = first
15 self.last = last # optional last for testing purposes.
16
17 def origin(self):
18 return "origin/%s" % self.name
19
20 def last_or_origin(self):
21 return self.last if self.last else self.origin()
22
23 def __str__(self):
24 return self.name
25
26# The initial commit is the furthest back we need to search on main.
27# Currently, it is the merge point of main onto 4.0.23-dev
28MAIN = Branch('main', 'a2e203580aa00a36f85cd68d3d584b97aef34d59')
29OLDEST_BRANCH_VERSION = (4, 0)
30DEV_BRANCH_VERSION = [int(s) for s in r8_release.R8_DEV_BRANCH.split('.')]
31
32# List of change ids that should not be reported.
33IGNORED = [
34 'I92d7bf3afbf609fdea21683941cfd15c90305cf2'
35]
36
37VERBOSE = False
38
39# Helper to call and decode a shell command.
40def run_cmd(cmd):
41 if VERBOSE:
42 print(' '.join(cmd))
43 return subprocess.check_output(cmd).decode('UTF-8')
44
45# Comparator on major and minor branch versions.
46def branch_version_less_than(b1, b2):
47 if b1[0] < b2[0]:
48 return True
49 if b1[0] == b2[0] and b1[1] < b2[1]:
50 return True
51 return False
52
53# Find all release branches between OLDEST_BRANCH and DEV_BRANCH
54def get_release_branches():
55 # Release branches are assumed to be of the form 'origin/X.Y'
56 out = run_cmd(['git', 'branch', '-r', '-l'])
57 pattern = re.compile('origin/(\d+).(\d+)')
58 releases = []
59 for line in out.split('\n'):
60 m = pattern.search(line.strip())
61 if m:
62 major = m.group(1)
63 minor = m.group(2)
64 if major and minor:
65 candidate = (int(major), int(minor))
66 if branch_version_less_than(candidate, OLDEST_BRANCH_VERSION):
67 continue
68 if branch_version_less_than(candidate, DEV_BRANCH_VERSION):
69 releases.extend(find_dev_cutoff(candidate))
70 return releases
71
72# Find the most recent commit hash that is for a -dev version.
73# This is the starting point for the map of commits after cutoff from main.
74def find_dev_cutoff(branch_version):
75 out = run_cmd([
76 'git',
77 'log',
78 'origin/%d.%d' % branch_version,
79 '--grep', 'Version .*-dev',
80 '--pretty=oneline',
81 ])
82 # Format of output is: <hash> Version <version>-dev
83 try:
84 hash = out[0:out.index(' ')]
85 return [Branch('%d.%d' % branch_version, hash)]
86 except ValueError:
87 throw_error("Failed to find dev cutoff for branch %d.%d" % branch_version)
88
89# Build a map from each "Change-Id" hash to the hash of its commit.
90def get_change_id_map(branch):
91 out = run_cmd([
92 'git',
93 'log',
94 '%s..%s' % (branch.first, branch.last_or_origin())
95 ])
96 map = {}
97 current_commit = None
98 for line in out.split('\n'):
99 if line.startswith('commit '):
100 current_commit = line[len('commit '):]
101 assert len(current_commit) == 40
102 elif line.strip().startswith('Change-Id: '):
103 change_id = line.strip()[len('Change-Id: '):]
104 assert len(change_id) == 41
105 map[change_id] = current_commit
106 return map
107
108# Check if a specific commit is present on a specific branch.
109def is_commit_in(commit, branch):
110 out = run_cmd(['git', 'branch', '-r', branch.origin(), '--contains', commit])
111 return out.strip() == branch.origin()
112
113def main():
114 found_errors = False
115 # The main map is all commits back to the "init" point.
116 main_map = get_change_id_map(MAIN)
117 # Compute the release branches.
118 release_branches = get_release_branches()
119 # Populate the release maps with all commits after the last -dev point.
120 release_maps = {}
121 for branch in release_branches:
122 release_maps[branch.name] = get_change_id_map(branch)
123 # Each branch is then compared forwards with each subsequent branch.
124 for i in range(len(release_branches)):
125 branch = release_branches[i]
126 newer_branches = release_branches[i+1:]
127 if (len(newer_branches) == 0):
128 print('Last non-dev release branch is %s, nothing to check.' % branch)
129 continue
130 print('Checking branch %s.' % branch)
131 changes = release_maps[branch.name]
132 cherry_picks_count = 0
133 for change in changes.keys():
134 is_cherry_pick = False
135 missing_from = None
136 commit_on_main = main_map.get(change)
137 for newer_branch in newer_branches:
138 if change in release_maps[newer_branch.name]:
139 is_cherry_pick = True
140 # If the change is in the release mappings check for holes.
141 if missing_from:
142 found_errors = change_error(
143 change,
144 'Error: missing Change-Id %s on branch %s. '
145 'Is present on %s and again on %s.' % (
146 change, missing_from, branch, newer_branch,
147 ))
148 elif commit_on_main:
149 is_cherry_pick = True
150 # The change is not in the non-dev part of the branch, so we need to
151 # check that the fork from main included the change.
152 if not is_commit_in(commit_on_main, newer_branch):
153 found_errors = change_error(
154 change,
155 'Error: missing Change-Id %s on branch %s. '
156 'Is present on %s and on main as commit %s.' % (
157 change, newer_branch, branch, commit_on_main
158 ))
159 else:
160 # The change is not on "main" so we just record for holes on releases.
161 missing_from = newer_branch
162 if is_cherry_pick:
163 cherry_picks_count += 1
164 print('Found %d cherry-picks (out of %d commits).' % (
165 cherry_picks_count, len(changes)))
166
167 if found_errors:
168 return 1
169 return 0
170
171def change_error(change, msg):
172 if change in IGNORED:
173 return False
174 error(msg)
175 return True
176
177def throw_error(msg):
178 raise ValueError(msg)
179
180def error(msg):
181 print(msg, file=sys.stderr)
182
183if __name__ == '__main__':
184 sys.exit(main())