Prepare v2023.10
[platform/kernel/u-boot.git] / tools / patman / settings.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2011 The Chromium OS Authors.
3 # Copyright (c) 2022 Maxim Cournoyer <maxim.cournoyer@savoirfairelinux.com>
4 #
5
6 try:
7     import configparser as ConfigParser
8 except Exception:
9     import ConfigParser
10
11 import argparse
12 import os
13 import re
14
15 from patman import gitutil
16
17 """Default settings per-project.
18
19 These are used by _ProjectConfigParser.  Settings names should match
20 the "dest" of the option parser from patman.py.
21 """
22 _default_settings = {
23     "u-boot": {},
24     "linux": {
25         "process_tags": "False",
26         "check_patch_use_tree": "True",
27     },
28     "gcc": {
29         "process_tags": "False",
30         "add_signoff": "False",
31         "check_patch": "False",
32     },
33 }
34
35
36 class _ProjectConfigParser(ConfigParser.ConfigParser):
37     """ConfigParser that handles projects.
38
39     There are two main goals of this class:
40     - Load project-specific default settings.
41     - Merge general default settings/aliases with project-specific ones.
42
43     # Sample config used for tests below...
44     >>> from io import StringIO
45     >>> sample_config = '''
46     ... [alias]
47     ... me: Peter P. <likesspiders@example.com>
48     ... enemies: Evil <evil@example.com>
49     ...
50     ... [sm_alias]
51     ... enemies: Green G. <ugly@example.com>
52     ...
53     ... [sm2_alias]
54     ... enemies: Doc O. <pus@example.com>
55     ...
56     ... [settings]
57     ... am_hero: True
58     ... '''
59
60     # Check to make sure that bogus project gets general alias.
61     >>> config = _ProjectConfigParser("zzz")
62     >>> config.readfp(StringIO(sample_config))
63     >>> str(config.get("alias", "enemies"))
64     'Evil <evil@example.com>'
65
66     # Check to make sure that alias gets overridden by project.
67     >>> config = _ProjectConfigParser("sm")
68     >>> config.readfp(StringIO(sample_config))
69     >>> str(config.get("alias", "enemies"))
70     'Green G. <ugly@example.com>'
71
72     # Check to make sure that settings get merged with project.
73     >>> config = _ProjectConfigParser("linux")
74     >>> config.readfp(StringIO(sample_config))
75     >>> sorted((str(a), str(b)) for (a, b) in config.items("settings"))
76     [('am_hero', 'True'), ('check_patch_use_tree', 'True'), ('process_tags', 'False')]
77
78     # Check to make sure that settings works with unknown project.
79     >>> config = _ProjectConfigParser("unknown")
80     >>> config.readfp(StringIO(sample_config))
81     >>> sorted((str(a), str(b)) for (a, b) in config.items("settings"))
82     [('am_hero', 'True')]
83     """
84     def __init__(self, project_name):
85         """Construct _ProjectConfigParser.
86
87         In addition to standard ConfigParser initialization, this also
88         loads project defaults.
89
90         Args:
91             project_name: The name of the project.
92         """
93         self._project_name = project_name
94         ConfigParser.ConfigParser.__init__(self)
95
96         # Update the project settings in the config based on
97         # the _default_settings global.
98         project_settings = "%s_settings" % project_name
99         if not self.has_section(project_settings):
100             self.add_section(project_settings)
101         project_defaults = _default_settings.get(project_name, {})
102         for setting_name, setting_value in project_defaults.items():
103             self.set(project_settings, setting_name, setting_value)
104
105     def get(self, section, option, *args, **kwargs):
106         """Extend ConfigParser to try project_section before section.
107
108         Args:
109             See ConfigParser.
110         Returns:
111             See ConfigParser.
112         """
113         try:
114             val = ConfigParser.ConfigParser.get(
115                 self, "%s_%s" % (self._project_name, section), option,
116                 *args, **kwargs
117             )
118         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
119             val = ConfigParser.ConfigParser.get(
120                 self, section, option, *args, **kwargs
121             )
122         return val
123
124     def items(self, section, *args, **kwargs):
125         """Extend ConfigParser to add project_section to section.
126
127         Args:
128             See ConfigParser.
129         Returns:
130             See ConfigParser.
131         """
132         project_items = []
133         has_project_section = False
134         top_items = []
135
136         # Get items from the project section
137         try:
138             project_items = ConfigParser.ConfigParser.items(
139                 self, "%s_%s" % (self._project_name, section), *args, **kwargs
140             )
141             has_project_section = True
142         except ConfigParser.NoSectionError:
143             pass
144
145         # Get top-level items
146         try:
147             top_items = ConfigParser.ConfigParser.items(
148                 self, section, *args, **kwargs
149             )
150         except ConfigParser.NoSectionError:
151             # If neither section exists raise the error on...
152             if not has_project_section:
153                 raise
154
155         item_dict = dict(top_items)
156         item_dict.update(project_items)
157         return {(item, val) for item, val in item_dict.items()}
158
159
160 def ReadGitAliases(fname):
161     """Read a git alias file. This is in the form used by git:
162
163     alias uboot  u-boot@lists.denx.de
164     alias wd     Wolfgang Denk <wd@denx.de>
165
166     Args:
167         fname: Filename to read
168     """
169     try:
170         fd = open(fname, 'r', encoding='utf-8')
171     except IOError:
172         print("Warning: Cannot find alias file '%s'" % fname)
173         return
174
175     re_line = re.compile(r'alias\s+(\S+)\s+(.*)')
176     for line in fd.readlines():
177         line = line.strip()
178         if not line or line[0] == '#':
179             continue
180
181         m = re_line.match(line)
182         if not m:
183             print("Warning: Alias file line '%s' not understood" % line)
184             continue
185
186         list = alias.get(m.group(1), [])
187         for item in m.group(2).split(','):
188             item = item.strip()
189             if item:
190                 list.append(item)
191         alias[m.group(1)] = list
192
193     fd.close()
194
195
196 def CreatePatmanConfigFile(config_fname):
197     """Creates a config file under $(HOME)/.patman if it can't find one.
198
199     Args:
200         config_fname: Default config filename i.e., $(HOME)/.patman
201
202     Returns:
203         None
204     """
205     name = gitutil.get_default_user_name()
206     if name is None:
207         name = input("Enter name: ")
208
209     email = gitutil.get_default_user_email()
210
211     if email is None:
212         email = input("Enter email: ")
213
214     try:
215         f = open(config_fname, 'w')
216     except IOError:
217         print("Couldn't create patman config file\n")
218         raise
219
220     print('''[alias]
221 me: %s <%s>
222
223 [bounces]
224 nxp = Zhikang Zhang <zhikang.zhang@nxp.com>
225 ''' % (name, email), file=f)
226     f.close()
227
228
229 def _UpdateDefaults(main_parser, config):
230     """Update the given OptionParser defaults based on config.
231
232     We'll walk through all of the settings from all parsers.
233     For each setting we'll look for a default in the option parser.
234     If it's found we'll update the option parser default.
235
236     The idea here is that the .patman file should be able to update
237     defaults but that command line flags should still have the final
238     say.
239
240     Args:
241         parser: An instance of an ArgumentParser whose defaults will be
242             updated.
243         config: An instance of _ProjectConfigParser that we will query
244             for settings.
245     """
246     # Find all the parsers and subparsers
247     parsers = [main_parser]
248     parsers += [subparser for action in main_parser._actions
249                 if isinstance(action, argparse._SubParsersAction)
250                 for _, subparser in action.choices.items()]
251
252     # Collect the defaults from each parser
253     defaults = {}
254     parser_defaults = []
255     for parser in parsers:
256         pdefs = parser.parse_known_args()[0]
257         parser_defaults.append(pdefs)
258         defaults.update(vars(pdefs))
259
260     # Go through the settings and collect defaults
261     for name, val in config.items('settings'):
262         if name in defaults:
263             default_val = defaults[name]
264             if isinstance(default_val, bool):
265                 val = config.getboolean('settings', name)
266             elif isinstance(default_val, int):
267                 val = config.getint('settings', name)
268             elif isinstance(default_val, str):
269                 val = config.get('settings', name)
270             defaults[name] = val
271         else:
272             print("WARNING: Unknown setting %s" % name)
273
274     # Set all the defaults and manually propagate them to subparsers
275     main_parser.set_defaults(**defaults)
276     for parser, pdefs in zip(parsers, parser_defaults):
277         parser.set_defaults(**{k: v for k, v in defaults.items()
278                                if k in pdefs})
279
280
281 def _ReadAliasFile(fname):
282     """Read in the U-Boot git alias file if it exists.
283
284     Args:
285         fname: Filename to read.
286     """
287     if os.path.exists(fname):
288         bad_line = None
289         with open(fname, encoding='utf-8') as fd:
290             linenum = 0
291             for line in fd:
292                 linenum += 1
293                 line = line.strip()
294                 if not line or line.startswith('#'):
295                     continue
296                 words = line.split(None, 2)
297                 if len(words) < 3 or words[0] != 'alias':
298                     if not bad_line:
299                         bad_line = "%s:%d:Invalid line '%s'" % (fname, linenum,
300                                                                 line)
301                     continue
302                 alias[words[1]] = [s.strip() for s in words[2].split(',')]
303         if bad_line:
304             print(bad_line)
305
306
307 def _ReadBouncesFile(fname):
308     """Read in the bounces file if it exists
309
310     Args:
311         fname: Filename to read.
312     """
313     if os.path.exists(fname):
314         with open(fname) as fd:
315             for line in fd:
316                 if line.startswith('#'):
317                     continue
318                 bounces.add(line.strip())
319
320
321 def GetItems(config, section):
322     """Get the items from a section of the config.
323
324     Args:
325         config: _ProjectConfigParser object containing settings
326         section: name of section to retrieve
327
328     Returns:
329         List of (name, value) tuples for the section
330     """
331     try:
332         return config.items(section)
333     except ConfigParser.NoSectionError:
334         return []
335
336
337 def Setup(parser, project_name, config_fname=None):
338     """Set up the settings module by reading config files.
339
340     Unless `config_fname` is specified, a `.patman` config file local
341     to the git repository is consulted, followed by the global
342     `$HOME/.patman`. If none exists, the later is created. Values
343     defined in the local config file take precedence over those
344     defined in the global one.
345
346     Args:
347         parser:         The parser to update.
348         project_name:   Name of project that we're working on; we'll look
349             for sections named "project_section" as well.
350         config_fname:   Config filename to read.  An error is raised if it
351             does not exist.
352     """
353     # First read the git alias file if available
354     _ReadAliasFile('doc/git-mailrc')
355     config = _ProjectConfigParser(project_name)
356
357     if config_fname and not os.path.exists(config_fname):
358         raise Exception(f'provided {config_fname} does not exist')
359
360     if not config_fname:
361         config_fname = '%s/.patman' % os.getenv('HOME')
362     has_config = os.path.exists(config_fname)
363
364     git_local_config_fname = os.path.join(gitutil.get_top_level(), '.patman')
365     has_git_local_config = os.path.exists(git_local_config_fname)
366
367     # Read the git local config last, so that its values override
368     # those of the global config, if any.
369     if has_config:
370         config.read(config_fname)
371     if has_git_local_config:
372         config.read(git_local_config_fname)
373
374     if not (has_config or has_git_local_config):
375         print("No config file found.\nCreating ~/.patman...\n")
376         CreatePatmanConfigFile(config_fname)
377
378     for name, value in GetItems(config, 'alias'):
379         alias[name] = value.split(',')
380
381     _ReadBouncesFile('doc/bounces')
382     for name, value in GetItems(config, 'bounces'):
383         bounces.add(value)
384
385     _UpdateDefaults(parser, config)
386
387
388 # These are the aliases we understand, indexed by alias. Each member is a list.
389 alias = {}
390 bounces = set()
391
392 if __name__ == "__main__":
393     import doctest
394
395     doctest.testmod()