diff -Nru bzr-pqm-1.4.0~bzr83/cmds.py bzr-pqm-1.4.0~bzr90/cmds.py --- bzr-pqm-1.4.0~bzr83/cmds.py 1970-01-01 00:00:00.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/cmds.py 2012-03-01 21:54:15.000000000 +0000 @@ -0,0 +1,164 @@ +# Copyright (C) 2006-2012 by Canonical Ltd +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""Functionality for controlling a Patch Queue Manager (pqm). +""" + +from __future__ import absolute_import + +from bzrlib.commands import Command +from bzrlib.option import Option +from bzrlib.errors import BzrCommandError + + +class cmd_pqm_submit(Command): + """Submit the parent tree to the pqm. + + This acts like: + $ echo "star-merge $PARENT $TARGET" + | gpg --cl + | mail pqm@somewhere -s "merge text" + + But it pays attention to who the local committer is + (using their e-mail address), and uses the local + gpg signing configuration. (As well as target pqm + settings, etc.) + + The reason we use 'parent' instead of the local branch + is that most likely the local branch is not a public + branch. And the branch must be available to the pqm. + + This can be configured at the branch level using ~/.bazaar/locations.conf. + Here is an example: + [/home/emurphy/repo] + pqm_email = PQM + pqm_user_email = User Name + submit_branch = http://code.example.com/code/project/devel + # Set public_branch appropriately for all branches in repository: + public_branch = http://code.example.com/code/emurphy/project + public_branch:policy = appendpath + [/home/emurphy/repo/branch] + # Override public_branch for this repository: + public_branch = http://alternate.host.example.com/other/public/branch + + smtp_server = host:port + smtp_username = + smtp_password = + + If you don't specify the smtp server, the message will be sent via localhost. + """ + + takes_args = ['location?'] + takes_options = [ + Option('message', + help='Message to use on merge to pqm. ' + 'Currently must be a single line because of pqm limits.', + short_name='m', + type=unicode), + Option('dry-run', help='Print request instead of sending.'), + Option('public-location', type=str, + help='Use this url as the public location to the pqm.'), + Option('submit-branch', type=str, + help='Use this url as the target submission branch.'), + Option('ignore-local', help='Do not check the local branch or tree.'), + ] + + def run(self, location=None, message=None, public_location=None, + dry_run=False, submit_branch=None, ignore_local=False): + from bzrlib import bzrdir + from bzrlib.plugins.pqm.pqm_submit import submit + + if ignore_local: + tree, b, relpath = None, None, None + else: + if location is None: + location = '.' + tree, b, relpath = bzrdir.BzrDir.open_containing_tree_or_branch( + location) + if b is not None: + b.lock_read() + self.add_cleanup(b.unlock) + if relpath and not tree and location != '.': + raise BzrCommandError( + 'No working tree was found, but we were not given the ' + 'exact path to the branch.\n' + 'We found a branch at: %s' % (b.base,)) + if message is None: + raise BzrCommandError( + 'You must supply a commit message for the pqm to use.') + submit(b, message=message, dry_run=dry_run, + public_location=public_location, + submit_location=submit_branch, + tree=tree, ignore_local=ignore_local) + +class cmd_lp_land(Command): + """Land the merge proposal for this branch via PQM. + + The branch will be submitted to PQM according to the merge proposal. If + there is more than one one outstanding proposal for the branch, its + location must be specified. + """ + + takes_args = ['location?'] + + takes_options = [ + Option('dry-run', help='Display the PQM message instead of sending.'), + Option( + 'testfix', + help="This is a testfix (tags commit with [testfix])."), + Option( + 'no-qa', + help="Does not require QA (tags commit with [no-qa])."), + Option( + 'incremental', + help="Incremental to other bug fix (tags commit with [incr])."), + Option( + 'rollback', type=int, + help=( + "Rollback given revision number. (tags commit with " + "[rollback=revno]).")), + ] + + def run(self, location=None, dry_run=False, testfix=False, + no_qa=False, incremental=False, rollback=None): + from bzrlib.plugins.pqm.lpland import Submitter + from bzrlib.plugins.pqm.lpland import ( + MissingReviewError, MissingBugsError, MissingBugsIncrementalError) + + if dry_run: + outf = self.outf + else: + outf = None + if rollback and (no_qa or incremental): + print "--rollback option used. Ignoring --no-qa and --incremental." + try: + submitter = Submitter.from_cmdline( + location, testfix, no_qa, incremental, + rollback=rollback).run(outf) + except MissingReviewError: + raise BzrCommandError( + "Cannot land branches that haven't got approved code " + "reviews. Get an 'Approved' vote so we can fill in the " + "[r=REVIEWER] section.") + except MissingBugsError: + raise BzrCommandError( + "Branch doesn't have linked bugs and doesn't have no-qa " + "option set. Use --no-qa, or link the related bugs to the " + "branch.") + except MissingBugsIncrementalError: + raise BzrCommandError( + "--incremental option requires bugs linked to the branch. " + "Link the bugs or remove the --incremental option.") diff -Nru bzr-pqm-1.4.0~bzr83/config.py bzr-pqm-1.4.0~bzr90/config.py --- bzr-pqm-1.4.0~bzr83/config.py 1970-01-01 00:00:00.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/config.py 2011-12-19 13:29:41.000000000 +0000 @@ -0,0 +1,45 @@ +# Copyright (C) 2011 by Canonical Ltd +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Functionality for controlling a Patch Queue Manager (pqm). +""" + +from bzrlib.config import Option + +pqm_user_email = Option('pqm_user_email', + help='''\ +From address to use for PQM submissions. + +This defaults to the name and email address in the ``email`` setting. +''') +child_pqm_email = Option('child_pqm_email', + help='''\ +E-mail address for the PQM bot, for submissions of this branch. + +See also the ``pqm_email`` setting. +''') +pqm_email = Option('pqm_email', + help='''\ +E-mail address of the PQM bot, for submissions from this branch. + +This setting takes precedence over the ``child_pqm_email`` setting +in the submit branch. + +See also the ``child_pqm_email` setting. +''') +pqm_bcc = Option('pqm_bcc', + help='''\ +List of addresses to BCC on PQM requests. +''') diff -Nru bzr-pqm-1.4.0~bzr83/debian/changelog bzr-pqm-1.4.0~bzr90/debian/changelog --- bzr-pqm-1.4.0~bzr83/debian/changelog 2012-01-20 14:51:51.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/debian/changelog 2012-04-01 22:11:05.000000000 +0000 @@ -1,3 +1,19 @@ +bzr-pqm (1.4.0~bzr90-1) unstable; urgency=low + + * New upstream snapshot. + + Fixes testsuite when used with bzr 2.6. Closes: #666605 + + -- Jelmer Vernooij Sun, 01 Apr 2012 23:09:08 +0200 + +bzr-pqm (1.4.0~bzr87-1) unstable; urgency=low + + * Use machine-parseable copyright file format. + * Add tests for autopkgtest. + * Bump standards version to 3.9.3 (no changes). + * New upstream snapshot. + + -- Jelmer Vernooij Tue, 28 Feb 2012 16:16:50 +0100 + bzr-pqm (1.4.0~bzr83-1) unstable; urgency=low * New upstream snapshot. diff -Nru bzr-pqm-1.4.0~bzr83/debian/control bzr-pqm-1.4.0~bzr90/debian/control --- bzr-pqm-1.4.0~bzr83/debian/control 2012-01-20 14:51:02.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/debian/control 2012-04-01 22:11:15.000000000 +0000 @@ -5,12 +5,12 @@ Uploaders: Etienne Goyer , James Henstridge , Jelmer Vernooij -Build-Depends: python-bzrlib.tests | bzr (<< 2.4.0~beta1-2), +Build-Depends: python-bzrlib.tests, bzr (>= 2.0), debhelper (>= 7.0.50~), python (>= 2.6.6-3) Build-Depends-Indep: python-launchpadlib (>= 1.5.4), python-testtools -Standards-Version: 3.9.2 +Standards-Version: 3.9.3 X-Python-Version: >= 2.4 DM-Upload-Allowed: yes Vcs-Bzr: http://bzr.debian.org/pkg-bazaar/bzr-pqm/unstable diff -Nru bzr-pqm-1.4.0~bzr83/debian/copyright bzr-pqm-1.4.0~bzr90/debian/copyright --- bzr-pqm-1.4.0~bzr83/debian/copyright 2012-01-20 14:51:02.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/debian/copyright 2012-02-28 15:14:37.000000000 +0000 @@ -1,24 +1,48 @@ -This package was debianized by Etienne Goyer on -Wed, 18 Oct 2006 19:50:35 -0400 - -It was downloaded from https://launchpad.net/bzr-pqm - -Copyright (C) 2005, 2006, 2007 by Canonical Ltd - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License with -the Debian GNU/Linux distribution in file /usr/share/common-licenses/GPL; -if not, write to the Free Software Foundation, Inc., 51 Franklin St, -Fifth Floor, Boston, MA 02110-1301, USA. - -On Debian systems, the complete text of the GNU General Public -License can be found in /usr/share/common-licenses/GPL file. +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: bzr-pqm +Upstream-Contact: Bazaar Developers +Source: https://launchpad.net/bzr-pqm + +Files: debian/* +Copyright: 2006-2012 Canonical Ltd +Comment: This package was debianized by Etienne Goyer + on Wed, 18 Oct 2006 19:50:35 -0400 +License: GPL-2+ + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License with + the Debian GNU/Linux distribution in file /usr/share/common-licenses/GPL; + if not, write to the Free Software Foundation, Inc., 51 Franklin St, + Fifth Floor, Boston, MA 02110-1301, USA. + . + On Debian systems, the complete text of the GNU General Public + License can be found in /usr/share/common-licenses/GPL file. + +Files: * +Copyright: 2005, 2006, 2007 by Canonical Ltd +License: GPL-2+ + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License with + the Debian GNU/Linux distribution in file /usr/share/common-licenses/GPL; + if not, write to the Free Software Foundation, Inc., 51 Franklin St, + Fifth Floor, Boston, MA 02110-1301, USA. + . + On Debian systems, the complete text of the GNU General Public + License can be found in /usr/share/common-licenses/GPL file. diff -Nru bzr-pqm-1.4.0~bzr83/debian/tests/control bzr-pqm-1.4.0~bzr90/debian/tests/control --- bzr-pqm-1.4.0~bzr83/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/debian/tests/control 2012-02-28 12:17:10.000000000 +0000 @@ -0,0 +1,3 @@ +Tests: testsuite +Depends: bzr, python-bzrlib.tests +Features: no-build-needed diff -Nru bzr-pqm-1.4.0~bzr83/debian/tests/testsuite bzr-pqm-1.4.0~bzr90/debian/tests/testsuite --- bzr-pqm-1.4.0~bzr83/debian/tests/testsuite 1970-01-01 00:00:00.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/debian/tests/testsuite 2012-02-28 15:15:53.000000000 +0000 @@ -0,0 +1,4 @@ +#!/bin/sh +# Note that since the installed version of the package has to be tested, +# there is no mucking about with BZR_PLUGINS_AT. +bzr selftest -s bp.pqm -v --parallel=fork diff -Nru bzr-pqm-1.4.0~bzr83/__init__.py bzr-pqm-1.4.0~bzr90/__init__.py --- bzr-pqm-1.4.0~bzr83/__init__.py 2011-12-19 11:04:03.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/__init__.py 2012-03-01 21:54:15.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2010 by Canonical Ltd +# Copyright (C) 2006-2012 by Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,9 +19,7 @@ from __future__ import absolute_import from bzrlib import version_info as bzrlib_version -from bzrlib.commands import Command, register_command -from bzrlib.option import Option -from bzrlib.errors import BzrCommandError +from bzrlib.commands import plugin_cmds version_info = (1, 4, 0, 'dev', 0) @@ -33,155 +31,19 @@ __version__ = version_string -class cmd_pqm_submit(Command): - """Submit the parent tree to the pqm. +plugin_cmds.register_lazy("cmd_pqm_submit", [], 'bzrlib.plugins.pqm.cmds') +plugin_cmds.register_lazy("cmd_lp_land", [], 'bzrlib.plugins.pqm.cmds') - This acts like: - $ echo "star-merge $PARENT $TARGET" - | gpg --cl - | mail pqm@somewhere -s "merge text" - - But it pays attention to who the local committer is - (using their e-mail address), and uses the local - gpg signing configuration. (As well as target pqm - settings, etc.) - - The reason we use 'parent' instead of the local branch - is that most likely the local branch is not a public - branch. And the branch must be available to the pqm. - - This can be configured at the branch level using ~/.bazaar/locations.conf. - Here is an example: - [/home/emurphy/repo] - pqm_email = PQM - pqm_user_email = User Name - submit_branch = http://code.example.com/code/project/devel - # Set public_branch appropriately for all branches in repository: - public_branch = http://code.example.com/code/emurphy/project - public_branch:policy = appendpath - [/home/emurphy/repo/branch] - # Override public_branch for this repository: - public_branch = http://alternate.host.example.com/other/public/branch - - smtp_server = host:port - smtp_username = - smtp_password = - - If you don't specify the smtp server, the message will be sent via localhost. - """ - - takes_args = ['location?'] - takes_options = [ - Option('message', - help='Message to use on merge to pqm. ' - 'Currently must be a single line because of pqm limits.', - short_name='m', - type=unicode), - Option('dry-run', help='Print request instead of sending.'), - Option('public-location', type=str, - help='Use this url as the public location to the pqm.'), - Option('submit-branch', type=str, - help='Use this url as the target submission branch.'), - Option('ignore-local', help='Do not check the local branch or tree.'), - ] - - def run(self, location=None, message=None, public_location=None, - dry_run=False, submit_branch=None, ignore_local=False): - from bzrlib import trace, bzrdir - if __name__ != 'bzrlib.plugins.pqm': - trace.warning('The bzr-pqm plugin needs to be called' - ' "bzrlib.plugins.pqm" not "%s"\n' - 'Please rename the plugin.', - __name__) - return 1 - from bzrlib.plugins.pqm.pqm_submit import submit - - if ignore_local: - tree, b, relpath = None, None, None - else: - if location is None: - location = '.' - tree, b, relpath = bzrdir.BzrDir.open_containing_tree_or_branch( - location) - if b is not None: - b.lock_read() - self.add_cleanup(b.unlock) - if relpath and not tree and location != '.': - raise BzrCommandError( - 'No working tree was found, but we were not given the ' - 'exact path to the branch.\n' - 'We found a branch at: %s' % (b.base,)) - if message is None: - raise BzrCommandError( - 'You must supply a commit message for the pqm to use.') - submit(b, message=message, dry_run=dry_run, - public_location=public_location, - submit_location=submit_branch, - tree=tree, ignore_local=ignore_local) - -class cmd_lp_land(Command): - """Land the merge proposal for this branch via PQM. - - The branch will be submitted to PQM according to the merge proposal. If - there is more than one one outstanding proposal for the branch, its - location must be specified. - """ - - takes_args = ['location?'] - - takes_options = [ - Option('dry-run', help='Display the PQM message instead of sending.'), - Option( - 'testfix', - help="This is a testfix (tags commit with [testfix])."), - Option( - 'no-qa', - help="Does not require QA (tags commit with [no-qa])."), - Option( - 'incremental', - help="Incremental to other bug fix (tags commit with [incr])."), - Option( - 'rollback', type=int, - help=( - "Rollback given revision number. (tags commit with " - "[rollback=revno]).")), - ] - - def run(self, location=None, dry_run=False, testfix=False, - no_qa=False, incremental=False, rollback=None): - from bzrlib.plugins.pqm.lpland import Submitter - from bzrlib import branch as _mod_branch - from bzrlib.plugins.pqm.lpland import ( - MissingReviewError, MissingBugsError, MissingBugsIncrementalError) - - branch = _mod_branch.Branch.open_containing('.')[0] - if dry_run: - outf = self.outf - else: - outf = None - if rollback and (no_qa or incremental): - print "--rollback option used. Ignoring --no-qa and --incremental." - try: - submitter = Submitter(branch, location, testfix, no_qa, - incremental, rollback=rollback).run(outf) - except MissingReviewError: - raise BzrCommandError( - "Cannot land branches that haven't got approved code " - "reviews. Get an 'Approved' vote so we can fill in the " - "[r=REVIEWER] section.") - except MissingBugsError: - raise BzrCommandError( - "Branch doesn't have linked bugs and doesn't have no-qa " - "option set. Use --no-qa, or link the related bugs to the " - "branch.") - except MissingBugsIncrementalError: - raise BzrCommandError( - "--incremental option requires bugs linked to the branch. " - "Link the bugs or remove the --incremental option.") - - -register_command(cmd_pqm_submit) -register_command(cmd_lp_land) +if bzrlib_version >= (2, 5): + from bzrlib import config as _mod_config + _mod_config.option_registry.register_lazy('pqm_user_email', + 'bzrlib.plugins.pqm.config', 'pqm_user_email') + _mod_config.option_registry.register_lazy('child_pqm_email', + 'bzrlib.plugins.pqm.config', 'child_pqm_email') + _mod_config.option_registry.register_lazy('pqm_email', + 'bzrlib.plugins.pqm.config', 'pqm_email') + _mod_config.option_registry.register_lazy('pqm_bcc', + 'bzrlib.plugins.pqm.config', 'pqm_bcc') def test_suite(): diff -Nru bzr-pqm-1.4.0~bzr83/lpland.py bzr-pqm-1.4.0~bzr90/lpland.py --- bzr-pqm-1.4.0~bzr83/lpland.py 2011-12-16 20:21:06.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/lpland.py 2012-02-28 16:57:20.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (C) 2010 by Canonical Ltd +# Copyright (C) 2010-2012 by Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -91,16 +91,19 @@ """Get the merge proposal from the branch.""" lp_branch = self.get_lp_branch(branch) - proposals = lp_branch.landing_targets + proposals = [ + mp for mp in lp_branch.landing_targets + if mp.queue_status in ('Needs review', 'Approved')] if len(proposals) == 0: raise BzrCommandError( - "The public branch has no source merge proposals. " + "The public branch has no active source merge proposals. " "You must have a merge proposal before attempting to " "land the branch.") elif len(proposals) > 1: raise BzrCommandError( - "The public branch has multiple source merge proposals. " - "You must provide the URL to the one you wish to use.") + "The public branch has multiple active source merge " + "proposals. You must provide the URL to the one you wish to" + " use.") return MergeProposal(proposals[0]) @@ -143,9 +146,11 @@ :return: A set of `IPerson`s. """ # XXX: JonathanLange 2009-09-24: No unit tests. - return set( + emails = set( map(get_email, [self._mp.source_branch.owner, self._launchpad.me])) + emails.discard(None) + return emails def get_reviews(self): """Return a dictionary of all Approved reviews. @@ -163,6 +168,8 @@ continue reviewers = reviews.setdefault(vote.review_type, []) reviewers.append(vote.reviewer) + if self.is_approved and not reviews: + reviews[None] = [self._mp.reviewer] return reviews def get_bugs(self): @@ -196,41 +203,58 @@ return '%s %s' % (tags, commit_text) - class Submitter(object): + """Class that submits a to PQM from a merge proposal.""" - def __init__(self, branch, location, testfix=False, no_qa=False, + def __init__(self, branch, mp, testfix=False, no_qa=False, incremental=False, rollback=None): self.branch = branch + self.mp = mp self.testfix = testfix self.no_qa = no_qa self.incremental = incremental self.rollback = rollback - self.config = self.branch.get_config() - self.mail_from = self.config.get_user_option('pqm_user_email') - if not self.mail_from: - self.mail_from = self.config.username() - self.lander = LaunchpadBranchLander.load() - self.location = location + self.config = pqm_submit.get_stacked_config(self.branch, None) + self.mail_from = pqm_submit.get_mail_from(self.config) + + @classmethod + def from_cmdline(cls, location, testfix, no_qa, incremental, rollback): + """Factory that returns a Submitter from commandline arguments.""" + from bzrlib import branch as _mod_branch + branch = _mod_branch.Branch.open_containing('.')[0] + lander = LaunchpadBranchLander.load() + if location is None: + mp = lander.get_merge_proposal_from_branch(branch) + else: + mp = lander.load_merge_proposal(location) + return cls(branch, mp, testfix, no_qa, incremental, rollback) - def submission(self, mp): + def submission(self): + """Create a PQMSubmission for this Submitter.""" submission = pqm_submit.PQMSubmission(self.branch, - mp.source_branch, mp.target_branch, '') + self.mp.source_branch, self.mp.target_branch, '') return submission @staticmethod def check_submission(submission): + """Ensure the Submission is reasonable.""" submission.check_tree() submission.check_public_branch() - def set_message(self, submission, mp): - pqm_command = ''.join(submission.to_lines()) - commit_message = mp.commit_message or '' - start_message = mp.get_commit_message(commit_message, self.testfix, - self.no_qa, self.incremental, rollback=self.rollback) + def set_message(self, submission): + """Set the message of the Submission. + + This starts with automatic generation from merge proposal and branch + properties, then allows the user to edit the values. + """ + commit_message = self.mp.commit_message or '' + start_message = self.mp.get_commit_message(commit_message, + self.testfix, self.no_qa, self.incremental, + rollback=self.rollback) prompt = ('Proposed commit message:\n%s\nEdit before sending' % start_message) if commit_message == '' or ui.ui_factory.get_boolean(prompt): + pqm_command = ''.join(submission.to_lines()) unicode_message = msgeditor.edit_commit_message( 'pqm command:\n%s' % pqm_command, start_message=start_message).rstrip('\n') @@ -239,18 +263,17 @@ message = start_message submission.message = message - def run(self, outf): - if self.location is None: - mp = self.lander.get_merge_proposal_from_branch(self.branch) - else: - mp = self.lander.load_merge_proposal(self.location) - submission = self.submission(mp) - self.check_submission(submission) - self.set_message(submission, mp) - mail_to = pqm_submit.pqm_email(self.config, mp.target_branch) + def make_submission_email(self, submission, sign=True): + mail_to = pqm_submit.pqm_email(self.config, self.mp.target_branch) if not mail_to: raise pqm_submit.NoPQMSubmissionAddress(self.branch) - email = submission.to_email(self.mail_from, mail_to) + return submission.to_email(self.mail_from, mail_to, sign=sign) + + def run(self, outf, sign=True): + submission = self.submission() + self.check_submission(submission) + self.set_message(submission) + email = self.make_submission_email(submission, sign=sign) if outf is not None: outf.write(email.as_string()) else: @@ -264,6 +287,8 @@ # error when the email address isn't set. e.g. with name12 in the sample # data. e.g. "httplib2.RelativeURIError: Only absolute URIs are allowed. # uri = tag:launchpad.net:2008:redacted". + if email_object is None: + return None # A team most likely. return email_object.email @@ -275,7 +300,16 @@ """ if not bugs: return '' - return '[bug=%s]' % ','.join(str(bug.id) for bug in bugs) + bug_ids = [] + for bug in bugs: + for task in bug.bug_tasks: + if (task.bug_target_name == 'launchpad' + and task.status not in ['Fix Committed', 'Fix Released']): + bug_ids.append(str(bug.id)) + break + if not bug_ids: + return '' + return '[bug=%s]' % ','.join(bug_ids) def get_testfix_clause(testfix=False): diff -Nru bzr-pqm-1.4.0~bzr83/pqm_submit.py bzr-pqm-1.4.0~bzr90/pqm_submit.py --- bzr-pqm-1.4.0~bzr83/pqm_submit.py 2012-01-20 14:42:23.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/pqm_submit.py 2012-03-06 20:12:29.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2011 by Canonical Ltd +# Copyright (C) 2006-2012 by Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,7 +26,7 @@ from bzrlib.branch import Branch from bzrlib.email_message import EmailMessage from bzrlib.smtp_connection import SMTPConnection -from bzrlib.trace import note, warning +from bzrlib.trace import note class BadCommitMessage(errors.BzrError): @@ -86,19 +86,6 @@ if public_location is None: public_location = self.source_branch.get_public_branch() - # Fall back to the old public_repository hack. - if public_location is None: - src_loc = source_branch.bzrdir.root_transport.local_abspath('.') - repository = source_branch.repository - repo_loc = repository.bzrdir.root_transport.local_abspath('.') - repo_config = _mod_config.LocationConfig(repo_loc) - public_repo = repo_config.get_user_option("public_repository") - if public_repo is not None: - warning("Please use public_branch, not public_repository, " - "to set the public location of branches.") - branch_relpath = osutils.relpath(repo_loc, src_loc) - public_location = urlutils.join(public_repo, branch_relpath) - if public_location is None: raise errors.NoPublicBranch(self.source_branch) self.public_location = public_location @@ -108,14 +95,7 @@ raise errors.BzrError( "Cannot determine submit location to use.") config = self.source_branch.get_config() - # First check the deprecated pqm_branch config key: - submit_location = config.get_user_option('pqm_branch') - if submit_location is not None: - warning("Please use submit_branch, not pqm_branch to set " - "the PQM merge target branch.") - else: - # Otherwise, use the standard config key: - submit_location = self.source_branch.get_submit_branch() + submit_location = self.source_branch.get_submit_branch() if submit_location is None: raise errors.NoSubmitBranch(self.source_branch) @@ -173,7 +153,7 @@ unsigned_text = ''.join(self.to_lines()) unsigned_text = unsigned_text.encode('ascii') #URLs should be ascii - if bzrlib_version < (2, 5, 0, 'beta', 5): + if bzrlib_version < (2, 5): if self.source_branch: config = self.source_branch.get_config() else: @@ -219,7 +199,16 @@ return value return None - get = _get_user_option + def get(self, option_name): + """Return an option exactly as bzrlib.config.Stack would. + + Since Stack allows the environment to override 'email', this uses the + same logic. + """ + if option_name == 'email': + return self.username() + else: + return self._get_user_option(option_name) def _get_user_id(self): for source in self._sources: @@ -228,6 +217,9 @@ return value return None + def set(self, name, value): + self._sources[0].set_user_option(name, value) + def pqm_email(local_config, submit_location): """Determine the PQM email address. @@ -238,17 +230,28 @@ mail_to = local_config.get('pqm_email') if not mail_to: submit_branch = Branch.open(submit_location) - submit_branch_config = submit_branch.get_config() + submit_branch_config = get_stacked_config(submit_branch) mail_to = submit_branch_config.get('child_pqm_email') if mail_to is None: return None return mail_to.encode('utf8') # same here -def submit(branch, message, dry_run=False, public_location=None, - submit_location=None, tree=None, ignore_local=False): - """Submit the given branch to the pqm.""" - if bzrlib_version < (2, 5, 0, 'beta', 5): +def get_stacked_config(branch=None, public_location=None): + """Return the relevant stacked config. + + If the branch is supplied, a branch stacked config is returned. + Otherwise, if the public location is supplied, a stacked location config + is returned. + Otherwise, a global config is returned. + + For bzr versions earlier than 2.5, pqm_submit.StackedConfig is used. For + later versions, the standard stacked config is used. + + :param branch: The branch to retrieve the config for. + :param public_location: The public location to retrieve the config for. + """ + if bzrlib_version < (2, 5): config = StackedConfig() if branch: config.add_source(branch.get_config()) @@ -263,16 +266,31 @@ config = _mod_config.LocationStack(public_location) else: config = _mod_config.GlobalStack() + return config - submission = PQMSubmission( - source_branch=branch, public_location=public_location, message=message, - submit_location=submit_location, - tree=tree) +def get_mail_from(config): + """Return the email id that the email is from. + + :param config: The config to use for determining the from address. + """ mail_from = config.get('pqm_user_email') if not mail_from: mail_from = config.get('email') mail_from = mail_from.encode('utf8') # Make sure this isn't unicode + return mail_from + + +def submit(branch, message, dry_run=False, public_location=None, + submit_location=None, tree=None, ignore_local=False): + """Submit the given branch to the pqm.""" + config = get_stacked_config(branch, public_location) + submission = PQMSubmission( + source_branch=branch, public_location=public_location, message=message, + submit_location=submit_location, + tree=tree) + + mail_from = get_mail_from(config) mail_to = pqm_email(config, submit_location) if not mail_to: raise NoPQMSubmissionAddress(branch) diff -Nru bzr-pqm-1.4.0~bzr83/tests/test_lpland.py bzr-pqm-1.4.0~bzr90/tests/test_lpland.py --- bzr-pqm-1.4.0~bzr83/tests/test_lpland.py 2011-02-16 00:01:50.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/tests/test_lpland.py 2012-03-06 20:12:29.000000000 +0000 @@ -1,23 +1,55 @@ -# Copyright 2010 Canonical Ltd. This software is licensed under the +# Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Tests for automatic landing thing.""" __metaclass__ = type +from cStringIO import StringIO +import re +from textwrap import dedent import unittest -from launchpadlib.uris import ( - DEV_SERVICE_ROOT, EDGE_SERVICE_ROOT, LPNET_SERVICE_ROOT, - STAGING_SERVICE_ROOT) - +from bzrlib import ( + errors, + ui, +) +from bzrlib.plugins.pqm.pqm_submit import get_stacked_config from bzrlib.plugins.pqm.lpland import ( - get_bugs_clause, get_reviewer_clause, - get_reviewer_handle, get_testfix_clause, get_qa_clause, - MissingReviewError, MissingBugsError, MissingBugsIncrementalError, - MergeProposal) - -from fakemethod import FakeMethod + get_bugs_clause, + get_email, + get_reviewer_clause, + get_reviewer_handle, + get_testfix_clause, + get_qa_clause, + LaunchpadBranchLander, + MissingReviewError, + MissingBugsError, + MissingBugsIncrementalError, + MergeProposal, + Submitter, +) +from bzrlib.tests import ( + TestCase, + TestCaseWithTransport, + ) + +from bzrlib.plugins.pqm.tests.fakemethod import FakeMethod + +DEFAULT=object() + +class FakeLaunchpad: + + def __init__(self, branches=None): + self.branches = FakeLaunchpadBranches(branches) + self.me = FakePerson() + + +class FakeBugTask: + + def __init__(self, target_name, status): + self.bug_target_name = target_name + self.status = status class FakeBug: @@ -26,8 +58,17 @@ Only used for the purposes of testing. """ - def __init__(self, id): + def __init__(self, id, bug_tasks=None): self.id = id + if bug_tasks is None: + bug_tasks = [FakeBugTask('launchpad', 'Triaged')] + self.bug_tasks = bug_tasks + + +class FakeEmailAddress: + + def __init__(self, email): + self.email = email class FakePerson: @@ -36,9 +77,14 @@ Only used for the purposes of testing. """ - def __init__(self, name, irc_handles): + def __init__(self, name='jrandom', irc_handles=(), email=DEFAULT): self.name = name self.irc_nicknames = list(irc_handles) + if email is not DEFAULT: + self.preferred_email_address = email + else: + self.preferred_email_address = FakeEmailAddress( + 'jrandom@example.org') class FakeIRC: @@ -52,14 +98,132 @@ self.network = network +class FakeBzrBranch: + + def __init__(self): + pass + + def get_public_branch(self): + return 'public' + + +class FakeLaunchpadBranches: + + def __init__(self, branches): + self.branches = branches + + def getByUrl(self, url): + for branch in self.branches: + if branch.location == url: + return branch + + +class FakeBranch: + + def __init__(self, location): + self.location = location + self.linked_bugs = [FakeBug(5)] + self.landing_targets = [] + self.owner = FakePerson() + + def composePublicURL(self, scheme): + return self.location + + +class FakeComment: + + def __init__(self): + self.vote = 'Approve' + + +class FakeVote: + + def __init__(self): + self.comment = FakeComment() + self.review_type = None + self.reviewer = FakePerson() + + class FakeLPMergeProposal: """Fake launchpadlib MergeProposal object. Only used for the purposes of testing. """ - def __init__(self, root=None): + def __init__(self, root=None, queue_status='Approved', votes=None, + reviewer=None): + if root is None: + root = FakeLaunchpad() self._root = root + self.source_branch = FakeBranch('lp_source') + self.target_branch = FakeBranch('lp_target') + self.commit_message = 'Message1' + if votes is not None: + self.votes = votes + else: + self.votes = [FakeVote()] + self.queue_status = queue_status + self.reviewer = reviewer + + def lp_save(self): + pass + + +class TestGetEmail(TestCase): + + def test_get_email(self): + self.assertEqual('jrandom@example.org', get_email(FakePerson())) + + def test_get_email_none(self): + self.assertIs(None, get_email(FakePerson(email=None))) + + +class TestPQMRegexAcceptance(unittest.TestCase): + """Tests if the generated commit message is accepted by PQM regexes.""" + + def setUp(self): + # PQM regexes; might need update once in a while + self.devel_open_re = ("(?is)^\s*(:?\[testfix\])?\[(?:" + "release-critical=[^\]]+|rs?=[^\]]+)\]") + self.dbdevel_normal_re = ("(?is)^\s*(:?\[testfix\])?\[(?:" + "release-critical|rs?=[^\]]+)\]") + + self.mp = MergeProposal(FakeLPMergeProposal()) + self.fake_bug = FakeBug(20) + self.fake_person = FakePerson('foo', []) + self.mp.get_bugs = FakeMethod([self.fake_bug]) + self.mp.get_reviews = FakeMethod({None : [self.fake_person]}) + + def assertRegexpMatches(self, text, expected_regexp, msg=None): + """Fail the test unless the text matches the regular expression. + + Method default in Python 2.7. Can be removed as soon as LP goes 2.7. + """ + if isinstance(expected_regexp, basestring): + expected_regexp = re.compile(expected_regexp) + if not expected_regexp.search(text): + msg = msg or "Regexp didn't match" + msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern, + text) + raise self.failureException(msg) + + def _test_commit_message_match(self, incr, no_qa, testfix): + commit_message = self.mp.get_commit_message("Foobaring the sbrubble.", + testfix, no_qa, incr) + self.assertRegexpMatches(commit_message, self.devel_open_re) + self.assertRegexpMatches(commit_message, self.dbdevel_normal_re) + + def test_testfix_match(self): + self._test_commit_message_match(incr=False, no_qa=False, testfix=True) + + def test_regular_match(self): + self._test_commit_message_match(incr=False, no_qa=False, testfix=False) + + def test_noqa_match(self): + self._test_commit_message_match(incr=False, no_qa=True, testfix=False) + + def test_incr_match(self): + self._test_commit_message_match(incr=True, no_qa=False, testfix=False) class TestBugsClaused(unittest.TestCase): @@ -83,6 +247,36 @@ bugs_clause = get_bugs_clause([bug1, bug2]) self.assertEqual('[bug=20,45]', bugs_clause) + def test_fixed_bugs_are_excluded(self): + # If a bug is fixed then it is excluded from the bugs clause. + bug1 = FakeBug(20) + bug2 = FakeBug(45, bug_tasks=[ + FakeBugTask('fake-project', 'Fix Released')]) + bug3 = FakeBug(67, bug_tasks=[ + FakeBugTask('fake-project', 'Fix Committed')]) + bugs_clause = get_bugs_clause([bug1, bug2, bug3]) + self.assertEqual('[bug=20]', bugs_clause) + + def test_bugs_open_on_launchpad_are_included(self): + # If a bug has been fixed on one target but not in launchpad, then it + # is included in the bugs clause, because it's relevant to launchpad + # QA. + bug = FakeBug(20, bug_tasks=[ + FakeBugTask('fake-project', 'Fix Released'), + FakeBugTask('launchpad', 'Triaged')]) + bugs_clause = get_bugs_clause([bug]) + self.assertEqual('[bug=20]', bugs_clause) + + def test_bugs_fixed_on_launchpad_but_open_in_others_are_excluded(self): + # If a bug has been fixed in Launchpad but not fixed on a different + # target, then it is excluded from the bugs clause, since we don't + # want to QA it. + bug = FakeBug(20, bug_tasks=[ + FakeBugTask('fake-project', 'Triaged'), + FakeBugTask('launchpad', 'Fix Released')]) + bugs_clause = get_bugs_clause([bug]) + self.assertEqual('', bugs_clause) + class TestGetTestfixClause(unittest.TestCase): """Tests for `get_testfix_clause`""" @@ -155,10 +349,7 @@ def test_rollback_and_noqa_and_incr_given(self): bugs = None - no_qa = True - incr = True - self.assertEqual('[rollback=123]', - get_qa_clause(bugs, rollback=123)) + self.assertEqual('[rollback=123]', get_qa_clause(bugs, rollback=123)) class TestGetReviewerHandle(unittest.TestCase): @@ -361,3 +552,184 @@ # If the merge proposal hasn't been approved by anyone, we cannot # generate a valid clause. self.assertRaises(MissingReviewError, self.get_reviewer_clause, {}) + + +class TestLaunchpadBranchLander(TestCase): + + def get_lander(self, landing_targets=None): + branch = FakeBranch('public') + if landing_targets is not None: + branch.landing_targets = landing_targets + launchpad = FakeLaunchpad([branch]) + return LaunchpadBranchLander(launchpad) + + def test_get_merge_proposal_from_branch_no_proposals(self): + lander = self.get_lander() + branch = FakeBzrBranch() + e = self.assertRaises(errors.BzrCommandError, + lander.get_merge_proposal_from_branch, branch) + self.assertEqual('The public branch has no active source merge' + ' proposals. You must have a merge proposal before' + ' attempting to land the branch.', str(e)) + + def test_get_merge_proposal_one_proposal(self): + proposal = FakeLPMergeProposal() + lander = self.get_lander([proposal]) + branch = FakeBzrBranch() + lander_proposal = lander.get_merge_proposal_from_branch(branch) + self.assertIs(proposal, lander_proposal._mp) + + def test_get_merge_proposal_two_proposal(self): + lander = self.get_lander([FakeLPMergeProposal(), + FakeLPMergeProposal()]) + branch = FakeBzrBranch() + e = self.assertRaises(errors.BzrCommandError, + lander.get_merge_proposal_from_branch, branch) + self.assertEqual('The public branch has multiple active source merge' + ' proposals. You must provide the URL to the one' + ' you wish to use.', str(e)) + + def test_get_merge_proposal_inactive(self): + for status in ['Rejected', 'Work in progress', 'Merged', 'Queued', + 'Code failed to merge', 'Superseded']: + proposal = FakeLPMergeProposal(queue_status=status) + lander = self.get_lander([proposal]) + branch = FakeBzrBranch() + e = self.assertRaises(errors.BzrCommandError, + lander.get_merge_proposal_from_branch, + branch) + self.assertEqual('The public branch has no active source merge' + ' proposals. You must have a merge proposal' + ' before attempting to land the branch.', + str(e)) + + def test_get_merge_proposal_active(self): + branch = FakeBzrBranch() + for status in ['Approved', 'Needs review']: + proposal = FakeLPMergeProposal(queue_status=status) + lander = self.get_lander([proposal]) + lander_proposal = lander.get_merge_proposal_from_branch(branch) + self.assertIs(proposal, lander_proposal._mp) + + +class TestMergeProposal(TestCase): + + def test_get_reviews_none_unapproved(self): + """Reviewer is not considered for un-approved propoals.""" + reviewer = FakePerson() + proposal = FakeLPMergeProposal(queue_status='Needs review', votes=[], + reviewer=reviewer) + mp = MergeProposal(proposal) + approvals = mp.get_reviews() + self.assertEqual({}, approvals) + + def test_get_reviews_none_approved(self): + """Approving a proposal counts as a vote if other approvals.""" + reviewer = FakePerson() + proposal = FakeLPMergeProposal(queue_status='Approved', votes=[], + reviewer=reviewer) + mp = MergeProposal(proposal) + approvals = mp.get_reviews() + self.assertEqual({None: [reviewer]}, approvals) + + def test_get_reviews_one_approved(self): + """As long as there is one approve vote, don't use reviewer.""" + reviewer = FakePerson() + proposal = FakeLPMergeProposal(queue_status='Approved', + votes=[FakeVote()], + reviewer=reviewer) + mp = MergeProposal(proposal) + approvals = mp.get_reviews() + self.assertEqual({None: [proposal.votes[0].reviewer]}, approvals) + + def test_get_stakeholder_emails(self): + lp = FakeLaunchpad() + mp = MergeProposal(FakeLPMergeProposal(root=lp)) + lp.me.preferred_email_address = FakeEmailAddress('lander@example.org') + owner = mp._mp.source_branch.owner + owner.preferred_email_address = FakeEmailAddress('owner@example.org') + expected = set(['owner@example.org', 'lander@example.org']) + emails = mp.get_stakeholder_emails() + self.assertEqual(expected, emails) + + def test_get_stakeholder_emails_none(self): + lp = FakeLaunchpad() + mp = MergeProposal(FakeLPMergeProposal(root=lp)) + lp.me.preferred_email_address = FakeEmailAddress('lander@example.org') + owner = mp._mp.source_branch.owner + owner.preferred_email_address = None + expected = set(['lander@example.org']) + emails = mp.get_stakeholder_emails() + self.assertEqual(expected, emails) + + +class TestSubmitter(TestCaseWithTransport): + + def make_submitter(self): + """Return a Submitter for testing.""" + b = self.make_branch('source') + get_stacked_config(b).set('pqm_email', 'PQM ') + lp_source = self.make_branch('lp_source') + mp = MergeProposal(FakeLPMergeProposal()) + return Submitter(b, mp) + + def test_submission(self): + """Creating a submission produces the expected result.""" + submitter = self.make_submitter() + submission = submitter.submission() + self.assertEqual(submitter.branch, submission.source_branch) + self.assertEqual('lp_source', submission.public_location) + self.assertEqual('lp_target', submission.submit_location) + self.assertEqual('', submission.message) + + def test_check_submission(self): + """Checking the submission returns in the comment case.""" + submitter = self.make_submitter() + submitter.check_submission(submitter.submission()) + + def test_check_submission_public_branch(self): + """Checking the submission raises if the public branch is outdated.""" + submitter = self.make_submitter() + tree = submitter.branch.create_checkout('tree', lightweight=True) + tree.commit('message') + self.assertRaises(errors.PublicBranchOutOfDate, + submitter.check_submission, submitter.submission()) + + def test_set_message(self): + """Setting the message produces one in the correct format.""" + submitter = self.make_submitter() + submission = submitter.submission() + ui.ui_factory = ui.CannedInputUIFactory([False]) + submitter.set_message(submission) + self.assertEqual('[r=jrandom][bug=5] Message1', submission.message) + + def test_make_submission_email(self): + """Creating a submission email works.""" + submitter = self.make_submitter() + submission = submitter.submission() + submission.message = 'Hello!' + target = self.make_branch('lp_target') + email = submitter.make_submission_email(submission, sign=False) + self.assertEqual('PQM ', email['To']) + self.assertEqual('jrandom@example.com', email['From']) + self.assertEqual('Hello!', email['Subject']) + self.assertContainsRe(email['User-Agent'], '^Bazaar') + self.assertEqual('star-merge lp_source lp_target\n', email._body) + + def test_run(self): + """End-to-end test of Submitter.run.""" + submitter = self.make_submitter() + ui.ui_factory = ui.CannedInputUIFactory([False]) + outf = StringIO() + submitter.run(outf, sign=False) + self.assertContainsRe(outf.getvalue(), dedent("""\ + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + From: jrandom@example.com + Subject: \[r=jrandom\]\[bug=5\] Message1 + To: PQM + User-Agent: Bazaar (.*) + + star-merge lp_source lp_target + """)) diff -Nru bzr-pqm-1.4.0~bzr83/tests/test_pqm_submit.py bzr-pqm-1.4.0~bzr90/tests/test_pqm_submit.py --- bzr-pqm-1.4.0~bzr83/tests/test_pqm_submit.py 2010-08-06 16:52:30.000000000 +0000 +++ bzr-pqm-1.4.0~bzr90/tests/test_pqm_submit.py 2012-03-06 20:12:29.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2010 by Canonical Ltd +# Copyright (C) 2006-2010, 2012 by Canonical Ltd # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -223,25 +223,6 @@ self.assertEqual('http://example.com/public', submission.public_location) - def test_find_public_branch_from_repo(self): - # Test that public_location can be inferred from the obsolete - # "public_repository" config option. - source_repo = self.make_repository('repo', shared=True) - source_branch = self.make_bzrdir('repo/source').create_branch() - - config = _mod_config.LocationConfig( - source_repo.bzrdir.root_transport.base) - config.set_user_option( - 'public_repository', 'http://example.com/repo', - store=_mod_config.STORE_LOCATION_NORECURSE) - - submission = pqm_submit.PQMSubmission( - source_branch=source_branch, - submit_location='submit-branch', - message='commit message') - self.assertEqual('http://example.com/repo/source', - submission.public_location) - def test_find_public_branch_missing(self): source_branch = self.make_branch('source') self.assertRaises( @@ -262,23 +243,6 @@ message='commit message') self.assertEqual(submit_transport.base, submission.submit_location) - def test_find_submit_branch_from_pqm_branch(self): - # Test that submit_branch can be picked up from the obsolete - # pqm_branch config option. - source_branch = self.make_branch('source') - submit_transport = self.get_transport().clone('submit') - submit_transport.ensure_base() - source_branch.get_config().set_user_option( - 'pqm_branch', submit_transport.base) - # pqm_branch is used in preference to the submit_branch: - source_branch.set_submit_branch('bad-value') - - submission = pqm_submit.PQMSubmission( - source_branch=source_branch, - public_location='public-branch', - message='commit message') - self.assertEqual(submit_transport.base, submission.submit_location) - def test_find_submit_branch_missing(self): source_branch = self.make_branch('source') self.assertRaises( @@ -471,3 +435,23 @@ call[1:3]) self.assertContainsRe(call[3], EMAIL) + +class TestPqmEmail(TestCaseWithMemoryTransport): + + def test_child_pqm_email(self): + local_branch = self.make_branch('local') + local_config = pqm_submit.get_stacked_config(local_branch) + submit_branch = self.make_branch('submit') + submit_config = submit_branch.get_config() + submit_config.set_user_option('child_pqm_email', 'child@example.org') + result = pqm_submit.pqm_email(local_config, submit_branch.base) + self.assertEqual('child@example.org', result) + + +class TestConfig(TestCaseWithMemoryTransport): + + def test_email_from_environ(self): + """The config can get the email from the environ.""" + branch = self.make_branch('foo') + config = pqm_submit.get_stacked_config(branch, None) + self.assertEqual('jrandom@example.com', config.get('email'))