summaryrefslogtreecommitdiff
path: root/bin/commit_in_branch.py
blob: e4e2edb50ab18d4774b51e0fe9953c7a03efc7d9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/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)