1#!/usr/bin/env python3 2 3import argparse 4import subprocess 5import sys 6 7 8def print_(args: argparse.Namespace, success: bool, message: str) -> None: 9 """ 10 Print function with extra coloring when supported and/or requested, 11 and with a "quiet" switch 12 """ 13 14 COLOR_SUCCESS = '\033[32m' 15 COLOR_FAILURE = '\033[31m' 16 COLOR_RESET = '\033[0m' 17 18 if args.quiet: 19 return 20 21 if args.color == 'auto': 22 use_colors = sys.stdout.isatty() 23 else: 24 use_colors = args.color == 'always' 25 26 s = '' 27 if use_colors: 28 if success: 29 s += COLOR_SUCCESS 30 else: 31 s += COLOR_FAILURE 32 33 s += message 34 35 if use_colors: 36 s += COLOR_RESET 37 38 print(s) 39 40 41def is_commit_valid(commit: str) -> bool: 42 ret = subprocess.call(['git', 'cat-file', '-e', commit], 43 stdout=subprocess.DEVNULL, 44 stderr=subprocess.DEVNULL) 45 return ret == 0 46 47 48def branch_has_commit(upstream_branch: str, commit: str) -> bool: 49 """ 50 Returns True if the commit is actually present in the branch 51 """ 52 ret = subprocess.call(['git', 'merge-base', '--is-ancestor', 53 commit, upstream_branch], 54 stdout=subprocess.DEVNULL, 55 stderr=subprocess.DEVNULL) 56 return ret == 0 57 58 59def branch_has_backport_of_commit(upstream_branch: str, commit: str) -> str: 60 """ 61 Returns the commit hash if the commit has been backported to the branch, 62 or an empty string if is hasn't 63 """ 64 upstream, _ = upstream_branch.split('/', 1) 65 66 out = subprocess.check_output(['git', 'log', '--format=%H', 67 upstream + '..' + upstream_branch, 68 '--grep', 'cherry picked from commit ' + commit], 69 stderr=subprocess.DEVNULL) 70 return out.decode().strip() 71 72 73def canonicalize_commit(commit: str) -> str: 74 """ 75 Takes a commit-ish and returns a commit sha1 if the commit exists 76 """ 77 78 # Make sure input is valid first 79 if not is_commit_valid(commit): 80 raise argparse.ArgumentTypeError('invalid commit identifier: ' + commit) 81 82 out = subprocess.check_output(['git', 'rev-parse', commit], 83 stderr=subprocess.DEVNULL) 84 return out.decode().strip() 85 86 87def validate_branch(branch: str) -> str: 88 if '/' not in branch: 89 raise argparse.ArgumentTypeError('must be in the form `remote/branch`') 90 91 out = subprocess.check_output(['git', 'remote', '--verbose'], 92 stderr=subprocess.DEVNULL) 93 remotes = out.decode().splitlines() 94 upstream, _ = branch.split('/', 1) 95 valid_remote = False 96 for line in remotes: 97 if line.startswith(upstream + '\t'): 98 valid_remote = True 99 100 if not valid_remote: 101 raise argparse.ArgumentTypeError('Invalid remote: ' + upstream) 102 103 if not is_commit_valid(branch): 104 raise argparse.ArgumentTypeError('Invalid branch: ' + branch) 105 106 return branch 107 108 109if __name__ == "__main__": 110 parser = argparse.ArgumentParser(description=""" 111 Returns 0 if the commit is present in the branch, 112 1 if it's not, 113 and 2 if it couldn't be determined (eg. invalid commit) 114 """) 115 parser.add_argument('commit', 116 type=canonicalize_commit, 117 help='commit sha1') 118 parser.add_argument('branch', 119 type=validate_branch, 120 help='branch to check, in the form `remote/branch`') 121 parser.add_argument('--quiet', 122 action='store_true', 123 help='suppress all output; exit code can still be used') 124 parser.add_argument('--color', 125 choices=['auto', 'always', 'never'], 126 default='auto', 127 help='colorize output (default: true if stdout is a terminal)') 128 args = parser.parse_args() 129 130 if branch_has_commit(args.branch, args.commit): 131 print_(args, True, 'Commit ' + args.commit + ' is in branch ' + args.branch) 132 exit(0) 133 134 backport = branch_has_backport_of_commit(args.branch, args.commit) 135 if backport: 136 print_(args, True, 137 'Commit ' + args.commit + ' was backported to branch ' + args.branch + ' as commit ' + backport) 138 exit(0) 139 140 print_(args, False, 'Commit ' + args.commit + ' is NOT in branch ' + args.branch) 141 exit(1) 142