1# SPDX-License-Identifier: GPL-2.0+
2#
3# Copyright 2020 Google LLC
4#
5"""Handles the main control logic of patman
6
7This module provides various functions called by the main program to implement
8the features of patman.
9"""
10
11import os
12import sys
13
14from patman import checkpatch
15from patman import gitutil
16from patman import patchstream
17from u_boot_pylib import terminal
18
19def setup():
20    """Do required setup before doing anything"""
21    gitutil.setup()
22
23def prepare_patches(col, branch, count, start, end, ignore_binary, signoff):
24    """Figure out what patches to generate, then generate them
25
26    The patch files are written to the current directory, e.g. 0001_xxx.patch
27    0002_yyy.patch
28
29    Args:
30        col (terminal.Color): Colour output object
31        branch (str): Branch to create patches from (None = current)
32        count (int): Number of patches to produce, or -1 to produce patches for
33            the current branch back to the upstream commit
34        start (int): Start partch to use (0=first / top of branch)
35        end (int): End patch to use (0=last one in series, 1=one before that,
36            etc.)
37        ignore_binary (bool): Don't generate patches for binary files
38
39    Returns:
40        Tuple:
41            Series object for this series (set of patches)
42            Filename of the cover letter as a string (None if none)
43            patch_files: List of patch filenames, each a string, e.g.
44                ['0001_xxx.patch', '0002_yyy.patch']
45    """
46    if count == -1:
47        # Work out how many patches to send if we can
48        count = (gitutil.count_commits_to_branch(branch) - start)
49
50    if not count:
51        str = 'No commits found to process - please use -c flag, or run:\n' \
52              '  git branch --set-upstream-to remote/branch'
53        sys.exit(col.build(col.RED, str))
54
55    # Read the metadata from the commits
56    to_do = count - end
57    series = patchstream.get_metadata(branch, start, to_do)
58    cover_fname, patch_files = gitutil.create_patches(
59        branch, start, to_do, ignore_binary, series, signoff)
60
61    # Fix up the patch files to our liking, and insert the cover letter
62    patchstream.fix_patches(series, patch_files)
63    if cover_fname and series.get('cover'):
64        patchstream.insert_cover_letter(cover_fname, series, to_do)
65    return series, cover_fname, patch_files
66
67def check_patches(series, patch_files, run_checkpatch, verbose, use_tree):
68    """Run some checks on a set of patches
69
70    This santiy-checks the patman tags like Series-version and runs the patches
71    through checkpatch
72
73    Args:
74        series (Series): Series object for this series (set of patches)
75        patch_files (list): List of patch filenames, each a string, e.g.
76            ['0001_xxx.patch', '0002_yyy.patch']
77        run_checkpatch (bool): True to run checkpatch.pl
78        verbose (bool): True to print out every line of the checkpatch output as
79            it is parsed
80        use_tree (bool): If False we'll pass '--no-tree' to checkpatch.
81
82    Returns:
83        bool: True if the patches had no errors, False if they did
84    """
85    # Do a few checks on the series
86    series.DoChecks()
87
88    # Check the patches
89    if run_checkpatch:
90        ok = checkpatch.check_patches(verbose, patch_files, use_tree)
91    else:
92        ok = True
93    return ok
94
95
96def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
97                  ignore_bad_tags, add_maintainers, get_maintainer_script, limit,
98                  dry_run, in_reply_to, thread, smtp_server):
99    """Email patches to the recipients
100
101    This emails out the patches and cover letter using 'git send-email'. Each
102    patch is copied to recipients identified by the patch tag and output from
103    the get_maintainer.pl script. The cover letter is copied to all recipients
104    of any patch.
105
106    To make this work a CC file is created holding the recipients for each patch
107    and the cover letter. See the main program 'cc_cmd' for this logic.
108
109    Args:
110        col (terminal.Color): Colour output object
111        series (Series): Series object for this series (set of patches)
112        cover_fname (str): Filename of the cover letter as a string (None if
113            none)
114        patch_files (list): List of patch filenames, each a string, e.g.
115            ['0001_xxx.patch', '0002_yyy.patch']
116        process_tags (bool): True to process subject tags in each patch, e.g.
117            for 'dm: spi: Add SPI support' this would be 'dm' and 'spi'. The
118            tags are looked up in the configured sendemail.aliasesfile and also
119            in ~/.patman (see README)
120        its_a_go (bool): True if we are going to actually send the patches,
121            False if the patches have errors and will not be sent unless
122            @ignore_errors
123        ignore_bad_tags (bool): True to just print a warning for unknown tags,
124            False to halt with an error
125        add_maintainers (bool): Run the get_maintainer.pl script for each patch
126        get_maintainer_script (str): The script used to retrieve which
127            maintainers to cc
128        limit (int): Limit on the number of people that can be cc'd on a single
129            patch or the cover letter (None if no limit)
130        dry_run (bool): Don't actually email the patches, just print out what
131            would be sent
132        in_reply_to (str): If not None we'll pass this to git as --in-reply-to.
133            Should be a message ID that this is in reply to.
134        thread (bool): True to add --thread to git send-email (make all patches
135            reply to cover-letter or first patch in series)
136        smtp_server (str): SMTP server to use to send patches (None for default)
137    """
138    cc_file = series.MakeCcFile(process_tags, cover_fname, not ignore_bad_tags,
139                                add_maintainers, limit, get_maintainer_script)
140
141    # Email the patches out (giving the user time to check / cancel)
142    cmd = ''
143    if its_a_go:
144        cmd = gitutil.email_patches(
145            series, cover_fname, patch_files, dry_run, not ignore_bad_tags,
146            cc_file, in_reply_to=in_reply_to, thread=thread,
147            smtp_server=smtp_server)
148    else:
149        print(col.build(col.RED, "Not sending emails due to errors/warnings"))
150
151    # For a dry run, just show our actions as a sanity check
152    if dry_run:
153        series.ShowActions(patch_files, cmd, process_tags)
154        if not its_a_go:
155            print(col.build(col.RED, "Email would not be sent"))
156
157    os.remove(cc_file)
158
159def send(args):
160    """Create, check and send patches by email
161
162    Args:
163        args (argparse.Namespace): Arguments to patman
164    """
165    setup()
166    col = terminal.Color()
167    series, cover_fname, patch_files = prepare_patches(
168        col, args.branch, args.count, args.start, args.end,
169        args.ignore_binary, args.add_signoff)
170    ok = check_patches(series, patch_files, args.check_patch,
171                       args.verbose, args.check_patch_use_tree)
172
173    ok = ok and gitutil.check_suppress_cc_config()
174
175    its_a_go = ok or args.ignore_errors
176    email_patches(
177        col, series, cover_fname, patch_files, args.process_tags,
178        its_a_go, args.ignore_bad_tags, args.add_maintainers,
179        args.get_maintainer_script, args.limit, args.dry_run,
180        args.in_reply_to, args.thread, args.smtp_server)
181
182def patchwork_status(branch, count, start, end, dest_branch, force,
183                     show_comments, url):
184    """Check the status of patches in patchwork
185
186    This finds the series in patchwork using the Series-link tag, checks for new
187    comments and review tags, displays then and creates a new branch with the
188    review tags.
189
190    Args:
191        branch (str): Branch to create patches from (None = current)
192        count (int): Number of patches to produce, or -1 to produce patches for
193            the current branch back to the upstream commit
194        start (int): Start partch to use (0=first / top of branch)
195        end (int): End patch to use (0=last one in series, 1=one before that,
196            etc.)
197        dest_branch (str): Name of new branch to create with the updated tags
198            (None to not create a branch)
199        force (bool): With dest_branch, force overwriting an existing branch
200        show_comments (bool): True to display snippets from the comments
201            provided by reviewers
202        url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org'.
203            This is ignored if the series provides a Series-patchwork-url tag.
204
205    Raises:
206        ValueError: if the branch has no Series-link value
207    """
208    if count == -1:
209        # Work out how many patches to send if we can
210        count = (gitutil.count_commits_to_branch(branch) - start)
211
212    series = patchstream.get_metadata(branch, start, count - end)
213    warnings = 0
214    for cmt in series.commits:
215        if cmt.warn:
216            print('%d warnings for %s:' % (len(cmt.warn), cmt.hash))
217            for warn in cmt.warn:
218                print('\t', warn)
219                warnings += 1
220            print
221    if warnings:
222        raise ValueError('Please fix warnings before running status')
223    links = series.get('links')
224    if not links:
225        raise ValueError("Branch has no Series-links value")
226
227    # Find the link without a version number (we don't support versions yet)
228    found = [link for link in links.split() if not ':' in link]
229    if not found:
230        raise ValueError('Series-links has no current version (without :)')
231
232    # Allow the series to override the URL
233    if 'patchwork_url' in series:
234        url = series.patchwork_url
235
236    # Import this here to avoid failing on other commands if the dependencies
237    # are not present
238    from patman import status
239    status.check_patchwork_status(series, found[0], branch, dest_branch, force,
240                                  show_comments, url)
241