~bzr-pqm-devel/bzr-pqm/devel

84 by Vincent Ladeuil
(2, 5, 0, 'beta', 5) < (2, 5, 0, 'dev', 5) so (2,5) should be used. People can only encounter the issue if they upgrade bzr to > 2.5b5 at which point they need to upgrade the plugin too.
1
# Copyright (C) 2006-2012 by Canonical Ltd
24 by John Arbash Meinel
Add GPL copyright
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
57 by James Henstridge
* Bump version number to 1.3.0 final.
13
# You should have received a copy of the GNU General Public License along
14
# with this program; if not, write to the Free Software Foundation, Inc.,
15
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2 by John Arbash Meinel
Adding basic work for a test suite.
16
"""Submit an email to a Patch Queue Manager"""
17
18.2.1 by Aaron Bentley
Better diagnostics when out-of-sync
18
from bzrlib import (
37.1.3 by James Henstridge
Try and share more configuration from merge directive code:
19
    config as _mod_config,
18.2.1 by Aaron Bentley
Better diagnostics when out-of-sync
20
    errors,
37.1.3 by James Henstridge
Try and share more configuration from merge directive code:
21
    gpg,
22
    osutils,
23
    urlutils,
79.1.1 by Jelmer Vernooij
Fix compatibility with newer versions of bzr, which use a new config method.
24
    version_info as bzrlib_version,
18.2.1 by Aaron Bentley
Better diagnostics when out-of-sync
25
    )
11 by John Arbash Meinel
[patch] Aaron Bentley: Check the public branch has the expected revision.
26
from bzrlib.branch import Branch
37.1.2 by James Henstridge
Kill the custom smtplib code, and use bzr's email+smtp infrastructure.
27
from bzrlib.email_message import EmailMessage
28
from bzrlib.smtp_connection import SMTPConnection
80.2.2 by Jelmer Vernooij
Register the bzr-pqm configuration options.
29
from bzrlib.trace import note
2 by John Arbash Meinel
Adding basic work for a test suite.
30
31
34 by Robert Collins
Fix the test_suite method to actually return the correct tests and add a test for commit messages with newlines, fixing bug #110137. (Robert Collins)
32
class BadCommitMessage(errors.BzrError):
39 by John Arbash Meinel
Update copyright and version information.
33
34 by Robert Collins
Fix the test_suite method to actually return the correct tests and add a test for commit messages with newlines, fixing bug #110137. (Robert Collins)
34
    _fmt = "The commit message %(msg)r cannot be used by pqm."
35
76 by Vincent Ladeuil
Fix bzr test failure, 'message' shouldn'y be used as an exception attribute (arguably the test should test that instead of the __init__ arguments).
36
    def __init__(self, msg):
34 by Robert Collins
Fix the test_suite method to actually return the correct tests and add a test for commit messages with newlines, fixing bug #110137. (Robert Collins)
37
        errors.BzrError.__init__(self)
76 by Vincent Ladeuil
Fix bzr test failure, 'message' shouldn'y be used as an exception attribute (arguably the test should test that instead of the __init__ arguments).
38
        self.msg = msg
34 by Robert Collins
Fix the test_suite method to actually return the correct tests and add a test for commit messages with newlines, fixing bug #110137. (Robert Collins)
39
40
54 by James Henstridge
* Use the standard NoPublicBranch exception to complain about a missing
41
class NoPQMSubmissionAddress(errors.BzrError):
42
43
    _fmt = "No PQM submission email address specified for %(branch_url)s."
44
45
    def __init__(self, branch):
63.2.1 by Martin Pool
More robust NoPQMSubmissionAddress message
46
        if (branch is None) or (branch.base is None):
47
            branch_url = '(none)'
48
        else:
49
            branch_url = urlutils.unescape_for_display(branch.base, 'ascii')
54 by James Henstridge
* Use the standard NoPublicBranch exception to complain about a missing
50
        errors.BzrError.__init__(self, branch_url=branch_url)
51
52
45 by John Arbash Meinel
Fix bug #160530, don't use proper email subject lines, as the PQM bot doesn't understand them.
53
class PQMEmailMessage(EmailMessage):
54
    """PQM doesn't support proper email subjects, so we hack around it."""
55
56
    def __init__(self, from_address, to_address, subject, body=None):
57
        EmailMessage.__init__(self, from_address=from_address,
58
                              to_address=to_address, subject=subject,
59
                              body=body)
60
        # Now override self.Subject to use raw utf-8
61
        self._headers['Subject'] = osutils.safe_unicode(subject).encode('UTF-8')
62
63
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
64
class PQMSubmission(object):
65
    """A request to perform a PQM merge into a branch."""
66
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
67
    def __init__(self, source_branch, public_location=None,
42 by John Arbash Meinel
Fix bug #141026, check that the WT is clean before submitting.
68
                 submit_location=None, message=None,
69
                 tree=None):
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
70
        """Create a PQMSubmission object.
71
72
        :param source_branch: the source branch for the merge
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
73
        :param public_location: the public location of the source branch
74
        :param submit_location: the location of the target branch
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
75
        :param message: The message to use when committing this merge
42 by John Arbash Meinel
Fix bug #141026, check that the WT is clean before submitting.
76
        :param tree: A WorkingTree or None. If not None the WT will be checked
77
            for uncommitted changes.
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
78
79
        If any of public_location, submit_location or message are
80
        omitted, they will be calculated from source_branch.
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
81
        """
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
82
        if source_branch is None and public_location is None:
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
83
            raise errors.NoMergeSource()
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
84
        self.source_branch = source_branch
42 by John Arbash Meinel
Fix bug #141026, check that the WT is clean before submitting.
85
        self.tree = tree
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
86
87
        if public_location is None:
88
            public_location = self.source_branch.get_public_branch()
89
            if public_location is None:
54 by James Henstridge
* Use the standard NoPublicBranch exception to complain about a missing
90
                raise errors.NoPublicBranch(self.source_branch)
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
91
        self.public_location = public_location
92
93
        if submit_location is None:
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
94
            if self.source_branch is None:
95
                raise errors.BzrError(
96
                    "Cannot determine submit location to use.")
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
97
            config = self.source_branch.get_config()
80.2.1 by Jelmer Vernooij
Drop support for the long deprecated `pqm_branch` and `public_repository` options.
98
            submit_location = self.source_branch.get_submit_branch()
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
99
100
            if submit_location is None:
101
                raise errors.NoSubmitBranch(self.source_branch)
46 by John Arbash Meinel
Now that we are using submit_branch instead of pqm_branch,
102
            # See if the submit_location has a public branch
103
            try:
104
                submit_branch = Branch.open(submit_location)
105
            except errors.NotBranchError:
106
                pass
107
            else:
108
                submit_public_location = submit_branch.get_public_branch()
109
                if submit_public_location is not None:
110
                    submit_location = submit_public_location
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
111
        self.submit_location = submit_location
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
112
113
        # Check that the message is okay to pass to PQM
52.1.1 by John Arbash Meinel
Require using the --message flag.
114
        assert message is not None
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
115
        self.message = message.encode('utf8')
116
        if '\n' in self.message:
117
            raise BadCommitMessage(self.message)
118
42 by John Arbash Meinel
Fix bug #141026, check that the WT is clean before submitting.
119
    def check_tree(self):
120
        """Check that the working tree has no uncommitted changes."""
121
        if self.tree is None:
122
            return
123
        note('Checking the working tree is clean ...')
124
        self.tree.lock_read()
125
        try:
126
            basis_tree = self.tree.basis_tree()
127
            basis_tree.lock_read()
128
            try:
52 by Aaron Bentley
Update to use public iter_changes
129
                for change in self.tree.iter_changes(basis_tree):
42 by John Arbash Meinel
Fix bug #141026, check that the WT is clean before submitting.
130
                    # If we have any changes, the tree is not clean
131
                    raise errors.UncommittedChanges(self.tree)
132
            finally:
133
                basis_tree.unlock()
134
        finally:
135
            self.tree.unlock()
136
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
137
    def check_public_branch(self):
138
        """Check that the public branch is up to date with the local copy."""
59 by John Arbash Meinel
Display the public location when we check it for accuracy.
139
        note('Checking that the public branch is up to date at\n    %s',
140
             urlutils.unescape_for_display(self.public_location, 'utf-8'))
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
141
        local_revision = self.source_branch.last_revision()
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
142
        public_revision = Branch.open(self.public_location).last_revision()
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
143
        if local_revision != public_revision:
144
            raise errors.PublicBranchOutOfDate(
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
145
                self.public_location, local_revision)
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
146
147
    def to_lines(self):
148
        """Serialise as a list of lines."""
37.1.5 by James Henstridge
Simplify PQMSubmission object, fixing up locations in the constructor.
149
        return ['star-merge %s %s\n' % (self.public_location, self.submit_location)]
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
150
151
    def to_signed(self):
152
        """Serialize as a signed string."""
153
        unsigned_text = ''.join(self.to_lines())
154
        unsigned_text = unsigned_text.encode('ascii') #URLs should be ascii
155
84 by Vincent Ladeuil
(2, 5, 0, 'beta', 5) < (2, 5, 0, 'dev', 5) so (2,5) should be used. People can only encounter the issue if they upgrade bzr to > 2.5b5 at which point they need to upgrade the plugin too.
156
        if bzrlib_version < (2, 5):
79.1.1 by Jelmer Vernooij
Fix compatibility with newer versions of bzr, which use a new config method.
157
            if self.source_branch:
158
                config = self.source_branch.get_config()
159
            else:
160
                config = _mod_config.GlobalConfig()
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
161
        else:
79.1.1 by Jelmer Vernooij
Fix compatibility with newer versions of bzr, which use a new config method.
162
            if self.source_branch:
163
                config = self.source_branch.get_config_stack()
164
            else:
165
                config = _mod_config.GlobalStack()
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
166
        strategy = gpg.GPGStrategy(config)
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
167
        return strategy.sign(unsigned_text)
168
169
    def to_email(self, mail_from, mail_to, sign=True):
170
        """Serialize as an email message.
171
172
        :param mail_from: The from address for the message
173
        :param mail_to: The address to send the message to
174
        :param sign: If True, gpg-sign the email
175
        :return: an email message
176
        """
177
        if sign:
178
            body = self.to_signed()
179
        else:
180
            body = ''.join(self.to_lines())
45 by John Arbash Meinel
Fix bug #160530, don't use proper email subject lines, as the PQM bot doesn't understand them.
181
        message = PQMEmailMessage(mail_from, mail_to, self.message, body)
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
182
        return message
37.1.3 by James Henstridge
Try and share more configuration from merge directive code:
183
184
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
185
class StackedConfig(_mod_config.Config):
186
187
    def __init__(self):
188
        super(StackedConfig, self).__init__()
189
        self._sources = []
190
191
    def add_source(self, source):
192
        self._sources.append(source)
193
194
    def _get_user_option(self, option_name):
195
        """See Config._get_user_option."""
196
        for source in self._sources:
197
            value = source._get_user_option(option_name)
198
            if value is not None:
199
                return value
200
        return None
201
84.1.2 by Aaron Bentley
Fix email detection
202
    def get(self, option_name):
84.1.5 by Aaron Bentley
Cleanup.
203
        """Return an option exactly as bzrlib.config.Stack would.
204
205
        Since Stack allows the environment to override 'email', this uses the
206
        same logic.
207
        """
84.1.2 by Aaron Bentley
Fix email detection
208
        if option_name == 'email':
209
            return self.username()
210
        else:
211
            return self._get_user_option(option_name)
82.1.1 by Jelmer Vernooij
Fix test suite when run against bzr 2.5.
212
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
213
    def _get_user_id(self):
214
        for source in self._sources:
215
            value = source._get_user_id()
216
            if value is not None:
217
                return value
218
        return None
219
90 by Aaron Bentley
Fix config use for bzr 2.3-2.6dev
220
    def set(self, name, value):
221
        self._sources[0].set_user_option(name, value)
222
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
223
80.1.1 by Jelmer Vernooij
Support child_pqm_email in lp-land command.
224
def pqm_email(local_config, submit_location):
225
    """Determine the PQM email address.
226
227
    :param local_config: Config object for local branch
228
    :param submit_location: Location of submit branch
229
    """
82.1.1 by Jelmer Vernooij
Fix test suite when run against bzr 2.5.
230
    mail_to = local_config.get('pqm_email')
80.1.1 by Jelmer Vernooij
Support child_pqm_email in lp-land command.
231
    if not mail_to:
232
        submit_branch = Branch.open(submit_location)
90 by Aaron Bentley
Fix config use for bzr 2.3-2.6dev
233
        submit_branch_config = get_stacked_config(submit_branch)
82.1.1 by Jelmer Vernooij
Fix test suite when run against bzr 2.5.
234
        mail_to = submit_branch_config.get('child_pqm_email')
80.1.1 by Jelmer Vernooij
Support child_pqm_email in lp-land command.
235
        if mail_to is None:
236
            return None
237
    return mail_to.encode('utf8') # same here
238
239
84.1.5 by Aaron Bentley
Cleanup.
240
def get_stacked_config(branch=None, public_location=None):
241
    """Return the relevant stacked config.
242
243
    If the branch is supplied, a branch stacked config is returned.
244
    Otherwise, if the public location is supplied, a stacked location config
245
    is returned.
246
    Otherwise, a global config is returned.
247
248
    For bzr versions earlier than 2.5, pqm_submit.StackedConfig is used.  For
249
    later versions, the standard stacked config is used.
250
251
    :param branch: The branch to retrieve the config for.
252
    :param public_location: The public location to retrieve the config for.
253
    """
84 by Vincent Ladeuil
(2, 5, 0, 'beta', 5) < (2, 5, 0, 'dev', 5) so (2,5) should be used. People can only encounter the issue if they upgrade bzr to > 2.5b5 at which point they need to upgrade the plugin too.
254
    if bzrlib_version < (2, 5):
82.1.1 by Jelmer Vernooij
Fix test suite when run against bzr 2.5.
255
        config = StackedConfig()
256
        if branch:
257
            config.add_source(branch.get_config())
258
        else:
259
            if public_location:
260
                config.add_source(_mod_config.LocationConfig(public_location))
261
            config.add_source(_mod_config.GlobalConfig())
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
262
    else:
82.1.1 by Jelmer Vernooij
Fix test suite when run against bzr 2.5.
263
        if branch:
82.1.2 by Jelmer Vernooij
Simplify config stack handling.
264
            config = branch.get_config_stack()
265
        elif public_location:
266
            config = _mod_config.LocationStack(public_location)
267
        else:
268
            config = _mod_config.GlobalStack()
84.1.2 by Aaron Bentley
Fix email detection
269
    return config
270
271
272
def get_mail_from(config):
84.1.5 by Aaron Bentley
Cleanup.
273
    """Return the email id that the email is from.
274
275
    :param config: The config to use for determining the from address.
276
    """
84.1.2 by Aaron Bentley
Fix email detection
277
    mail_from = config.get('pqm_user_email')
278
    if not mail_from:
279
        mail_from = config.get('email')
280
    mail_from = mail_from.encode('utf8') # Make sure this isn't unicode
281
    return mail_from
282
283
284
def submit(branch, message, dry_run=False, public_location=None,
285
           submit_location=None, tree=None, ignore_local=False):
286
    """Submit the given branch to the pqm."""
287
    config = get_stacked_config(branch, public_location)
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
288
    submission = PQMSubmission(
42 by John Arbash Meinel
Fix bug #141026, check that the WT is clean before submitting.
289
        source_branch=branch, public_location=public_location, message=message,
43 by John Arbash Meinel
Fix bug #110100, Add a --submit-location option.
290
        submit_location=submit_location,
42 by John Arbash Meinel
Fix bug #141026, check that the WT is clean before submitting.
291
        tree=tree)
3 by John Arbash Meinel
Everything is hooked up.
292
84.1.2 by Aaron Bentley
Fix email detection
293
    mail_from = get_mail_from(config)
80.1.1 by Jelmer Vernooij
Support child_pqm_email in lp-land command.
294
    mail_to = pqm_email(config, submit_location)
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
295
    if not mail_to:
80.1.1 by Jelmer Vernooij
Support child_pqm_email in lp-land command.
296
        raise NoPQMSubmissionAddress(branch)
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
297
61 by Andrew Bennetts
Slightly hackish way to allow submitting branches I don't have locally.
298
    if not ignore_local:
299
        submission.check_tree()
300
        submission.check_public_branch()
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
301
302
    message = submission.to_email(mail_from, mail_to)
3 by John Arbash Meinel
Everything is hooked up.
303
82.1.1 by Jelmer Vernooij
Fix test suite when run against bzr 2.5.
304
    mail_bcc = config.get('pqm_bcc')
65.2.1 by Jelmer Vernooij
Add 'pqm_bcc' option.
305
    if mail_bcc is not None:
306
        message["Bcc"] = mail_bcc
307
10 by John Arbash Meinel
[patch] Aaron Bentley: add --dry-run
308
    if dry_run:
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
309
        print message.as_string()
37.1.2 by James Henstridge
Kill the custom smtplib code, and use bzr's email+smtp infrastructure.
310
        return
311
37.1.4 by James Henstridge
* Move the PQM submission logic into a PQMSubmission object.
312
    SMTPConnection(config).send_email(message)