Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # -*- coding: utf-8 -*-" |
| 3 | # |
| 4 | # Copyright 2020 The Chromium OS Authors. All rights reserved. |
| 5 | # Use of this source code is governed by a BSD-style license that can be |
| 6 | # found in the LICENSE file. |
| 7 | # |
| 8 | # pylint: pylint: disable=filter-builtin-not-iterating |
| 9 | |
| 10 | """Utility command for engineers to get list of unresolved fixes |
| 11 | |
| 12 | This command can be used by engineers to get a list of unresolved |
| 13 | chromium or stable fixes. Users can ask for unresolved fixes with open CLs |
| 14 | or for unresolved fixes with conflicts. |
| 15 | |
| 16 | Before running this script, make sure you have been added to the |
| 17 | chromeos-missing-patches GCP project. |
| 18 | |
| 19 | Prerequisites to execute this script locally (RUN ONCE): |
Tzung-Bi Shih | 0283c97 | 2022-02-15 11:29:06 +0800 | [diff] [blame^] | 20 | >> ./scripts/local/local_database_setup.sh |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 21 | |
| 22 | All locally executed commands must be run in this directory's |
| 23 | virtual env (source env/bin/activate) before running any commands. |
| 24 | The associated shell script enables this environment. |
| 25 | """ |
| 26 | |
| 27 | from __future__ import print_function |
| 28 | |
| 29 | import argparse |
| 30 | import re |
Guenter Roeck | 0520f92 | 2020-12-29 09:52:50 -0800 | [diff] [blame] | 31 | import MySQLdb # pylint: disable=import-error |
Curtis Malainey | 584006e | 2020-05-27 16:01:35 -0700 | [diff] [blame] | 32 | |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 33 | import common |
| 34 | import git_interface |
Curtis Malainey | 584006e | 2020-05-27 16:01:35 -0700 | [diff] [blame] | 35 | import missing |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 36 | import synchronize |
| 37 | import util |
| 38 | |
| 39 | |
| 40 | # extract_numerics matches numeric parts of a Linux version as separate elements |
| 41 | # For example, "v5.4" matches "5" and "4", and "v5.4.12" matches "5", "4", and "12" |
| 42 | extract_numerics = re.compile(r'(?:v)?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?\s*') |
| 43 | |
| 44 | def branch_order(branch): |
| 45 | """Calculate numeric order of tag or branch. |
| 46 | |
| 47 | A branch with higher version number will return a larger number. |
| 48 | Ignores release candidates. For example, v5.7-rc1 will return the same |
| 49 | number as v5.7-rc2. |
| 50 | |
| 51 | Returns 0 if the kernel version can not be extracted. |
| 52 | """ |
| 53 | |
| 54 | m = extract_numerics.match(branch) |
| 55 | if m: |
| 56 | major = int(m.group(1)) |
| 57 | minor1 = int(m.group(2)) |
| 58 | minor2 = int(m.group(3)) if m.group(3) else 0 |
| 59 | return major * 10000 + minor1 * 100 + minor2 |
| 60 | return 0 |
| 61 | |
| 62 | |
| 63 | # version_baseline matches the numeric part of the major Linux version as a string |
| 64 | # For example, "v5.4.15" and "v5.4-rc3" both match "5.4" |
| 65 | version_baseline = re.compile(r'(?:v)?([0-9]+\.[0-9]+(?:\.[0-9]+)?)\s*') |
| 66 | |
| 67 | def version_list(branches, start, end): |
| 68 | """Return list of stable release branches between 'start' and 'end'. |
| 69 | |
| 70 | 'branches' is the sorted list of branches to match. |
| 71 | 'start' and 'end' can be any valid kernel version or tag. |
| 72 | If 'start' is empty or invalid, start list with first supported |
| 73 | stable branch. |
| 74 | If 'end' is empty or invalid, end list with last supported stable |
| 75 | branch. |
| 76 | """ |
| 77 | |
| 78 | offset = 0 |
| 79 | |
| 80 | if start: |
| 81 | # Extract numeric part of 'start' |
| 82 | start_match = version_baseline.match(start) |
| 83 | start = start_match.group(1) if start_match else None |
| 84 | if not start: |
| 85 | start = branches[0] |
| 86 | if end: |
| 87 | end_match = version_baseline.match(end) |
| 88 | end = end_match.group(1) if end_match else None |
| 89 | if not end: |
| 90 | end = branches[-1] |
| 91 | # If we have no 'end', also match the last branch |
| 92 | offset = 1 |
| 93 | |
| 94 | min_order = branch_order(start) |
| 95 | max_order = branch_order(end) + offset |
| 96 | return list(filter(lambda x: branch_order(x) >= min_order and branch_order(x) < max_order, |
| 97 | branches)) |
| 98 | |
| 99 | |
| 100 | def mysort(elem): |
| 101 | """Function to sort branches based on version number""" |
| 102 | _, branch = elem |
| 103 | return branch_order(branch) |
| 104 | |
| 105 | |
Guenter Roeck | 32b23dc | 2021-01-17 13:29:06 -0800 | [diff] [blame] | 106 | def report_integration_status_sha(handler, rc_handler, branch, branch_name, sha): |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 107 | """Report integration status for given repository, branch, and sha""" |
| 108 | |
Guenter Roeck | 32b23dc | 2021-01-17 13:29:06 -0800 | [diff] [blame] | 109 | if rc_handler: |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 110 | # It is possible that the patch has not yet been applied but is queued |
| 111 | # in a stable release candidate. Try to find it there as well. We use |
| 112 | # this information to override the final status if appropriate. |
Guenter Roeck | 32b23dc | 2021-01-17 13:29:06 -0800 | [diff] [blame] | 113 | # Try applying patch and get status |
| 114 | rc_status = rc_handler.cherrypick_status(sha, branch=branch) |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 115 | else: |
| 116 | rc_status = common.Status.OPEN |
| 117 | |
Guenter Roeck | 32b23dc | 2021-01-17 13:29:06 -0800 | [diff] [blame] | 118 | status = handler.cherrypick_status(sha, branch=branch) |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 119 | if status == common.Status.CONFLICT: |
| 120 | disposition = ' (queued)' if rc_status == common.Status.MERGED \ |
| 121 | else ' (conflicts - backport needed)' |
| 122 | elif status == common.Status.MERGED: |
| 123 | disposition = ' (already applied)' |
| 124 | else: |
| 125 | disposition = ' (queued)' if rc_status == common.Status.MERGED else '' |
| 126 | |
| 127 | print(' %s%s' % (branch_name, disposition)) |
| 128 | |
| 129 | def report_integration_status_branch(db, metadata, handled_shas, branch, conflicts): |
| 130 | """Report integration status for list of open patches in given repository and branch""" |
| 131 | |
| 132 | if metadata.kernel_fixes_table == 'stable_fixes': |
| 133 | table = 'linux_stable' |
Guenter Roeck | bc00c2f | 2021-01-20 16:28:22 -0800 | [diff] [blame] | 134 | handler = git_interface.commitHandler(common.Kernel.linux_stable) |
| 135 | rc_handler = git_interface.commitHandler(common.Kernel.linux_stable_rc) |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 136 | else: |
| 137 | table = 'linux_chrome' |
Guenter Roeck | bc00c2f | 2021-01-20 16:28:22 -0800 | [diff] [blame] | 138 | handler = git_interface.commitHandler(common.Kernel.linux_chrome) |
| 139 | rc_handler = None |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 140 | |
| 141 | c = db.cursor() |
| 142 | |
| 143 | status = 'CONFLICT' if conflicts else 'OPEN' |
| 144 | |
| 145 | # Walk through all fixes table entries with given status |
| 146 | q = """SELECT ct.fixedby_upstream_sha, lu.description, t.upstream_sha, t.description |
| 147 | FROM {chosen_table} AS ct |
| 148 | JOIN linux_upstream AS lu |
| 149 | ON ct.fixedby_upstream_sha = lu.sha |
| 150 | JOIN {table} as t |
| 151 | ON ct.kernel_sha = t.sha |
| 152 | WHERE ct.status = %s |
| 153 | AND ct.branch = %s""".format(chosen_table=metadata.kernel_fixes_table, table=table) |
| 154 | c.execute(q, [status, branch]) |
| 155 | for fixedby_sha, fixedby_description, fixes_sha, fixes_description in c.fetchall(): |
| 156 | # If we already handled a SHA while running this command while examining |
| 157 | # another branch, we don't need to handle it again. |
| 158 | if fixedby_sha in handled_shas: |
| 159 | continue |
| 160 | handled_shas += [fixedby_sha] |
| 161 | |
| 162 | print('Upstream commit %s ("%s")' % (fixedby_sha, fixedby_description)) |
Guenter Roeck | 0520f92 | 2020-12-29 09:52:50 -0800 | [diff] [blame] | 163 | |
Guenter Roeck | d8bf202 | 2021-01-19 07:31:54 -0800 | [diff] [blame] | 164 | changeid, _ = missing.get_change_id(db, branch, fixedby_sha) |
Guenter Roeck | 0520f92 | 2020-12-29 09:52:50 -0800 | [diff] [blame] | 165 | if changeid: |
| 166 | print(' CL:%s' % changeid) |
| 167 | |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 168 | integrated = git_interface.get_integrated_tag(fixedby_sha) |
| 169 | end = integrated |
| 170 | if not integrated: |
| 171 | integrated = 'ToT' |
| 172 | print(' upstream: %s' % integrated) |
| 173 | print(' Fixes: %s ("%s")' % (fixes_sha, fixes_description)) |
| 174 | |
| 175 | q = """SELECT sha, branch |
| 176 | FROM {table} |
| 177 | WHERE upstream_sha = %s""".format(table=table) |
| 178 | c.execute(q, [fixes_sha]) |
| 179 | fix_rows = sorted(c.fetchall(), key=mysort) |
| 180 | affected_branches = [] |
| 181 | for fix_sha, fix_branch in fix_rows: |
| 182 | if fix_branch in metadata.branches: |
| 183 | affected_branches += [fix_branch] |
Guenter Roeck | 32b23dc | 2021-01-17 13:29:06 -0800 | [diff] [blame] | 184 | branch_name = metadata.get_kernel_branch(fix_branch) |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 185 | print(' in %s: %s' % (branch_name, fix_sha)) |
| 186 | |
| 187 | start = git_interface.get_integrated_tag(fixes_sha) |
| 188 | if start: |
| 189 | print(' upstream: %s' % git_interface.get_integrated_tag(fixes_sha)) |
| 190 | |
| 191 | affected_branches += version_list(metadata.branches, start, end) |
| 192 | affected_branches = sorted(list(set(affected_branches)), key=branch_order) |
| 193 | if affected_branches: |
| 194 | print(' Affected branches:') |
| 195 | for affected_branch in affected_branches: |
Guenter Roeck | 32b23dc | 2021-01-17 13:29:06 -0800 | [diff] [blame] | 196 | branch_name = metadata.get_kernel_branch(affected_branch) |
| 197 | report_integration_status_sha(handler, rc_handler, affected_branch, branch_name, |
| 198 | fixedby_sha) |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 199 | |
Curtis Malainey | 584006e | 2020-05-27 16:01:35 -0700 | [diff] [blame] | 200 | # Check if this commit has been fixed as well and, if so, report it |
| 201 | subsequent_fixes = missing.get_subsequent_fixes(db, fixedby_sha) |
| 202 | # remove initial fix |
| 203 | subsequent_fixes.pop(0) |
| 204 | if subsequent_fixes: |
| 205 | subsequent_fixes = ["\'" + sha + "\'" for sha in subsequent_fixes] |
| 206 | parsed_fixes = ', '.join(subsequent_fixes) |
| 207 | # format query here since we are inserting n values |
| 208 | q = """SELECT sha, description |
| 209 | FROM linux_upstream |
Guenter Roeck | 0520f92 | 2020-12-29 09:52:50 -0800 | [diff] [blame] | 210 | WHERE sha in ({sha}) |
| 211 | ORDER BY FIELD(sha, {sha})""".format(sha=parsed_fixes) |
Curtis Malainey | 584006e | 2020-05-27 16:01:35 -0700 | [diff] [blame] | 212 | c.execute(q) |
| 213 | fixes = c.fetchall() |
| 214 | print(' Fixed by:') |
| 215 | for fix in fixes: |
| 216 | print(' %s ("%s")' % fix) |
| 217 | |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 218 | |
| 219 | @util.cloud_sql_proxy_decorator |
| 220 | @util.preliminary_check_decorator(False) |
| 221 | def report_integration_status(branch, conflicts, is_chromium): |
| 222 | """Report list of open patches""" |
| 223 | |
| 224 | handled_shas = [] |
| 225 | |
| 226 | if is_chromium: |
| 227 | metadata = common.get_kernel_metadata(common.Kernel.linux_chrome) |
| 228 | else: |
| 229 | metadata = common.get_kernel_metadata(common.Kernel.linux_stable) |
| 230 | |
| 231 | synchronize.synchronize_repositories(True) |
| 232 | |
Guenter Roeck | bcb73e2 | 2020-09-27 18:04:11 -0700 | [diff] [blame] | 233 | db = MySQLdb.Connect(user='linux_patches_robot', host='127.0.0.1', db='linuxdb', |
| 234 | charset='utf8mb4') |
Guenter Roeck | 0e9e8d1 | 2020-04-14 14:02:54 -0700 | [diff] [blame] | 235 | |
| 236 | if branch: |
| 237 | report_integration_status_branch(db, metadata, handled_shas, branch, conflicts) |
| 238 | else: |
| 239 | for b in metadata.branches: |
| 240 | print('\nBranch: linux-%s.y\n' % b) |
| 241 | report_integration_status_branch(db, metadata, handled_shas, b, conflicts) |
| 242 | |
| 243 | db.close() |
| 244 | |
| 245 | |
| 246 | def report_integration_status_parse(): |
| 247 | """Parses command line args and calls the actual function with parsed parameters |
| 248 | |
| 249 | To execute: |
| 250 | ./getopen [-b branch] [-c] [-C] |
| 251 | """ |
| 252 | |
| 253 | metadata = common.get_kernel_metadata(common.Kernel.linux_stable) |
| 254 | parser = argparse.ArgumentParser(description='Local functions to retrieve data from database') |
| 255 | parser.add_argument('-b', '--branch', type=str, choices=metadata.branches, |
| 256 | help='Branch to check') |
| 257 | parser.add_argument('-C', '--chromium', action='store_true', |
| 258 | help='Look for pending chromium patches') |
| 259 | parser.add_argument('-c', '--conflicts', action='store_true', |
| 260 | help='Check for conflicting patches') |
| 261 | args = parser.parse_args() |
| 262 | |
| 263 | report_integration_status(args.branch, args.conflicts, args.chromium) |
| 264 | |
| 265 | if __name__ == '__main__': |
| 266 | report_integration_status_parse() |