blob: 343d6abca4d23ff7e1078059ed8d82506adf71ca [file] [log] [blame] [edit]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Report branches including the provided fix(es)
"""
import argparse
import re
import subprocess
import sys
STABLE_URLS = [
'git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git',
]
UPSTREAM_URLS = [
'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
]
STABLE_QUEUE_URLS = [
'git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable-rc.git'
]
CHROMEOS_URLS = [
'https://chromium.googlesource.com/chromiumos/third_party/kernel'
]
BRANCHES = ('6.1', '5.15', '5.10', '5.4', '4.19', '4.14')
CHROMEOS_RELEASE_BRANCHES = ('R108-15183.B', 'R114-15437.B',
'R115-15474.B', 'R116-15509.B')
def _git(args, stdin=None, encoding='utf-8'):
"""Calls a git subcommand.
Similar to subprocess.check_output.
Args:
args: subcommand + args passed to 'git'.
stdin: a string or bytes (depending on encoding) that will be passed
to the git subcommand.
encoding: either 'utf-8' (default) or None. Override it to None if
you want both stdin and stdout to be raw bytes.
Returns:
the stdout of the git subcommand, same type as stdin. The output is
also run through strip to make sure there's no extra whitespace.
Raises:
subprocess.CalledProcessError: when return code is not zero.
The exception has a .returncode attribute.
"""
try:
# print(['git'] + args)
return subprocess.run(
['git'] + args,
encoding=encoding,
input=stdin,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=True,
).stdout.strip()
except subprocess.CalledProcessError:
return None
def _find_remote(urls):
"""Find a remote pointing to a given repository."""
# https remotes may end with or without '/'. Cover both variants.
_urls = []
for url in urls:
_urls.append(url)
if url.startswith('https://'):
if url.endswith('/'):
_urls.append(url.rstrip('/'))
else:
_urls.append(url + '/')
for remote in _git(['remote']).splitlines():
try:
if _git(['remote', 'get-url', remote]) in _urls:
return remote
except subprocess.CalledProcessError:
# Kinda weird, get-url failing on an item that git just gave us.
continue
return None
def integrated(sha):
"""Find tag at which a given SHA was integrated"""
fixed = _git(['describe', '--match', 'v*', '--contains', sha])
if fixed:
fixed = fixed.split('~')[0]
else:
# This patch may not be upstream.
upstream = _find_remote(UPSTREAM_URLS)
gitcommand = ['merge-base', '--is-ancestor', sha, f'{upstream}/master']
if _git(gitcommand) is not None:
fixed = "ToT"
else:
fixed = None
return fixed
def compare_releases(r1, r2):
"""Compare two releases.
Return 0 if equal, <0 if r1 is older than r2, >0 if r1 is newer than r2
"""
s1 = 0
s2 = 0
if r1 == r2:
return 0
if r1 == 'ToT':
return 1
if r2 == 'ToT':
return -1
m = re.match(r'(?:v)(\d+).(\d+)(?:\.(\d+))?.*', r1)
if m:
s1 = int(m.group(1)) * 1000000
s1 += int(m.group(2)) * 1000
if m.group(3) is not None:
s1 += int(m.group(3))
m = re.match(r'(?:v)?(\d+).(\d+)(?:\.(\d+))?.*', r2)
if m:
s2 = int(m.group(1)) * 1000000
s2 += int(m.group(2)) * 1000
if m.group(3) is not None:
s2 += int(m.group(3))
if s1 == s2:
return 0
if s1 < s2:
return -1
return 1
def checkbranch(remote, baseline, subject, queued=False):
"""Check if a commit is present in a branch.
Check if a commit described by its subject line is present
in the provided remote and branch (identified by its baseline version
number)
"""
found=False
gitcommand = ['log', '--oneline', f'v{baseline}..{remote}/linux-{baseline}.y']
commits = _git(gitcommand)
if commits: # pylint: disable=too-many-nested-blocks
for commit in commits.splitlines():
if subject in commit:
ssha = commit.split(' ')[0]
isha = integrated(ssha)
if not isha or isha == 'ToT':
if queued:
print((f' Expected to be fixed in chromeos-{baseline} '
f'with next stable release merge (sha {ssha})'))
elif not queued:
msg=f' Fixed in chromeos-{baseline} with merge of {isha} (sha {ssha})'
print(msg)
# Now check release branches
# FIXME: Patches may be in release branches but use a different SHA there.
# Output in that case should be "Fixed in <Release> (sha <sha>)"
chromeos_remote = _find_remote(CHROMEOS_URLS)
if chromeos_remote:
contained = []
not_contained = []
for b in CHROMEOS_RELEASE_BRANCHES:
release_branch = f'release-{b}-chromeos-{baseline}'
release = b.split('-', maxsplit=1)[0]
gitcommand = ['merge-base', '--is-ancestor', ssha,
f'{chromeos_remote}/{release_branch}']
result = _git(gitcommand)
if result is None:
not_contained += [release]
else:
contained += [release]
if not_contained:
print(' Not in ' + ', '.join(not_contained))
if contained:
print(' In ' + ', '.join(contained))
found=True
break
return found
def main(args):
"""Main entrypoint.
Args:
args: sys.argv[1:]
Returns:
An int return code.
"""
parser = argparse.ArgumentParser()
parser.add_argument('shas', nargs='+',
help='A valid SHA')
args = vars(parser.parse_args(args))
remote = _find_remote(STABLE_URLS)
if remote:
_git(['fetch', remote])
else:
print('Stable remote not found, results may be incomplete')
queue_remote = _find_remote(STABLE_QUEUE_URLS)
if queue_remote:
_git(['fetch', queue_remote])
else:
print('Stable queue remote not found, results may be incomplete')
chromeos_remote = _find_remote(CHROMEOS_URLS)
if chromeos_remote:
_git(['fetch', chromeos_remote])
else:
print('ChromeOS remote not found, results may be incomplete')
# Try to find and fetch upstream
upstream = _find_remote(UPSTREAM_URLS)
if upstream:
_git(['fetch', upstream])
for sha in args['shas']:
subject = _git(['show', '--pretty=format:%s', '-s', sha])
if not subject:
print(f'Subject not found for SHA {sha}')
sys.exit(1)
full_sha = _git(['show', '--pretty=format:%H', '-s', sha])
if not full_sha:
print(f'Failed to extract full SHA for SHA {sha}')
sys.exit(1)
ssha = full_sha[0:11]
integrated_branch = integrated(ssha)
if integrated_branch:
print(f'Upstream commit {ssha} ("{subject}")')
print(f' Integrated in {integrated_branch}')
else:
# If the patch is not upstream, do not bother trying to find
# ChromeOS branches.
# FIXME: We should try to find non-upstream patches as well.
print(f'Commit {ssha} ("{subject}")')
print(' Not found in upstream kernel')
continue
for branch in BRANCHES:
if compare_releases(integrated_branch, branch) > 0:
if (not checkbranch(remote, branch, subject) and
(not queue_remote
or not checkbranch(queue_remote, branch, subject, queued=True))):
print(f' Not in chromeos-{branch}')
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))