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