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