1
# Copyright (C) 2005-2008 by Canonical Ltd
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.
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.
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.
16
"""Submit an email to a Patch Queue Manager"""
19
config as _mod_config,
25
from bzrlib.branch import Branch
26
from bzrlib.email_message import EmailMessage
27
from bzrlib.smtp_connection import SMTPConnection
28
from bzrlib.trace import note, warning
31
class BadCommitMessage(errors.BzrError):
33
_fmt = "The commit message %(msg)r cannot be used by pqm."
35
def __init__(self, message):
36
errors.BzrError.__init__(self)
40
class NoPQMSubmissionAddress(errors.BzrError):
42
_fmt = "No PQM submission email address specified for %(branch_url)s."
44
def __init__(self, branch):
45
branch_url = urlutils.unescape_for_display(branch.base, 'ascii')
46
errors.BzrError.__init__(self, branch_url=branch_url)
49
class PQMEmailMessage(EmailMessage):
50
"""PQM doesn't support proper email subjects, so we hack around it."""
52
def __init__(self, from_address, to_address, subject, body=None):
53
EmailMessage.__init__(self, from_address=from_address,
54
to_address=to_address, subject=subject,
56
# Now override self.Subject to use raw utf-8
57
self._headers['Subject'] = osutils.safe_unicode(subject).encode('UTF-8')
60
class PQMSubmission(object):
61
"""A request to perform a PQM merge into a branch."""
63
def __init__(self, source_branch, public_location=None,
64
submit_location=None, message=None,
66
"""Create a PQMSubmission object.
68
:param source_branch: the source branch for the merge
69
:param public_location: the public location of the source branch
70
:param submit_location: the location of the target branch
71
:param message: The message to use when committing this merge
72
:param tree: A WorkingTree or None. If not None the WT will be checked
73
for uncommitted changes.
75
If any of public_location, submit_location or message are
76
omitted, they will be calculated from source_branch.
78
if source_branch is None and public_location is None:
79
raise errors.NoMergeSource()
80
self.source_branch = source_branch
83
if public_location is None:
84
public_location = self.source_branch.get_public_branch()
85
# Fall back to the old public_repository hack.
86
if public_location is None:
87
src_loc = source_branch.bzrdir.root_transport.local_abspath('.')
88
repository = source_branch.repository
89
repo_loc = repository.bzrdir.root_transport.local_abspath('.')
90
repo_config = _mod_config.LocationConfig(repo_loc)
91
public_repo = repo_config.get_user_option("public_repository")
92
if public_repo is not None:
93
warning("Please use public_branch, not public_repository, "
94
"to set the public location of branches.")
95
branch_relpath = osutils.relpath(repo_loc, src_loc)
96
public_location = urlutils.join(public_repo, branch_relpath)
98
if public_location is None:
99
raise errors.NoPublicBranch(self.source_branch)
100
self.public_location = public_location
102
if submit_location is None:
103
if self.source_branch is None:
104
raise errors.BzrError(
105
"Cannot determine submit location to use.")
106
config = self.source_branch.get_config()
107
# First check the deprecated pqm_branch config key:
108
submit_location = config.get_user_option('pqm_branch')
109
if submit_location is not None:
110
warning("Please use submit_branch, not pqm_branch to set "
111
"the PQM merge target branch.")
113
# Otherwise, use the standard config key:
114
submit_location = self.source_branch.get_submit_branch()
116
if submit_location is None:
117
raise errors.NoSubmitBranch(self.source_branch)
118
# See if the submit_location has a public branch
120
submit_branch = Branch.open(submit_location)
121
except errors.NotBranchError:
124
submit_public_location = submit_branch.get_public_branch()
125
if submit_public_location is not None:
126
submit_location = submit_public_location
127
self.submit_location = submit_location
129
# Check that the message is okay to pass to PQM
130
assert message is not None
131
self.message = message.encode('utf8')
132
if '\n' in self.message:
133
raise BadCommitMessage(self.message)
135
def check_tree(self):
136
"""Check that the working tree has no uncommitted changes."""
137
if self.tree is None:
139
note('Checking the working tree is clean ...')
140
self.tree.lock_read()
142
basis_tree = self.tree.basis_tree()
143
basis_tree.lock_read()
145
for change in self.tree.iter_changes(basis_tree):
146
# If we have any changes, the tree is not clean
147
raise errors.UncommittedChanges(self.tree)
153
def check_public_branch(self):
154
"""Check that the public branch is up to date with the local copy."""
155
note('Checking that the public branch is up to date at\n %s',
156
urlutils.unescape_for_display(self.public_location, 'utf-8'))
157
local_revision = self.source_branch.last_revision()
158
public_revision = Branch.open(self.public_location).last_revision()
159
if local_revision != public_revision:
160
raise errors.PublicBranchOutOfDate(
161
self.public_location, local_revision)
164
"""Serialise as a list of lines."""
165
return ['star-merge %s %s\n' % (self.public_location, self.submit_location)]
168
"""Serialize as a signed string."""
169
unsigned_text = ''.join(self.to_lines())
170
unsigned_text = unsigned_text.encode('ascii') #URLs should be ascii
172
if self.source_branch:
173
config = self.source_branch.get_config()
175
config = _mod_config.GlobalConfig()
176
strategy = gpg.GPGStrategy(config)
177
return strategy.sign(unsigned_text)
179
def to_email(self, mail_from, mail_to, sign=True):
180
"""Serialize as an email message.
182
:param mail_from: The from address for the message
183
:param mail_to: The address to send the message to
184
:param sign: If True, gpg-sign the email
185
:return: an email message
188
body = self.to_signed()
190
body = ''.join(self.to_lines())
191
message = PQMEmailMessage(mail_from, mail_to, self.message, body)
195
class StackedConfig(_mod_config.Config):
198
super(StackedConfig, self).__init__()
201
def add_source(self, source):
202
self._sources.append(source)
204
def _get_user_option(self, option_name):
205
"""See Config._get_user_option."""
206
for source in self._sources:
207
value = source._get_user_option(option_name)
208
if value is not None:
212
def _get_user_id(self):
213
for source in self._sources:
214
value = source._get_user_id()
215
if value is not None:
220
def submit(branch, message, dry_run=False, public_location=None,
221
submit_location=None, tree=None, ignore_local=False):
222
"""Submit the given branch to the pqm."""
223
config = StackedConfig()
225
config.add_source(branch.get_config())
228
config.add_source(_mod_config.LocationConfig(public_location))
229
config.add_source(_mod_config.GlobalConfig())
231
submission = PQMSubmission(
232
source_branch=branch, public_location=public_location, message=message,
233
submit_location=submit_location,
236
mail_from = config.get_user_option('pqm_user_email')
238
mail_from = config.username()
239
mail_from = mail_from.encode('utf8') # Make sure this isn't unicode
240
mail_to = config.get_user_option('pqm_email')
242
raise NoPQMSubmissionAddress(branch)
243
mail_to = mail_to.encode('utf8') # same here
246
submission.check_tree()
247
submission.check_public_branch()
249
message = submission.to_email(mail_from, mail_to)
252
print message.as_string()
255
SMTPConnection(config).send_email(message)