17ec681f3Smrg#!/usr/bin/env python3 27ec681f3Smrg 37ec681f3Smrgimport argparse 47ec681f3Smrgimport subprocess 57ec681f3Smrgimport sys 67ec681f3Smrg 77ec681f3Smrg 87ec681f3Smrgdef print_(args: argparse.Namespace, success: bool, message: str) -> None: 97ec681f3Smrg """ 107ec681f3Smrg Print function with extra coloring when supported and/or requested, 117ec681f3Smrg and with a "quiet" switch 127ec681f3Smrg """ 137ec681f3Smrg 147ec681f3Smrg COLOR_SUCCESS = '\033[32m' 157ec681f3Smrg COLOR_FAILURE = '\033[31m' 167ec681f3Smrg COLOR_RESET = '\033[0m' 177ec681f3Smrg 187ec681f3Smrg if args.quiet: 197ec681f3Smrg return 207ec681f3Smrg 217ec681f3Smrg if args.color == 'auto': 227ec681f3Smrg use_colors = sys.stdout.isatty() 237ec681f3Smrg else: 247ec681f3Smrg use_colors = args.color == 'always' 257ec681f3Smrg 267ec681f3Smrg s = '' 277ec681f3Smrg if use_colors: 287ec681f3Smrg if success: 297ec681f3Smrg s += COLOR_SUCCESS 307ec681f3Smrg else: 317ec681f3Smrg s += COLOR_FAILURE 327ec681f3Smrg 337ec681f3Smrg s += message 347ec681f3Smrg 357ec681f3Smrg if use_colors: 367ec681f3Smrg s += COLOR_RESET 377ec681f3Smrg 387ec681f3Smrg print(s) 397ec681f3Smrg 407ec681f3Smrg 417ec681f3Smrgdef is_commit_valid(commit: str) -> bool: 427ec681f3Smrg ret = subprocess.call(['git', 'cat-file', '-e', commit], 437ec681f3Smrg stdout=subprocess.DEVNULL, 447ec681f3Smrg stderr=subprocess.DEVNULL) 457ec681f3Smrg return ret == 0 467ec681f3Smrg 477ec681f3Smrg 487ec681f3Smrgdef branch_has_commit(upstream: str, branch: str, commit: str) -> bool: 497ec681f3Smrg """ 507ec681f3Smrg Returns True if the commit is actually present in the branch 517ec681f3Smrg """ 527ec681f3Smrg ret = subprocess.call(['git', 'merge-base', '--is-ancestor', 537ec681f3Smrg commit, upstream + '/' + branch], 547ec681f3Smrg stdout=subprocess.DEVNULL, 557ec681f3Smrg stderr=subprocess.DEVNULL) 567ec681f3Smrg return ret == 0 577ec681f3Smrg 587ec681f3Smrg 597ec681f3Smrgdef branch_has_backport_of_commit(upstream: str, branch: str, commit: str) -> str: 607ec681f3Smrg """ 617ec681f3Smrg Returns the commit hash if the commit has been backported to the branch, 627ec681f3Smrg or an empty string if is hasn't 637ec681f3Smrg """ 647ec681f3Smrg out = subprocess.check_output(['git', 'log', '--format=%H', 657ec681f3Smrg branch + '-branchpoint..' + upstream + '/' + branch, 667ec681f3Smrg '--grep', 'cherry picked from commit ' + commit], 677ec681f3Smrg stderr=subprocess.DEVNULL) 687ec681f3Smrg return out.decode().strip() 697ec681f3Smrg 707ec681f3Smrg 717ec681f3Smrgdef canonicalize_commit(commit: str) -> str: 727ec681f3Smrg """ 737ec681f3Smrg Takes a commit-ish and returns a commit sha1 if the commit exists 747ec681f3Smrg """ 757ec681f3Smrg 767ec681f3Smrg # Make sure input is valid first 777ec681f3Smrg if not is_commit_valid(commit): 787ec681f3Smrg raise argparse.ArgumentTypeError('invalid commit identifier: ' + commit) 797ec681f3Smrg 807ec681f3Smrg out = subprocess.check_output(['git', 'rev-parse', commit], 817ec681f3Smrg stderr=subprocess.DEVNULL) 827ec681f3Smrg return out.decode().strip() 837ec681f3Smrg 847ec681f3Smrg 857ec681f3Smrgdef validate_branch(branch: str) -> str: 867ec681f3Smrg if '/' not in branch: 877ec681f3Smrg raise argparse.ArgumentTypeError('must be in the form `remote/branch`') 887ec681f3Smrg 897ec681f3Smrg out = subprocess.check_output(['git', 'remote', '--verbose'], 907ec681f3Smrg stderr=subprocess.DEVNULL) 917ec681f3Smrg remotes = out.decode().splitlines() 927ec681f3Smrg (upstream, _) = branch.split('/') 937ec681f3Smrg valid_remote = False 947ec681f3Smrg for line in remotes: 957ec681f3Smrg if line.startswith(upstream + '\t'): 967ec681f3Smrg valid_remote = True 977ec681f3Smrg 987ec681f3Smrg if not valid_remote: 997ec681f3Smrg raise argparse.ArgumentTypeError('Invalid remote: ' + upstream) 1007ec681f3Smrg 1017ec681f3Smrg if not is_commit_valid(branch): 1027ec681f3Smrg raise argparse.ArgumentTypeError('Invalid branch: ' + branch) 1037ec681f3Smrg 1047ec681f3Smrg return branch 1057ec681f3Smrg 1067ec681f3Smrg 1077ec681f3Smrgif __name__ == "__main__": 1087ec681f3Smrg parser = argparse.ArgumentParser(description=""" 1097ec681f3Smrg Returns 0 if the commit is present in the branch, 1107ec681f3Smrg 1 if it's not, 1117ec681f3Smrg and 2 if it couldn't be determined (eg. invalid commit) 1127ec681f3Smrg """) 1137ec681f3Smrg parser.add_argument('commit', 1147ec681f3Smrg type=canonicalize_commit, 1157ec681f3Smrg help='commit sha1') 1167ec681f3Smrg parser.add_argument('branch', 1177ec681f3Smrg type=validate_branch, 1187ec681f3Smrg help='branch to check, in the form `remote/branch`') 1197ec681f3Smrg parser.add_argument('--quiet', 1207ec681f3Smrg action='store_true', 1217ec681f3Smrg help='suppress all output; exit code can still be used') 1227ec681f3Smrg parser.add_argument('--color', 1237ec681f3Smrg choices=['auto', 'always', 'never'], 1247ec681f3Smrg default='auto', 1257ec681f3Smrg help='colorize output (default: true if stdout is a terminal)') 1267ec681f3Smrg args = parser.parse_args() 1277ec681f3Smrg 1287ec681f3Smrg (upstream, branch) = args.branch.split('/') 1297ec681f3Smrg 1307ec681f3Smrg if branch_has_commit(upstream, branch, args.commit): 1317ec681f3Smrg print_(args, True, 'Commit ' + args.commit + ' is in branch ' + branch) 1327ec681f3Smrg exit(0) 1337ec681f3Smrg 1347ec681f3Smrg backport = branch_has_backport_of_commit(upstream, branch, args.commit) 1357ec681f3Smrg if backport: 1367ec681f3Smrg print_(args, True, 1377ec681f3Smrg 'Commit ' + args.commit + ' was backported to branch ' + branch + ' as commit ' + backport) 1387ec681f3Smrg exit(0) 1397ec681f3Smrg 1407ec681f3Smrg print_(args, False, 'Commit ' + args.commit + ' is NOT in branch ' + branch) 1417ec681f3Smrg exit(1) 142