summaryrefslogtreecommitdiff
path: root/tools/patman/send.py
blob: 08a916aff1ada2cbe413b9128ffd65335776d08e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# SPDX-License-Identifier: GPL-2.0+
#
# Copyright 2025 Google LLC
#
"""Handles the 'send' subcommand
"""

import os
import sys

from patman import checkpatch
from patman import patchstream
from patman import settings
from u_boot_pylib import gitutil
from u_boot_pylib import terminal


def check_patches(series, patch_files, run_checkpatch, verbose, use_tree, cwd):
    """Run some checks on a set of patches

    This santiy-checks the patman tags like Series-version and runs the patches
    through checkpatch

    Args:
        series (Series): Series object for this series (set of patches)
        patch_files (list): List of patch filenames, each a string, e.g.
            ['0001_xxx.patch', '0002_yyy.patch']
        run_checkpatch (bool): True to run checkpatch.pl
        verbose (bool): True to print out every line of the checkpatch output as
            it is parsed
        use_tree (bool): If False we'll pass '--no-tree' to checkpatch.
        cwd (str): Path to use for patch files (None to use current dir)

    Returns:
        bool: True if the patches had no errors, False if they did
    """
    # Do a few checks on the series
    series.DoChecks()

    # Check the patches
    if run_checkpatch:
        ok = checkpatch.check_patches(verbose, patch_files, use_tree, cwd)
    else:
        ok = True
    return ok


def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
                  ignore_bad_tags, add_maintainers, get_maintainer_script, limit,
                  dry_run, in_reply_to, thread, smtp_server, cwd=None):
    """Email patches to the recipients

    This emails out the patches and cover letter using 'git send-email'. Each
    patch is copied to recipients identified by the patch tag and output from
    the get_maintainer.pl script. The cover letter is copied to all recipients
    of any patch.

    To make this work a CC file is created holding the recipients for each patch
    and the cover letter. See the main program 'cc_cmd' for this logic.

    Args:
        col (terminal.Color): Colour output object
        series (Series): Series object for this series (set of patches)
        cover_fname (str): Filename of the cover letter as a string (None if
            none)
        patch_files (list): List of patch filenames, each a string, e.g.
            ['0001_xxx.patch', '0002_yyy.patch']
        process_tags (bool): True to process subject tags in each patch, e.g.
            for 'dm: spi: Add SPI support' this would be 'dm' and 'spi'. The
            tags are looked up in the configured sendemail.aliasesfile and also
            in ~/.patman (see README)
        its_a_go (bool): True if we are going to actually send the patches,
            False if the patches have errors and will not be sent unless
            @ignore_errors
        ignore_bad_tags (bool): True to just print a warning for unknown tags,
            False to halt with an error
        add_maintainers (bool): Run the get_maintainer.pl script for each patch
        get_maintainer_script (str): The script used to retrieve which
            maintainers to cc
        limit (int): Limit on the number of people that can be cc'd on a single
            patch or the cover letter (None if no limit)
        dry_run (bool): Don't actually email the patches, just print out what
            would be sent
        in_reply_to (str): If not None we'll pass this to git as --in-reply-to.
            Should be a message ID that this is in reply to.
        thread (bool): True to add --thread to git send-email (make all patches
            reply to cover-letter or first patch in series)
        smtp_server (str): SMTP server to use to send patches (None for default)
        cwd (str): Path to use for patch files (None to use current dir)

    Return:
        Git command that was/would be run
    """
    cc_file = series.MakeCcFile(process_tags, cover_fname, not ignore_bad_tags,
                                add_maintainers, limit, get_maintainer_script,
                                settings.alias, cwd)

    # Email the patches out (giving the user time to check / cancel)
    cmd = ''
    if its_a_go:
        cmd = gitutil.email_patches(
            series, cover_fname, patch_files, dry_run, not ignore_bad_tags,
            cc_file, alias=settings.alias, in_reply_to=in_reply_to,
            thread=thread, smtp_server=smtp_server, cwd=cwd)
    else:
        print(col.build(col.RED, "Not sending emails due to errors/warnings"))

    # For a dry run, just show our actions as a sanity check
    if dry_run:
        series.ShowActions(patch_files, cmd, process_tags, settings.alias)
        if not its_a_go:
            print(col.build(col.RED, "Email would not be sent"))

    os.remove(cc_file)
    return cmd


def prepare_patches(col, branch, count, start, end, ignore_binary, signoff,
                    keep_change_id=False, git_dir=None, cwd=None):
    """Figure out what patches to generate, then generate them

    The patch files are written to the current directory, e.g. 0001_xxx.patch
    0002_yyy.patch

    Args:
        col (terminal.Color): Colour output object
        branch (str): Branch to create patches from (None = current)
        count (int): Number of patches to produce, or -1 to produce patches for
            the current branch back to the upstream commit
        start (int): Start patch to use (0=first / top of branch)
        end (int): End patch to use (0=last one in series, 1=one before that,
            etc.)
        ignore_binary (bool): Don't generate patches for binary files
        keep_change_id (bool): Preserve the Change-Id tag.
        git_dir (str): Path to git repository (None to use default)
        cwd (str): Path to use for git operations (None to use current dir)

    Returns:
        Tuple:
            Series object for this series (set of patches)
            Filename of the cover letter as a string (None if none)
            patch_files: List of patch filenames, each a string, e.g.
                ['0001_xxx.patch', '0002_yyy.patch']
    """
    if count == -1:
        # Work out how many patches to send if we can
        count = (gitutil.count_commits_to_branch(branch, git_dir=git_dir) -
                 start)

    if not count:
        msg = 'No commits found to process - please use -c flag, or run:\n' \
              '  git branch --set-upstream-to remote/branch'
        sys.exit(col.build(col.RED, msg))

    # Read the metadata from the commits
    to_do = count - end
    series = patchstream.get_metadata(branch, start, to_do, git_dir)
    cover_fname, patch_files = gitutil.create_patches(
        branch, start, to_do, ignore_binary, series, signoff, git_dir=git_dir,
        cwd=cwd)

    # Fix up the patch files to our liking, and insert the cover letter
    patchstream.fix_patches(series, patch_files, keep_change_id,
                            insert_base_commit=not cover_fname, cwd=cwd)
    if cover_fname and series.get('cover'):
        patchstream.insert_cover_letter(cover_fname, series, to_do, cwd=cwd)
    return series, cover_fname, patch_files


def send(args, git_dir=None, cwd=None):
    """Create, check and send patches by email

    Args:
        args (argparse.Namespace): Arguments to patman
        cwd (str): Path to use for git operations

    Return:
        bool: True if the patches were likely sent, else False
    """
    col = terminal.Color()
    series, cover_fname, patch_files = prepare_patches(
        col, args.branch, args.count, args.start, args.end,
        args.ignore_binary, args.add_signoff,
        keep_change_id=args.keep_change_id, git_dir=git_dir, cwd=cwd)
    ok = check_patches(series, patch_files, args.check_patch,
                       args.verbose, args.check_patch_use_tree, cwd)

    ok = ok and gitutil.check_suppress_cc_config()

    its_a_go = ok or args.ignore_errors
    cmd = email_patches(
        col, series, cover_fname, patch_files, args.process_tags,
        its_a_go, args.ignore_bad_tags, args.add_maintainers,
        args.get_maintainer_script, args.limit, args.dry_run,
        args.in_reply_to, args.thread, args.smtp_server, cwd=cwd)

    return cmd and its_a_go and not args.dry_run