| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*-" |
| # |
| # Copyright 2020 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Find missing stable and backported mainline fix patches in chromeos.""" |
| |
| from __future__ import print_function |
| import logging |
| import os |
| import subprocess |
| import sys |
| |
| import requests |
| |
| import MySQLdb # pylint: disable=import-error |
| import common |
| import cloudsql_interface |
| import gerrit_interface |
| import git_interface |
| |
| |
| # Constant representing number CL's we want created on single new missing patch run |
| NEW_CL_DAILY_LIMIT_PER_STABLE_BRANCH = 2 |
| NEW_CL_DAILY_LIMIT_PER_BRANCH = 2 |
| |
| |
| def get_subsequent_fixes(db, upstream_sha): |
| """Finds all fixes that might be needed to fix the incoming commit""" |
| |
| c = db.cursor() |
| |
| q = """ |
| WITH RECURSIVE sha_graph (sha, ord) AS ( |
| SELECT %s as sha, 1 as ord |
| UNION DISTINCT |
| SELECT fixedby_upstream_sha, ord + 1 |
| FROM sha_graph |
| JOIN upstream_fixes |
| ON upstream_sha = sha |
| ) |
| SELECT sha |
| FROM sha_graph |
| GROUP BY sha |
| ORDER BY MAX(ord); |
| """ |
| c.execute(q, [upstream_sha]) |
| return list(sum(c.fetchall(), ())) |
| |
| |
| def upstream_sha_to_kernel_sha(db, chosen_table, branch, upstream_sha): |
| """Retrieves chromeos/stable sha by indexing db. |
| |
| Returns sha or None if upstream sha doesn't exist downstream. |
| """ |
| c = db.cursor() |
| |
| q = """SELECT sha |
| FROM {chosen_table} |
| WHERE branch = %s |
| AND (upstream_sha = %s |
| OR patch_id IN ( |
| SELECT patch_id |
| FROM linux_upstream |
| WHERE sha = %s |
| ))""".format(chosen_table=chosen_table) |
| c.execute(q, [branch, upstream_sha, upstream_sha]) |
| row = c.fetchone() |
| |
| return row[0] if row else None |
| |
| |
| def get_change_id(db, branch, sha): |
| """Get Change-Id associated with provided upstream SHA. |
| |
| Returns Gerrit Change-Id for a provided SHA if available in either |
| the chrome_fixes or the stable_fixes table as well as in Gerrit. |
| None otherwise. |
| |
| If multiple Change IDs are available, pick one that has not been abandoned. |
| """ |
| |
| c = db.cursor() |
| change_id = None |
| status = None |
| reject_list = [] |
| |
| q = """SELECT c.fix_change_id, c.branch, s.fix_change_id, s.branch |
| FROM linux_upstream AS l1 |
| JOIN linux_upstream AS l2 |
| ON l1.patch_id = l2.patch_id |
| LEFT JOIN chrome_fixes AS c |
| ON c.fixedby_upstream_sha = l2.sha |
| LEFT JOIN stable_fixes as s |
| ON s.fixedby_upstream_sha = l2.sha |
| WHERE l1.sha = %s""" |
| |
| c.execute(q, [sha]) |
| for chrome_change_id, chrome_branch, stable_change_id, stable_branch in c.fetchall(): |
| # Some entries in fixes_table do not have a change id attached. |
| # This will be seen if a patch was identified as already merged |
| # or as duplicate. Skip those. Also skip empty entries returned |
| # by the query above. |
| # Return Change-Ids associated with abandoned CLs only if no other |
| # Change-Ids are found. |
| if chrome_change_id and chrome_branch: |
| try: |
| # Change-IDs stored in in chrome_fixes are not always available |
| # in Gerrit. This can happen, for example, if a commit was |
| # created using a git instance with pre-commit hook, and the |
| # commit was uploaded into Gerrit using a merge. We can not use |
| # such Change-Ids. To verify, try to get the status from Gerrit |
| # and skip if the Change-Id is not found. |
| gerrit_status = gerrit_interface.get_status(chrome_change_id, chrome_branch) |
| # We can not use a Change-ID which exists in Gerrit but is marked |
| # as abandoned for the target branch. |
| if chrome_change_id in reject_list: |
| continue |
| if chrome_branch == branch and gerrit_status == common.Status.ABANDONED.name: |
| reject_list += [chrome_change_id] |
| continue |
| if not change_id or gerrit_status != common.Status.ABANDONED.name: |
| change_id = chrome_change_id |
| status = gerrit_status |
| if status != common.Status.ABANDONED.name: |
| break |
| except requests.exceptions.HTTPError: |
| # Change-Id was not found in Gerrit |
| pass |
| |
| if stable_change_id and stable_branch: |
| try: |
| gerrit_status = gerrit_interface.get_status(stable_change_id, stable_branch) |
| if stable_change_id in reject_list: |
| continue |
| if stable_branch == branch and gerrit_status == common.Status.ABANDONED.name: |
| reject_list += [stable_change_id] |
| continue |
| if not change_id or gerrit_status != common.Status.ABANDONED.name: |
| change_id = stable_change_id |
| status = gerrit_status |
| except requests.exceptions.HTTPError: |
| pass |
| |
| if status and status != common.Status.ABANDONED.name: |
| break |
| |
| if change_id in reject_list: |
| change_id = None |
| |
| return change_id, status == common.Status.ABANDONED.name and bool(reject_list) |
| |
| |
| def find_duplicate(db, branch, upstream_sha): |
| """Find and report dupplicate entry in chrome_fixes table. |
| |
| Return [kernel_sha, fixedby_upstream_sha, status] if patch is already |
| in chrome_fixes table using a different upstream SHA with same patch_id, |
| None otherwise. |
| """ |
| |
| c = db.cursor() |
| |
| # Check if the commit is already in the fixes table using a different SHA |
| # (the same commit may be listed upstream under multiple SHAs). |
| q = """SELECT c.kernel_sha, c.fixedby_upstream_sha, c.status |
| FROM chrome_fixes AS c |
| JOIN linux_upstream AS l1 |
| ON c.fixedby_upstream_sha = l1.sha |
| JOIN linux_upstream AS l2 |
| ON l1.patch_id = l2.patch_id |
| WHERE l1.sha != l2.sha |
| AND c.branch = %s |
| AND l2.sha = %s""" |
| |
| c.execute(q, [branch, upstream_sha]) |
| return c.fetchone() |
| |
| |
| def insert_by_patch_id(db, branch, fixed_sha, fixedby_upstream_sha): |
| """Handles case where fixedby_upstream_sha may have changed in kernels. |
| |
| Returns True if successful patch_id insertion and False if patch_id not found. |
| """ |
| c = db.cursor() |
| |
| duplicate = find_duplicate(db, branch, fixedby_upstream_sha) |
| if duplicate: |
| # This commit is already queued or known under a different upstream SHA. |
| # Mark it as abandoned and point to the other entry as reason. |
| kernel_sha, recorded_upstream_sha, status = duplicate |
| # kernel_sha is the SHA in the database. |
| # fixed_sha is the SHA we are trying to fix, which matches kernel_sha |
| # (meaning both have the same patch ID). |
| # The entry we need to add to the fixes table is for fixed_sha, not for |
| # kernel_sha (because kernel_sha is already in the database). |
| reason = 'Already merged/queued into linux_chrome [upstream sha %s]' % recorded_upstream_sha |
| logging.info('SHA %s [%s] fixed by %s: %s', |
| fixed_sha, kernel_sha, fixedby_upstream_sha, reason) |
| |
| try: |
| q = """INSERT INTO chrome_fixes |
| (kernel_sha, fixedby_upstream_sha, branch, entry_time, |
| close_time, initial_status, status, reason) |
| VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""" |
| # Status must be OPEN, MERGED or ABANDONED since we don't have |
| # entries with CONFLICT in the fixes table. |
| # Change OPEN to ABANDONED for final status, but keep MERGED. |
| final_status = status |
| if final_status == common.Status.OPEN.name: |
| final_status = common.Status.ABANDONED.name |
| # ABANDONED is not a valid initial status. Change it to OPEN |
| # if encountered. |
| if status == common.Status.ABANDONED.name: |
| status = common.Status.OPEN.name |
| entry_time = common.get_current_time() |
| c.execute(q, [fixed_sha, fixedby_upstream_sha, |
| branch, entry_time, entry_time, |
| status, final_status, reason]) |
| db.commit() |
| except MySQLdb.Error as e: # pylint: disable=no-member |
| logging.error( |
| 'Failed to insert an already merged/queued entry into chrome_fixes: error %d (%s)', |
| e.args[0], e.args[1]) |
| return True |
| |
| # Commit sha may have been modified in cherry-pick, backport, etc. |
| # Retrieve SHA in linux_chrome by patch-id by checking for fixedby_upstream_sha |
| # removes entries that are already tracked in chrome_fixes |
| q = """SELECT lc.sha |
| FROM linux_chrome AS lc |
| JOIN linux_upstream AS lu |
| ON lc.patch_id = lu.patch_id |
| JOIN upstream_fixes as uf |
| ON lc.upstream_sha = uf.upstream_sha |
| WHERE uf.fixedby_upstream_sha = %s AND branch = %s |
| AND (lc.sha, uf.fixedby_upstream_sha) |
| NOT IN ( |
| SELECT kernel_sha, fixedby_upstream_sha |
| FROM chrome_fixes |
| WHERE branch = %s |
| )""" |
| c.execute(q, [fixedby_upstream_sha, branch, branch]) |
| chrome_shas = c.fetchall() |
| |
| # fixedby_upstream_sha has already been merged into linux_chrome |
| # chrome shas represent kernel sha for the upstream_sha fixedby_upstream_sha |
| if chrome_shas: |
| for chrome_sha in chrome_shas: |
| entry_time = common.get_current_time() |
| cl_status = common.Status.MERGED.name |
| reason = 'Already merged into linux_chrome [upstream sha %s]' % fixedby_upstream_sha |
| |
| try: |
| q = """INSERT INTO chrome_fixes |
| (kernel_sha, fixedby_upstream_sha, branch, entry_time, |
| close_time, initial_status, status, reason) |
| VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""" |
| c.execute(q, [chrome_sha, fixedby_upstream_sha, branch, entry_time, |
| entry_time, cl_status, cl_status, reason]) |
| db.commit() |
| except MySQLdb.Error as e: # pylint: disable=no-member |
| logging.error( |
| 'Failed to insert an already merged entry into chrome_fixes: error %d(%s)', |
| e.args[0], e.args[1]) |
| return True |
| |
| return False |
| |
| |
| def insert_fix_gerrit(db, chosen_table, chosen_fixes, branch, kernel_sha, fixedby_upstream_sha, |
| recursion=False): |
| """Inserts fix row by checking status of applying a fix change. |
| |
| Return True if we create a new Gerrit CL, otherwise return False. |
| """ |
| # Check if fix has been merged using it's patch-id since sha's might've changed |
| success = insert_by_patch_id(db, branch, kernel_sha, fixedby_upstream_sha) |
| created_new_change = False |
| if success: |
| return created_new_change |
| |
| c = db.cursor() |
| |
| handler = git_interface.commitHandler(common.Kernel.linux_chrome, branch=branch, |
| full_reset=not recursion) |
| # Try applying patch and get status |
| status = handler.cherrypick_status(fixedby_upstream_sha) |
| initial_status = status.name |
| current_status = status.name |
| |
| entry_time = common.get_current_time() |
| |
| close_time = fix_change_id = reason = None |
| |
| q = """INSERT INTO {chosen_fixes} |
| (kernel_sha, fixedby_upstream_sha, branch, entry_time, close_time, |
| fix_change_id, initial_status, status, reason) |
| VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""".format(chosen_fixes=chosen_fixes) |
| |
| if status == common.Status.MERGED: |
| # Create a row for the merged CL (we don't need to track this), but can be stored |
| # to indicate that the changes of this patch are already merged |
| # entry_time and close_time are the same since we weren't tracking when it was merged |
| fixedby_kernel_sha = upstream_sha_to_kernel_sha(db, chosen_table, |
| branch, fixedby_upstream_sha) |
| logging.info('%s SHA [%s] already merged bugfix patch [kernel: %s] [upstream: %s]', |
| chosen_fixes, kernel_sha, fixedby_kernel_sha, fixedby_upstream_sha) |
| |
| reason = 'Patch applied to linux_chrome before this robot was run' |
| close_time = entry_time |
| |
| # linux_chrome will have change-id's but stable merged fixes will not |
| # Correctly located fixedby_kernel_sha in linux_chrome |
| if chosen_table == 'linux_chrome' and fixedby_kernel_sha: |
| fix_change_id = git_interface.get_commit_changeid_linux_chrome(fixedby_kernel_sha) |
| elif status == common.Status.OPEN: |
| change_id, rejected = get_change_id(db, branch, fixedby_upstream_sha) |
| if rejected: |
| # A change-Id for this commit exists in the target branch, but it is marked |
| # as ABANDONED and we can not use it. Create a database entry and mark the commit |
| # as abandoned to prevent it from being retried over and over again. |
| current_status = common.Status.ABANDONED.name |
| else: |
| fix_change_id = gerrit_interface.create_change(kernel_sha, fixedby_upstream_sha, branch, |
| chosen_table == 'linux_chrome', |
| change_id) |
| created_new_change = bool(fix_change_id) |
| |
| # Checks if change was created successfully |
| if not created_new_change: |
| logging.error('Failed to create change for kernel_sha %s fixed by %s', |
| kernel_sha, fixedby_upstream_sha) |
| return False |
| elif status == common.Status.CONFLICT: |
| # Register conflict entry_time, do not create gerrit CL |
| # Requires engineer to manually explore why CL doesn't apply cleanly |
| pass |
| |
| try: |
| c.execute(q, [kernel_sha, fixedby_upstream_sha, branch, entry_time, |
| close_time, fix_change_id, initial_status, current_status, reason]) |
| logging.info('Inserted row into fixes table %s %s %s %s %s %s %s %s %s %s', |
| chosen_fixes, kernel_sha, fixedby_upstream_sha, branch, |
| entry_time, close_time, fix_change_id, initial_status, current_status, reason) |
| |
| except MySQLdb.Error as e: # pylint: disable=no-member |
| logging.error( |
| 'Error inserting fix CL into fixes table %s %s %s %s %s %s %s %s %s %s: error %d(%s)', |
| chosen_fixes, kernel_sha, fixedby_upstream_sha, branch, |
| entry_time, close_time, fix_change_id, initial_status, current_status, reason, |
| e.args[0], e.args[1]) |
| |
| if created_new_change and not recursion: |
| subsequent_fixes = get_subsequent_fixes(db, fixedby_upstream_sha) |
| if subsequent_fixes: |
| subsequent_fixes.pop(0) # 1st returned SHA is fixedby_upstream_sha |
| for fix in subsequent_fixes: |
| insert_fix_gerrit(db, chosen_table, chosen_fixes, branch, kernel_sha, fix, True) |
| |
| return created_new_change |
| |
| |
| def fixup_unmerged_patches(db, branch, kernel_metadata): |
| """Fixup script that attempts to reapply unmerged fixes to get latest status. |
| |
| 2 main actions performed by script include: |
| 1) Handle case where a conflicting CL later can be applied cleanly without merge conflicts |
| 2) Detect if the fix has been applied to linux_chrome externally |
| (i.e not merging through a fix created by this robot) |
| """ |
| c = db.cursor() |
| fixes_table = kernel_metadata.kernel_fixes_table |
| |
| q = """SELECT kernel_sha, fixedby_upstream_sha, status, fix_change_id |
| FROM {fixes_table} |
| WHERE status != 'MERGED' |
| AND branch = %s""".format(fixes_table=fixes_table) |
| c.execute(q, [branch]) |
| rows = c.fetchall() |
| handler = git_interface.commitHandler(common.Kernel.linux_chrome, branch) |
| for row in rows: |
| kernel_sha, fixedby_upstream_sha, status, fix_change_id = row |
| |
| new_status_enum = handler.cherrypick_status(fixedby_upstream_sha, |
| apply=status != 'ABANDONED') |
| if not new_status_enum: |
| continue |
| |
| new_status = new_status_enum.name |
| |
| if status == 'CONFLICT' and new_status == 'OPEN': |
| change_id, rejected = get_change_id(db, branch, fixedby_upstream_sha) |
| if rejected: |
| # The cherry-pick was successful, but the commit is marked as abandoned |
| # in gerrit. Mark it accordingly. |
| reason = 'Fix abandoned in Gerrit' |
| cloudsql_interface.update_change_abandoned(db, fixes_table, kernel_sha, |
| fixedby_upstream_sha, reason) |
| else: |
| fix_change_id = gerrit_interface.create_change(kernel_sha, fixedby_upstream_sha, |
| branch, kernel_metadata.path == 'linux_chrome', change_id) |
| |
| # Check if we successfully created the fix patch before performing update |
| if fix_change_id: |
| cloudsql_interface.update_conflict_to_open(db, fixes_table, |
| kernel_sha, fixedby_upstream_sha, fix_change_id) |
| elif new_status == 'MERGED': |
| # This specifically includes situations where a patch marked ABANDONED |
| # in the database was merged at some point anyway. |
| reason = 'Fix was merged externally and detected by robot.' |
| if fix_change_id: |
| gerrit_interface.abandon_change(fix_change_id, branch, reason) |
| cloudsql_interface.update_change_merged(db, fixes_table, |
| kernel_sha, fixedby_upstream_sha, reason) |
| |
| |
| def update_fixes_in_branch(db, branch, kernel_metadata, limit): |
| """Updates fix patch table row by determining if CL merged into linux_chrome.""" |
| del limit # unused here |
| |
| c = db.cursor() |
| chosen_fixes = kernel_metadata.kernel_fixes_table |
| |
| # Old rows to Update |
| q = """UPDATE {chosen_fixes} AS fixes |
| JOIN linux_chrome AS lc |
| ON fixes.fixedby_upstream_sha = lc.upstream_sha |
| SET status = 'MERGED', close_time = %s, reason = %s |
| WHERE fixes.branch = %s |
| AND lc.branch = %s |
| AND (fixes.status = 'OPEN' |
| OR fixes.status = 'CONFLICT' |
| OR fixes.status = 'ABANDONED')""".format(chosen_fixes=chosen_fixes) |
| |
| close_time = common.get_current_time() |
| reason = 'Patch has been applied to linux_chome' |
| |
| try: |
| c.execute(q, [close_time, reason, branch, branch]) |
| logging.info( |
| 'Updating rows that have been merged into linux_chrome in table %s / branch %s', |
| chosen_fixes, branch) |
| except MySQLdb.Error as e: # pylint: disable=no-member |
| logging.error('Error updating fixes table for merged commits %s %s %s %s: %d(%s)', |
| chosen_fixes, close_time, reason, branch, e.args[0], e.args[1]) |
| db.commit() |
| |
| # Sync status of unmerged patches in a branch |
| fixup_unmerged_patches(db, branch, kernel_metadata) |
| |
| |
| def create_new_fixes_in_branch(db, branch, kernel_metadata, limit): |
| """Look for missing Fixup commits in provided chromeos or stable release.""" |
| c = db.cursor() |
| branch_name = kernel_metadata.get_kernel_branch(branch) |
| |
| logging.info('Checking branch %s', branch_name) |
| subprocess.run(['git', 'checkout', branch_name], check=True, |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| |
| # chosen_table is either linux_stable or linux_chrome |
| chosen_table = kernel_metadata.path |
| chosen_fixes = kernel_metadata.kernel_fixes_table |
| |
| # New rows to insert |
| # Note: MySQLdb doesn't support inserting table names as parameters |
| # due to sql injection |
| q = """SELECT chosen_table.sha, uf.fixedby_upstream_sha |
| FROM {chosen_table} AS chosen_table |
| JOIN upstream_fixes AS uf |
| ON chosen_table.upstream_sha = uf.upstream_sha |
| WHERE branch = %s |
| AND (chosen_table.sha, uf.fixedby_upstream_sha) |
| NOT IN ( |
| SELECT chosen_fixes.kernel_sha, chosen_fixes.fixedby_upstream_sha |
| FROM {chosen_fixes} AS chosen_fixes |
| WHERE branch = %s |
| )""".format(chosen_table=chosen_table, chosen_fixes=chosen_fixes) |
| try: |
| c.execute(q, [branch, branch]) |
| logging.info('Finding new rows to insert into fixes table %s %s %s', |
| chosen_table, chosen_fixes, branch) |
| except MySQLdb.Error as e: # pylint: disable=no-member |
| logging.error('Error finding new rows to insert %s %s %s: error %d(%s)', |
| chosen_table, chosen_fixes, branch, e.args[0], e.args[1]) |
| |
| count_new_changes = 0 |
| # todo(hirthanan): Create an intermediate state in Status that allows us to |
| # create all the patches in chrome/stable fixes tables but does not add reviewers |
| # until quota is available. This should decouple the creation of gerrit CL's |
| # and adding reviewers to those CL's. |
| for (kernel_sha, fixedby_upstream_sha) in c.fetchall(): |
| new_change = insert_fix_gerrit(db, chosen_table, chosen_fixes, |
| branch, kernel_sha, fixedby_upstream_sha) |
| if new_change: |
| count_new_changes += 1 |
| if count_new_changes >= limit: |
| break |
| |
| db.commit() |
| return count_new_changes |
| |
| |
| def missing_patches_sync(db, kernel_metadata, sync_branch_method, limit=None): |
| """Helper to create or update fix patches in stable and chromeos releases.""" |
| if len(sys.argv) > 1: |
| branches = sys.argv[1:] |
| else: |
| branches = common.CHROMEOS_BRANCHES |
| |
| os.chdir(common.get_kernel_absolute_path(kernel_metadata.path)) |
| |
| for b in branches: |
| sync_branch_method(db, b, kernel_metadata, limit) |
| |
| os.chdir(common.WORKDIR) |
| |
| |
| def new_missing_patches(): |
| """Rate limit calling create_new_fixes_in_branch.""" |
| cloudsql_db = MySQLdb.Connect(user='linux_patches_robot', host='127.0.0.1', db='linuxdb', |
| charset='utf8mb4') |
| kernel_metadata = common.get_kernel_metadata(common.Kernel.linux_stable) |
| missing_patches_sync(cloudsql_db, kernel_metadata, create_new_fixes_in_branch, |
| NEW_CL_DAILY_LIMIT_PER_STABLE_BRANCH) |
| |
| kernel_metadata = common.get_kernel_metadata(common.Kernel.linux_chrome) |
| missing_patches_sync(cloudsql_db, kernel_metadata, create_new_fixes_in_branch, |
| NEW_CL_DAILY_LIMIT_PER_BRANCH) |
| cloudsql_db.close() |
| |
| |
| def update_missing_patches(): |
| """Updates fixes table entries on regular basis.""" |
| cloudsql_db = MySQLdb.Connect(user='linux_patches_robot', host='127.0.0.1', db='linuxdb', |
| charset='utf8mb4') |
| |
| kernel_metadata = common.get_kernel_metadata(common.Kernel.linux_stable) |
| missing_patches_sync(cloudsql_db, kernel_metadata, update_fixes_in_branch) |
| |
| kernel_metadata = common.get_kernel_metadata(common.Kernel.linux_chrome) |
| missing_patches_sync(cloudsql_db, kernel_metadata, update_fixes_in_branch) |
| |
| cloudsql_db.close() |