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 patman 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.CountCommitsToBranch(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.Color(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.CreatePatches(
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):
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
81    Returns:
82        bool: True if the patches had no errors, False if they did
83    """
84    # Do a few checks on the series
85    series.DoChecks()
86
87    # Check the patches, and run them through 'git am' just to be sure
88    if run_checkpatch:
89        ok = checkpatch.CheckPatches(verbose, patch_files)
90    else:
91        ok = True
92    return ok
93
94
95def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
96                  ignore_bad_tags, add_maintainers, limit, dry_run, in_reply_to,
97                  thread, smtp_server):
98    """Email patches to the recipients
99
100    This emails out the patches and cover letter using 'git send-email'. Each
101    patch is copied to recipients identified by the patch tag and output from
102    the get_maintainer.pl script. The cover letter is copied to all recipients
103    of any patch.
104
105    To make this work a CC file is created holding the recipients for each patch
106    and the cover letter. See the main program 'cc_cmd' for this logic.
107
108    Args:
109        col (terminal.Color): Colour output object
110        series (Series): Series object for this series (set of patches)
111        cover_fname (str): Filename of the cover letter as a string (None if
112            none)
113        patch_files (list): List of patch filenames, each a string, e.g.
114            ['0001_xxx.patch', '0002_yyy.patch']
115        process_tags (bool): True to process subject tags in each patch, e.g.
116            for 'dm: spi: Add SPI support' this would be 'dm' and 'spi'. The
117            tags are looked up in the configured sendemail.aliasesfile and also
118            in ~/.patman (see README)
119        its_a_go (bool): True if we are going to actually send the patches,
120            False if the patches have errors and will not be sent unless
121            @ignore_errors
122        ignore_bad_tags (bool): True to just print a warning for unknown tags,
123            False to halt with an error
124        add_maintainers (bool): Run the get_maintainer.pl script for each patch
125        limit (int): Limit on the number of people that can be cc'd on a single
126            patch or the cover letter (None if no limit)
127        dry_run (bool): Don't actually email the patches, just print out what
128            would be sent
129        in_reply_to (str): If not None we'll pass this to git as --in-reply-to.
130            Should be a message ID that this is in reply to.
131        thread (bool): True to add --thread to git send-email (make all patches
132            reply to cover-letter or first patch in series)
133        smtp_server (str): SMTP server to use to send patches (None for default)
134    """
135    cc_file = series.MakeCcFile(process_tags, cover_fname, not ignore_bad_tags,
136                                add_maintainers, limit)
137
138    # Email the patches out (giving the user time to check / cancel)
139    cmd = ''
140    if its_a_go:
141        cmd = gitutil.EmailPatches(
142            series, cover_fname, patch_files, dry_run, not ignore_bad_tags,
143            cc_file, in_reply_to=in_reply_to, thread=thread,
144            smtp_server=smtp_server)
145    else:
146        print(col.Color(col.RED, "Not sending emails due to errors/warnings"))
147
148    # For a dry run, just show our actions as a sanity check
149    if dry_run:
150        series.ShowActions(patch_files, cmd, process_tags)
151        if not its_a_go:
152            print(col.Color(col.RED, "Email would not be sent"))
153
154    os.remove(cc_file)
155
156def send(args):
157    """Create, check and send patches by email
158
159    Args:
160        args (argparse.Namespace): Arguments to patman
161    """
162    setup()
163    col = terminal.Color()
164    series, cover_fname, patch_files = prepare_patches(
165        col, args.branch, args.count, args.start, args.end,
166        args.ignore_binary, args.add_signoff)
167    ok = check_patches(series, patch_files, args.check_patch,
168                       args.verbose)
169
170    ok = ok and gitutil.CheckSuppressCCConfig()
171
172    its_a_go = ok or args.ignore_errors
173    email_patches(
174        col, series, cover_fname, patch_files, args.process_tags,
175        its_a_go, args.ignore_bad_tags, args.add_maintainers,
176        args.limit, args.dry_run, args.in_reply_to, args.thread,
177        args.smtp_server)
178
179def patchwork_status(branch, count, start, end, dest_branch, force,
180                     show_comments, url):
181    """Check the status of patches in patchwork
182
183    This finds the series in patchwork using the Series-link tag, checks for new
184    comments and review tags, displays then and creates a new branch with the
185    review tags.
186
187    Args:
188        branch (str): Branch to create patches from (None = current)
189        count (int): Number of patches to produce, or -1 to produce patches for
190            the current branch back to the upstream commit
191        start (int): Start partch to use (0=first / top of branch)
192        end (int): End patch to use (0=last one in series, 1=one before that,
193            etc.)
194        dest_branch (str): Name of new branch to create with the updated tags
195            (None to not create a branch)
196        force (bool): With dest_branch, force overwriting an existing branch
197        show_comments (bool): True to display snippets from the comments
198            provided by reviewers
199        url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org'.
200            This is ignored if the series provides a Series-patchwork-url tag.
201
202    Raises:
203        ValueError: if the branch has no Series-link value
204    """
205    if count == -1:
206        # Work out how many patches to send if we can
207        count = (gitutil.CountCommitsToBranch(branch) - start)
208
209    series = patchstream.get_metadata(branch, start, count - end)
210    warnings = 0
211    for cmt in series.commits:
212        if cmt.warn:
213            print('%d warnings for %s:' % (len(cmt.warn), cmt.hash))
214            for warn in cmt.warn:
215                print('\t', warn)
216                warnings += 1
217            print
218    if warnings:
219        raise ValueError('Please fix warnings before running status')
220    links = series.get('links')
221    if not links:
222        raise ValueError("Branch has no Series-links value")
223
224    # Find the link without a version number (we don't support versions yet)
225    found = [link for link in links.split() if not ':' in link]
226    if not found:
227        raise ValueError('Series-links has no current version (without :)')
228
229    # Allow the series to override the URL
230    if 'patchwork_url' in series:
231        url = series.patchwork_url
232
233    # Import this here to avoid failing on other commands if the dependencies
234    # are not present
235    from patman import status
236    status.check_patchwork_status(series, found[0], branch, dest_branch, force,
237                                  show_comments, url)
238