#!/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)