introduce `commit_in_branch.py` script to help devs figure this out
It's been pointed out to me that determining whether a commit is present in a stable branch is non-trivial (cherry-picks are a pain to search for) and the commands are hard to remember, making it too much to ask. This script aims to solve that problem; at its simplest form, it only takes a commit and a branch and tells the user whether that commit predates the branch, was cherry-picked to it, or is not present in any form in the branch. $ bin/commit_in_branch.pye58a10af64
fdo/20.1 Commite58a10af64
is in branch 20.1 $ echo $? 0 $ bin/commit_in_branch.pydd2bd68fa6
fdo/20.1 Commitdd2bd68fa6
was backported to branch 20.1 as commit d043d24654c851f0be57dbbf48274b5373dea42b $ echo $? 0 $ bin/commit_in_branch.py master fdo/20.1 Commit 2fbcfe170bf50fcbcd2fc70a564a4d69096d968c is NOT in branch 20.1 $ echo $? 1 Signed-off-by: Eric Engestrom <eric@engestrom.ch> Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/5306>
This commit is contained in:
parent
40a6de176d
commit
7f61f4180b
|
@ -0,0 +1,141 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def print_(args: argparse.Namespace, success: bool, message: str) -> None:
|
||||
"""
|
||||
Print function with extra coloring when supported and/or requested,
|
||||
and with a "quiet" switch
|
||||
"""
|
||||
|
||||
COLOR_SUCCESS = '\033[32m'
|
||||
COLOR_FAILURE = '\033[31m'
|
||||
COLOR_RESET = '\033[0m'
|
||||
|
||||
if args.quiet:
|
||||
return
|
||||
|
||||
if args.color == 'auto':
|
||||
use_colors = sys.stdout.isatty()
|
||||
else:
|
||||
use_colors = args.color == 'always'
|
||||
|
||||
s = ''
|
||||
if use_colors:
|
||||
if success:
|
||||
s += COLOR_SUCCESS
|
||||
else:
|
||||
s += COLOR_FAILURE
|
||||
|
||||
s += message
|
||||
|
||||
if use_colors:
|
||||
s += COLOR_RESET
|
||||
|
||||
print(s)
|
||||
|
||||
|
||||
def is_commit_valid(commit: str) -> bool:
|
||||
ret = subprocess.call(['git', 'cat-file', '-e', commit],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
return ret == 0
|
||||
|
||||
|
||||
def branch_has_commit(upstream: str, branch: str, commit: str) -> bool:
|
||||
"""
|
||||
Returns True if the commit is actually present in the branch
|
||||
"""
|
||||
ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
|
||||
commit, upstream + '/' + branch],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
return ret == 0
|
||||
|
||||
|
||||
def branch_has_backport_of_commit(upstream: str, branch: str, commit: str) -> str:
|
||||
"""
|
||||
Returns the commit hash if the commit has been backported to the branch,
|
||||
or an empty string if is hasn't
|
||||
"""
|
||||
out = subprocess.check_output(['git', 'log', '--format=%H',
|
||||
branch + '-branchpoint..' + upstream + '/' + branch,
|
||||
'--grep', 'cherry picked from commit ' + commit],
|
||||
stderr=subprocess.DEVNULL)
|
||||
return out.decode().strip()
|
||||
|
||||
|
||||
def canonicalize_commit(commit: str) -> str:
|
||||
"""
|
||||
Takes a commit-ish and returns a commit sha1 if the commit exists
|
||||
"""
|
||||
|
||||
# Make sure input is valid first
|
||||
if not is_commit_valid(commit):
|
||||
raise argparse.ArgumentTypeError('invalid commit identifier: ' + commit)
|
||||
|
||||
out = subprocess.check_output(['git', 'rev-parse', commit],
|
||||
stderr=subprocess.DEVNULL)
|
||||
return out.decode().strip()
|
||||
|
||||
|
||||
def validate_branch(branch: str) -> str:
|
||||
if '/' not in branch:
|
||||
raise argparse.ArgumentTypeError('must be in the form `remote/branch`')
|
||||
|
||||
out = subprocess.check_output(['git', 'remote', '--verbose'],
|
||||
stderr=subprocess.DEVNULL)
|
||||
remotes = out.decode().splitlines()
|
||||
(upstream, _) = branch.split('/')
|
||||
valid_remote = False
|
||||
for line in remotes:
|
||||
if line.startswith(upstream + '\t'):
|
||||
valid_remote = True
|
||||
|
||||
if not valid_remote:
|
||||
raise argparse.ArgumentTypeError('Invalid remote: ' + upstream)
|
||||
|
||||
if not is_commit_valid(branch):
|
||||
raise argparse.ArgumentTypeError('Invalid branch: ' + branch)
|
||||
|
||||
return branch
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="""
|
||||
Returns 0 if the commit is present in the branch,
|
||||
1 if it's not,
|
||||
and 2 if it couldn't be determined (eg. invalid commit)
|
||||
""")
|
||||
parser.add_argument('commit',
|
||||
type=canonicalize_commit,
|
||||
help='commit sha1')
|
||||
parser.add_argument('branch',
|
||||
type=validate_branch,
|
||||
help='branch to check, in the form `remote/branch`')
|
||||
parser.add_argument('--quiet',
|
||||
action='store_true',
|
||||
help='suppress all output; exit code can still be used')
|
||||
parser.add_argument('--color',
|
||||
choices=['auto', 'always', 'never'],
|
||||
default='auto',
|
||||
help='colorize output (default: true if stdout is a terminal)')
|
||||
args = parser.parse_args()
|
||||
|
||||
(upstream, branch) = args.branch.split('/')
|
||||
|
||||
if branch_has_commit(upstream, branch, args.commit):
|
||||
print_(args, True, 'Commit ' + args.commit + ' is in branch ' + branch)
|
||||
exit(0)
|
||||
|
||||
backport = branch_has_backport_of_commit(upstream, branch, args.commit)
|
||||
if backport:
|
||||
print_(args, True,
|
||||
'Commit ' + args.commit + ' was backported to branch ' + branch + ' as commit ' + backport)
|
||||
exit(0)
|
||||
|
||||
print_(args, False, 'Commit ' + args.commit + ' is NOT in branch ' + branch)
|
||||
exit(1)
|
|
@ -0,0 +1,116 @@
|
|||
import argparse
|
||||
import pytest # type: ignore
|
||||
import subprocess
|
||||
|
||||
from .commit_in_branch import (
|
||||
is_commit_valid,
|
||||
branch_has_commit,
|
||||
branch_has_backport_of_commit,
|
||||
canonicalize_commit,
|
||||
validate_branch,
|
||||
)
|
||||
|
||||
|
||||
def get_upstream() -> str:
|
||||
# Let's assume master is bound to the upstream remote and not a fork
|
||||
out = subprocess.check_output(['git', 'for-each-ref',
|
||||
'--format=%(upstream)',
|
||||
'refs/heads/master'],
|
||||
stderr=subprocess.DEVNULL)
|
||||
return out.decode().strip().split('/')[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'commit, expected',
|
||||
[
|
||||
('20.1-branchpoint', True),
|
||||
('master', True),
|
||||
('e58a10af640ba58b6001f5c5ad750b782547da76', True),
|
||||
('d043d24654c851f0be57dbbf48274b5373dea42b', True),
|
||||
('dd2bd68fa69124c86cd008b256d06f44fab8e6cd', True),
|
||||
('0000000000000000000000000000000000000000', False),
|
||||
('not-even-a-valid-commit-format', False),
|
||||
])
|
||||
def test_canonicalize_commit(commit: str, expected: bool) -> None:
|
||||
if expected:
|
||||
assert canonicalize_commit(commit)
|
||||
else:
|
||||
try:
|
||||
assert canonicalize_commit(commit)
|
||||
except argparse.ArgumentTypeError:
|
||||
return
|
||||
assert False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'commit, expected',
|
||||
[
|
||||
(get_upstream() + '/20.1', True),
|
||||
(get_upstream() + '/master', True),
|
||||
('20.1', False),
|
||||
('master', False),
|
||||
('e58a10af640ba58b6001f5c5ad750b782547da76', False),
|
||||
('d043d24654c851f0be57dbbf48274b5373dea42b', False),
|
||||
('dd2bd68fa69124c86cd008b256d06f44fab8e6cd', False),
|
||||
('0000000000000000000000000000000000000000', False),
|
||||
('not-even-a-valid-commit-format', False),
|
||||
])
|
||||
def test_validate_branch(commit: str, expected: bool) -> None:
|
||||
if expected:
|
||||
assert validate_branch(commit)
|
||||
else:
|
||||
try:
|
||||
assert validate_branch(commit)
|
||||
except argparse.ArgumentTypeError:
|
||||
return
|
||||
assert False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'commit, expected',
|
||||
[
|
||||
('master', True),
|
||||
('20.1-branchpoint', True),
|
||||
('20.1', False),
|
||||
(get_upstream() + '/20.1', True),
|
||||
('e58a10af640ba58b6001f5c5ad750b782547da76', True),
|
||||
('d043d24654c851f0be57dbbf48274b5373dea42b', True),
|
||||
('dd2bd68fa69124c86cd008b256d06f44fab8e6cd', True),
|
||||
('0000000000000000000000000000000000000000', False),
|
||||
('not-even-a-valid-commit-format', False),
|
||||
])
|
||||
def test_is_commit_valid(commit: str, expected: bool) -> None:
|
||||
assert is_commit_valid(commit) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'branch, commit, expected',
|
||||
[
|
||||
('20.1', '20.1-branchpoint', True),
|
||||
('20.1', '20.0', False),
|
||||
('20.1', 'master', False),
|
||||
('20.1', 'e58a10af640ba58b6001f5c5ad750b782547da76', True),
|
||||
('20.1', 'd043d24654c851f0be57dbbf48274b5373dea42b', True),
|
||||
('20.1', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', False),
|
||||
('master', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', True),
|
||||
('20.0', 'd043d24654c851f0be57dbbf48274b5373dea42b', False),
|
||||
])
|
||||
def test_branch_has_commit(branch: str, commit: str, expected: bool) -> None:
|
||||
upstream = get_upstream()
|
||||
assert branch_has_commit(upstream, branch, commit) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'branch, commit, expected',
|
||||
[
|
||||
('20.1', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', 'd043d24654c851f0be57dbbf48274b5373dea42b'),
|
||||
('20.1', '20.1-branchpoint', ''),
|
||||
('20.1', '20.0', ''),
|
||||
('20.1', '20.2', ''),
|
||||
('20.1', 'master', ''),
|
||||
('20.1', 'd043d24654c851f0be57dbbf48274b5373dea42b', ''),
|
||||
('20.0', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', ''),
|
||||
])
|
||||
def test_branch_has_backport_of_commit(branch: str, commit: str, expected: bool) -> None:
|
||||
upstream = get_upstream()
|
||||
assert branch_has_backport_of_commit(upstream, branch, commit) == expected
|
Loading…
Reference in New Issue