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