1#! /usr/bin/env python3
2
3import sys
4import re
5import argparse
6
7# partially copied from tools/repohooks/rh/hooks.py
8
9TEST_MSG = """Commit message is missing a "Flag:" line.  It must match one of the
10following case-sensitive regex:
11
12    %s
13
14The Flag: stanza is regex matched and should describe whether your change is behind a flag or flags.
15As a CL author, you'll have a consistent place to describe the risk of the proposed change by explicitly calling out the name of the flag.
16For legacy flags use EXEMPT with your flag name.
17
18Some examples below:
19
20Flag: NONE Repohook Update
21Flag: TEST_ONLY
22Flag: EXEMPT resource only update
23Flag: EXEMPT bugfix
24Flag: EXEMPT refactor
25Flag: com.android.launcher3.enable_twoline_allapps
26Flag: com.google.android.apps.nexuslauncher.zero_state_web_data_loader
27
28Check the git history for more examples. It's a regex matched field. See go/android-flag-directive for more details on various formats.
29"""
30
31def main():
32    """Check the commit message for a 'Flag:' line."""
33    parser = argparse.ArgumentParser(
34        description='Check the commit message for a Flag: line.')
35    parser.add_argument('--msg',
36                        metavar='msg',
37                        type=str,
38                        nargs='?',
39                        default='HEAD',
40                        help='commit message to process.')
41    parser.add_argument(
42        '--files',
43        metavar='files',
44        nargs='?',
45        default='',
46        help=
47        'PREUPLOAD_FILES in repo upload to determine whether the check should run for the files.')
48    parser.add_argument(
49        '--project',
50        metavar='project',
51        type=str,
52        nargs='?',
53        default='',
54        help=
55        'REPO_PROJECT in repo upload to determine whether the check should run for this project.')
56
57    # Parse the arguments
58    args = parser.parse_args()
59    desc = args.msg
60    files = args.files
61    project = args.project
62
63    if not should_run_path(project, files):
64        return
65
66    field = 'Flag'
67    none = 'NONE'
68    testOnly = 'TEST_ONLY'
69    docsOnly = 'DOCS_ONLY'
70    exempt = 'EXEMPT'
71    justification = '<justification>'
72
73    # Aconfig Flag name format = <packageName>.<flagName>
74    # package name - Contains only lowercase alphabets + digits + '.' - Ex: com.android.launcher3
75    # For now alphabets, digits, "_", "." characters are allowed in flag name.
76    # Checks if there is "one dot" between packageName and flagName and not adding stricter format check
77    #common_typos_disable
78    flagName = '([a-zA-Z0-9.]+)([.]+)([a-zA-Z0-9_.]+)'
79
80    # None and Exempt needs justification
81    exemptRegex = fr'{exempt}\s*[a-zA-Z]+'
82    noneRegex = fr'{none}\s*[a-zA-Z]+'
83    #common_typos_enable
84
85    readableRegexMsg = '\n\tFlag: '+none+' '+justification+'\n\tFlag: <packageName>.<flagName>\n\tFlag: ' +exempt+' '+justification+'\n\tFlag: '+testOnly+'\n\tFlag: '+docsOnly
86
87    flagRegex = fr'^{field}: .*$'
88    check_flag = re.compile(flagRegex) #Flag:
89
90    # Ignore case for flag name format.
91    flagNameRegex = fr'(?i)^{field}:\s*({noneRegex}|{flagName}|{testOnly}|{docsOnly}|{exemptRegex})\s*'
92    check_flagName = re.compile(flagNameRegex) #Flag: <flag name format>
93
94    flagError = False
95    foundFlag = []
96    # Check for multiple "Flag:" lines and all lines should match this format
97    for line in desc.splitlines():
98        if check_flag.match(line):
99            if not check_flagName.match(line):
100                flagError = True
101                break
102            foundFlag.append(line)
103
104    # Throw error if
105    # 1. No "Flag:" line is found
106    # 2. "Flag:" doesn't follow right format.
107    if (not foundFlag) or (flagError):
108        error = TEST_MSG % (readableRegexMsg)
109        print(error)
110        sys.exit(1)
111
112    sys.exit(0)
113
114
115def should_run_path(project, files):
116    """Returns a boolean if this check should run with these paths.
117    If you want to check for a particular subdirectory under the path,
118    add a check here, call should_run_files and check for a specific sub dir path in should_run_files.
119    """
120    if not project:
121        return False
122    if project == 'platform/frameworks/base':
123        return should_run_files(files)
124    # Default case, run for all other projects which calls this script.
125    return True
126
127
128def should_run_files(files):
129    """Returns a boolean if this check should run with these files."""
130    if not files:
131        return False
132    if 'packages/SystemUI' in files:
133        return True
134    return False
135
136
137if __name__ == '__main__':
138    main()
139