summaryrefslogtreecommitdiffstats
path: root/BaseTools/Scripts/GetMaintainer.py
blob: 1361fb6c0e3021e5a3b33e20b21363c5ceb76539 (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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
## @file
#  Retrieves the people to request review from on submission of a commit.
#
#  Copyright (c) 2019, Linaro Ltd. All rights reserved.<BR>
#
#  SPDX-License-Identifier: BSD-2-Clause-Patent
#

from __future__ import print_function
from collections import defaultdict
from collections import OrderedDict
import argparse
import os
import re
import SetupGit

EXPRESSIONS = {
    'exclude':    re.compile(r'^X:\s*(?P<exclude>.*?)\r*$'),
    'file':       re.compile(r'^F:\s*(?P<file>.*?)\r*$'),
    'list':       re.compile(r'^L:\s*(?P<list>.*?)\r*$'),
    'maintainer': re.compile(r'^M:\s*(?P<maintainer>.*?)\r*$'),
    'reviewer':   re.compile(r'^R:\s*(?P<reviewer>.*?)\r*$'),
    'status':     re.compile(r'^S:\s*(?P<status>.*?)\r*$'),
    'tree':       re.compile(r'^T:\s*(?P<tree>.*?)\r*$'),
    'webpage':    re.compile(r'^W:\s*(?P<webpage>.*?)\r*$')
}

def printsection(section):
    """Prints out the dictionary describing a Maintainers.txt section."""
    print('===')
    for key in section.keys():
        print("Key: %s" % key)
        for item in section[key]:
            print('  %s' % item)

def pattern_to_regex(pattern):
    """Takes a string containing regular UNIX path wildcards
       and returns a string suitable for matching with regex."""

    pattern = pattern.replace('.', r'\.')
    pattern = pattern.replace('?', r'.')
    pattern = pattern.replace('*', r'.*')

    if pattern.endswith('/'):
        pattern += r'.*'
    elif pattern.endswith('.*'):
        pattern = pattern[:-2]
        pattern += r'(?!.*?/.*?)'

    return pattern

def path_in_section(path, section):
    """Returns True of False indicating whether the path is covered by
       the current section."""
    if not 'file' in section:
        return False

    for pattern in section['file']:
        regex = pattern_to_regex(pattern)

        match = re.match(regex, path)
        if match:
            # Check if there is an exclude pattern that applies
            for pattern in section['exclude']:
                regex = pattern_to_regex(pattern)

                match = re.match(regex, path)
                if match:
                    return False

            return True

    return False

def get_section_maintainers(path, section):
    """Returns a list with email addresses to any M: and R: entries
       matching the provided path in the provided section."""
    maintainers = []
    reviewers = []
    lists = []
    nowarn_status = ['Supported', 'Maintained']

    if path_in_section(path, section):
        for status in section['status']:
            if status not in nowarn_status:
                print('WARNING: Maintained status for "%s" is \'%s\'!' % (path, status))
        for address in section['maintainer']:
            # Convert to list if necessary
            if isinstance(address, list):
                maintainers += address
            else:
                maintainers += [address]
        for address in section['reviewer']:
            # Convert to list if necessary
            if isinstance(address, list):
                reviewers += address
            else:
                reviewers += [address]
        for address in section['list']:
            # Convert to list if necessary
            if isinstance(address, list):
                lists += address
            else:
                lists += [address]

    return {'maintainers': maintainers, 'reviewers': reviewers, 'lists': lists}

def get_maintainers(path, sections, level=0):
    """For 'path', iterates over all sections, returning maintainers
       for matching ones."""
    maintainers = []
    reviewers = []
    lists = []
    for section in sections:
        recipients = get_section_maintainers(path, section)
        maintainers += recipients['maintainers']
        reviewers += recipients['reviewers']
        lists += recipients['lists']

    if not maintainers:
        # If no match found, look for match for (nonexistent) file
        # REPO.working_dir/<default>
        print('"%s": no maintainers found, looking for default' % path)
        if level == 0:
            recipients = get_maintainers('<default>', sections, level=level + 1)
            maintainers += recipients['maintainers']
            reviewers += recipients['reviewers']
            lists += recipients['lists']
        else:
            print("No <default> maintainers set for project.")
        if not maintainers:
            return None

    return {'maintainers': maintainers, 'reviewers': reviewers, 'lists': lists}

def parse_maintainers_line(line):
    """Parse one line of Maintainers.txt, returning any match group and its key."""
    for key, expression in EXPRESSIONS.items():
        match = expression.match(line)
        if match:
            return key, match.group(key)
    return None, None

def parse_maintainers_file(filename):
    """Parse the Maintainers.txt from top-level of repo and
       return a list containing dictionaries of all sections."""
    with open(filename, 'r') as text:
        line = text.readline()
        sectionlist = []
        section = defaultdict(list)
        while line:
            key, value = parse_maintainers_line(line)
            if key and value:
                section[key].append(value)

            line = text.readline()
            # If end of section (end of file, or non-tag line encountered)...
            if not key or not value or not line:
                # ...if non-empty, append section to list.
                if section:
                    sectionlist.append(section.copy())
                    section.clear()

        return sectionlist

def get_modified_files(repo, args):
    """Returns a list of the files modified by the commit specified in 'args'."""
    commit = repo.commit(args.commit)
    return commit.stats.files

if __name__ == '__main__':
    PARSER = argparse.ArgumentParser(
        description='Retrieves information on who to cc for review on a given commit')
    PARSER.add_argument('commit',
                        action="store",
                        help='git revision to examine (default: HEAD)',
                        nargs='?',
                        default='HEAD')
    PARSER.add_argument('-l', '--lookup',
                        help='Find section matches for path LOOKUP',
                        required=False)
    ARGS = PARSER.parse_args()

    REPO = SetupGit.locate_repo()

    CONFIG_FILE = os.path.join(REPO.working_dir, 'Maintainers.txt')

    SECTIONS = parse_maintainers_file(CONFIG_FILE)

    if ARGS.lookup:
        FILES = [ARGS.lookup.replace('\\','/')]
    else:
        FILES = get_modified_files(REPO, ARGS)

    ADDRESSES = []

    for file in FILES:
        print(file)
        recipients = get_maintainers(file, SECTIONS)
        ADDRESSES += recipients['maintainers'] + recipients['reviewers'] + recipients['lists']

    for address in list(OrderedDict.fromkeys(ADDRESSES)):
        if '<' in address and '>' in address:
            address = address.split('>', 1)[0] + '>'
        print('  %s' % address)