]>
Commit | Line | Data |
---|---|---|
8781b143 MK |
1 | ## @file\r |
2 | # Retrieves the people to request review from on submission of a commit.\r | |
3 | #\r | |
4 | # Copyright (c) 2019, Linaro Ltd. All rights reserved.<BR>\r | |
5 | #\r | |
6 | # SPDX-License-Identifier: BSD-2-Clause-Patent\r | |
7 | #\r | |
8 | \r | |
9 | from __future__ import print_function\r | |
10 | from collections import defaultdict\r | |
11 | from collections import OrderedDict\r | |
12 | import argparse\r | |
13 | import os\r | |
14 | import re\r | |
15 | import SetupGit\r | |
16 | \r | |
17 | EXPRESSIONS = {\r | |
18 | 'exclude': re.compile(r'^X:\s*(?P<exclude>.*?)\r*$'),\r | |
19 | 'file': re.compile(r'^F:\s*(?P<file>.*?)\r*$'),\r | |
20 | 'list': re.compile(r'^L:\s*(?P<list>.*?)\r*$'),\r | |
28ef05ce | 21 | 'maintainer': re.compile(r'^M:\s*(?P<maintainer>.*?)\r*$'),\r |
8781b143 MK |
22 | 'reviewer': re.compile(r'^R:\s*(?P<reviewer>.*?)\r*$'),\r |
23 | 'status': re.compile(r'^S:\s*(?P<status>.*?)\r*$'),\r | |
24 | 'tree': re.compile(r'^T:\s*(?P<tree>.*?)\r*$'),\r | |
25 | 'webpage': re.compile(r'^W:\s*(?P<webpage>.*?)\r*$')\r | |
26 | }\r | |
27 | \r | |
28 | def printsection(section):\r | |
29 | """Prints out the dictionary describing a Maintainers.txt section."""\r | |
30 | print('===')\r | |
31 | for key in section.keys():\r | |
32 | print("Key: %s" % key)\r | |
33 | for item in section[key]:\r | |
34 | print(' %s' % item)\r | |
35 | \r | |
36 | def pattern_to_regex(pattern):\r | |
37 | """Takes a string containing regular UNIX path wildcards\r | |
38 | and returns a string suitable for matching with regex."""\r | |
39 | \r | |
40 | pattern = pattern.replace('.', r'\.')\r | |
41 | pattern = pattern.replace('?', r'.')\r | |
42 | pattern = pattern.replace('*', r'.*')\r | |
43 | \r | |
44 | if pattern.endswith('/'):\r | |
45 | pattern += r'.*'\r | |
46 | elif pattern.endswith('.*'):\r | |
47 | pattern = pattern[:-2]\r | |
48 | pattern += r'(?!.*?/.*?)'\r | |
49 | \r | |
50 | return pattern\r | |
51 | \r | |
52 | def path_in_section(path, section):\r | |
53 | """Returns True of False indicating whether the path is covered by\r | |
54 | the current section."""\r | |
55 | if not 'file' in section:\r | |
56 | return False\r | |
57 | \r | |
58 | for pattern in section['file']:\r | |
59 | regex = pattern_to_regex(pattern)\r | |
60 | \r | |
61 | match = re.match(regex, path)\r | |
62 | if match:\r | |
63 | # Check if there is an exclude pattern that applies\r | |
64 | for pattern in section['exclude']:\r | |
65 | regex = pattern_to_regex(pattern)\r | |
66 | \r | |
67 | match = re.match(regex, path)\r | |
68 | if match:\r | |
69 | return False\r | |
70 | \r | |
71 | return True\r | |
72 | \r | |
73 | return False\r | |
74 | \r | |
75 | def get_section_maintainers(path, section):\r | |
76 | """Returns a list with email addresses to any M: and R: entries\r | |
77 | matching the provided path in the provided section."""\r | |
78 | maintainers = []\r | |
79 | lists = []\r | |
f355b986 | 80 | nowarn_status = ['Supported', 'Maintained']\r |
8781b143 MK |
81 | \r |
82 | if path_in_section(path, section):\r | |
f355b986 LL |
83 | for status in section['status']:\r |
84 | if status not in nowarn_status:\r | |
85 | print('WARNING: Maintained status for "%s" is \'%s\'!' % (path, status))\r | |
8781b143 MK |
86 | for address in section['maintainer'], section['reviewer']:\r |
87 | # Convert to list if necessary\r | |
88 | if isinstance(address, list):\r | |
89 | maintainers += address\r | |
90 | else:\r | |
91 | lists += [address]\r | |
92 | for address in section['list']:\r | |
93 | # Convert to list if necessary\r | |
94 | if isinstance(address, list):\r | |
95 | lists += address\r | |
96 | else:\r | |
97 | lists += [address]\r | |
98 | \r | |
99 | return maintainers, lists\r | |
100 | \r | |
101 | def get_maintainers(path, sections, level=0):\r | |
102 | """For 'path', iterates over all sections, returning maintainers\r | |
103 | for matching ones."""\r | |
104 | maintainers = []\r | |
105 | lists = []\r | |
106 | for section in sections:\r | |
107 | tmp_maint, tmp_lists = get_section_maintainers(path, section)\r | |
108 | if tmp_maint:\r | |
109 | maintainers += tmp_maint\r | |
110 | if tmp_lists:\r | |
111 | lists += tmp_lists\r | |
112 | \r | |
113 | if not maintainers:\r | |
114 | # If no match found, look for match for (nonexistent) file\r | |
115 | # REPO.working_dir/<default>\r | |
116 | print('"%s": no maintainers found, looking for default' % path)\r | |
117 | if level == 0:\r | |
118 | maintainers = get_maintainers('<default>', sections, level=level + 1)\r | |
119 | else:\r | |
120 | print("No <default> maintainers set for project.")\r | |
121 | if not maintainers:\r | |
122 | return None\r | |
123 | \r | |
124 | return maintainers + lists\r | |
125 | \r | |
126 | def parse_maintainers_line(line):\r | |
127 | """Parse one line of Maintainers.txt, returning any match group and its key."""\r | |
128 | for key, expression in EXPRESSIONS.items():\r | |
129 | match = expression.match(line)\r | |
130 | if match:\r | |
131 | return key, match.group(key)\r | |
132 | return None, None\r | |
133 | \r | |
134 | def parse_maintainers_file(filename):\r | |
135 | """Parse the Maintainers.txt from top-level of repo and\r | |
136 | return a list containing dictionaries of all sections."""\r | |
137 | with open(filename, 'r') as text:\r | |
138 | line = text.readline()\r | |
139 | sectionlist = []\r | |
140 | section = defaultdict(list)\r | |
141 | while line:\r | |
142 | key, value = parse_maintainers_line(line)\r | |
143 | if key and value:\r | |
144 | section[key].append(value)\r | |
145 | \r | |
146 | line = text.readline()\r | |
147 | # If end of section (end of file, or non-tag line encountered)...\r | |
148 | if not key or not value or not line:\r | |
149 | # ...if non-empty, append section to list.\r | |
150 | if section:\r | |
151 | sectionlist.append(section.copy())\r | |
152 | section.clear()\r | |
153 | \r | |
154 | return sectionlist\r | |
155 | \r | |
156 | def get_modified_files(repo, args):\r | |
157 | """Returns a list of the files modified by the commit specified in 'args'."""\r | |
158 | commit = repo.commit(args.commit)\r | |
159 | return commit.stats.files\r | |
160 | \r | |
161 | if __name__ == '__main__':\r | |
162 | PARSER = argparse.ArgumentParser(\r | |
163 | description='Retrieves information on who to cc for review on a given commit')\r | |
164 | PARSER.add_argument('commit',\r | |
165 | action="store",\r | |
166 | help='git revision to examine (default: HEAD)',\r | |
167 | nargs='?',\r | |
168 | default='HEAD')\r | |
169 | PARSER.add_argument('-l', '--lookup',\r | |
170 | help='Find section matches for path LOOKUP',\r | |
171 | required=False)\r | |
172 | ARGS = PARSER.parse_args()\r | |
173 | \r | |
174 | REPO = SetupGit.locate_repo()\r | |
175 | \r | |
176 | CONFIG_FILE = os.path.join(REPO.working_dir, 'Maintainers.txt')\r | |
177 | \r | |
178 | SECTIONS = parse_maintainers_file(CONFIG_FILE)\r | |
179 | \r | |
180 | if ARGS.lookup:\r | |
28ef05ce | 181 | FILES = [ARGS.lookup.replace('\\','/')]\r |
8781b143 MK |
182 | else:\r |
183 | FILES = get_modified_files(REPO, ARGS)\r | |
184 | \r | |
185 | ADDRESSES = []\r | |
186 | \r | |
187 | for file in FILES:\r | |
188 | print(file)\r | |
189 | addresslist = get_maintainers(file, SECTIONS)\r | |
190 | if addresslist:\r | |
191 | ADDRESSES += addresslist\r | |
192 | \r | |
193 | for address in list(OrderedDict.fromkeys(ADDRESSES)):\r | |
28ef05ce MK |
194 | if '<' in address and '>' in address:\r |
195 | address = address.split('>', 1)[0] + '>'\r | |
8781b143 | 196 | print(' %s' % address)\r |