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