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 12import os 13import re 14 15from patman import gitutil 16 17"""Default settings per-project. 18 19These are used by _ProjectConfigParser. Settings names should match 20the "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 36class _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 160def 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 196def 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] 221me: %s <%s> 222 223[bounces] 224nxp = Zhikang Zhang <zhikang.zhang@nxp.com> 225''' % (name, email), file=f) 226 f.close() 227 228 229def _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 281def _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 307def _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 321def 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 337def 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. 389alias = {} 390bounces = set() 391 392if __name__ == "__main__": 393 import doctest 394 395 doctest.testmod() 396