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: str, 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: str, 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 out = subprocess.check_output(['git', 'log', '--format=%H', 65 branch + '-branchpoint..' + upstream + '/' + branch, 66 '--grep', 'cherry picked from commit ' + commit], 67 stderr=subprocess.DEVNULL) 68 return out.decode().strip() 69 70 71def canonicalize_commit(commit: str) -> str: 72 """ 73 Takes a commit-ish and returns a commit sha1 if the commit exists 74 """ 75 76 # Make sure input is valid first 77 if not is_commit_valid(commit): 78 raise argparse.ArgumentTypeError('invalid commit identifier: ' + commit) 79 80 out = subprocess.check_output(['git', 'rev-parse', commit], 81 stderr=subprocess.DEVNULL) 82 return out.decode().strip() 83 84 85def validate_branch(branch: str) -> str: 86 if '/' not in branch: 87 raise argparse.ArgumentTypeError('must be in the form `remote/branch`') 88 89 out = subprocess.check_output(['git', 'remote', '--verbose'], 90 stderr=subprocess.DEVNULL) 91 remotes = out.decode().splitlines() 92 (upstream, _) = branch.split('/') 93 valid_remote = False 94 for line in remotes: 95 if line.startswith(upstream + '\t'): 96 valid_remote = True 97 98 if not valid_remote: 99 raise argparse.ArgumentTypeError('Invalid remote: ' + upstream) 100 101 if not is_commit_valid(branch): 102 raise argparse.ArgumentTypeError('Invalid branch: ' + branch) 103 104 return branch 105 106 107if __name__ == "__main__": 108 parser = argparse.ArgumentParser(description=""" 109 Returns 0 if the commit is present in the branch, 110 1 if it's not, 111 and 2 if it couldn't be determined (eg. invalid commit) 112 """) 113 parser.add_argument('commit', 114 type=canonicalize_commit, 115 help='commit sha1') 116 parser.add_argument('branch', 117 type=validate_branch, 118 help='branch to check, in the form `remote/branch`') 119 parser.add_argument('--quiet', 120 action='store_true', 121 help='suppress all output; exit code can still be used') 122 parser.add_argument('--color', 123 choices=['auto', 'always', 'never'], 124 default='auto', 125 help='colorize output (default: true if stdout is a terminal)') 126 args = parser.parse_args() 127 128 (upstream, branch) = args.branch.split('/') 129 130 if branch_has_commit(upstream, branch, args.commit): 131 print_(args, True, 'Commit ' + args.commit + ' is in branch ' + branch) 132 exit(0) 133 134 backport = branch_has_backport_of_commit(upstream, branch, args.commit) 135 if backport: 136 print_(args, True, 137 'Commit ' + args.commit + ' was backported to branch ' + branch + ' as commit ' + backport) 138 exit(0) 139 140 print_(args, False, 'Commit ' + args.commit + ' is NOT in branch ' + branch) 141 exit(1) 142