/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/plugins/launchpad/lp_propose.py

  • Committer: Vincent Ladeuil
  • Date: 2012-01-18 14:09:19 UTC
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120118140919-rlvdrhpc0nq1lbwi
Change set/remove to require a lock for the branch config files.

This means that tests (or any plugin for that matter) do not requires an
explicit lock on the branch anymore to change a single option. This also
means the optimisation becomes "opt-in" and as such won't be as
spectacular as it may be and/or harder to get right (nothing fails
anymore).

This reduces the diff by ~300 lines.

Code/tests that were updating more than one config option is still taking
a lock to at least avoid some IOs and demonstrate the benefits through
the decreased number of hpss calls.

The duplication between BranchStack and BranchOnlyStack will be removed
once the same sharing is in place for local config files, at which point
the Stack class itself may be able to host the changes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2010, 2011 Canonical Ltd
 
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
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
from __future__ import absolute_import
 
18
 
 
19
from bzrlib import (
 
20
    errors,
 
21
    hooks,
 
22
    )
 
23
from bzrlib.lazy_import import lazy_import
 
24
lazy_import(globals(), """
 
25
import webbrowser
 
26
 
 
27
from bzrlib import (
 
28
    msgeditor,
 
29
    )
 
30
from bzrlib.i18n import gettext
 
31
from bzrlib.plugins.launchpad import (
 
32
    lp_api,
 
33
    lp_registration,
 
34
    )
 
35
""")
 
36
 
 
37
 
 
38
class ProposeMergeHooks(hooks.Hooks):
 
39
    """Hooks for proposing a merge on Launchpad."""
 
40
 
 
41
    def __init__(self):
 
42
        hooks.Hooks.__init__(self, "bzrlib.plugins.launchpad.lp_propose",
 
43
            "Proposer.hooks")
 
44
        self.add_hook('get_prerequisite',
 
45
            "Return the prerequisite branch for proposing as merge.", (2, 1))
 
46
        self.add_hook('merge_proposal_body',
 
47
            "Return an initial body for the merge proposal message.", (2, 1))
 
48
 
 
49
 
 
50
class Proposer(object):
 
51
 
 
52
    hooks = ProposeMergeHooks()
 
53
 
 
54
    def __init__(self, tree, source_branch, target_branch, message, reviews,
 
55
                 staging=False, approve=False):
 
56
        """Constructor.
 
57
 
 
58
        :param tree: The working tree for the source branch.
 
59
        :param source_branch: The branch to propose for merging.
 
60
        :param target_branch: The branch to merge into.
 
61
        :param message: The commit message to use.  (May be None.)
 
62
        :param reviews: A list of tuples of reviewer, review type.
 
63
        :param staging: If True, propose the merge against staging instead of
 
64
            production.
 
65
        :param approve: If True, mark the new proposal as approved immediately.
 
66
            This is useful when a project permits some things to be approved
 
67
            by the submitter (e.g. merges between release and deployment
 
68
            branches).
 
69
        """
 
70
        self.tree = tree
 
71
        if staging:
 
72
            lp_instance = 'staging'
 
73
        else:
 
74
            lp_instance = 'production'
 
75
        service = lp_registration.LaunchpadService(lp_instance=lp_instance)
 
76
        self.launchpad = lp_api.login(service)
 
77
        self.source_branch = lp_api.LaunchpadBranch.from_bzr(
 
78
            self.launchpad, source_branch)
 
79
        if target_branch is None:
 
80
            self.target_branch = self.source_branch.get_target()
 
81
        else:
 
82
            self.target_branch = lp_api.LaunchpadBranch.from_bzr(
 
83
                self.launchpad, target_branch)
 
84
        self.commit_message = message
 
85
        # XXX: this is where bug lp:583638 could be tackled.
 
86
        if reviews == []:
 
87
            self.reviews = []
 
88
        else:
 
89
            self.reviews = [(self.launchpad.people[reviewer], review_type)
 
90
                            for reviewer, review_type in
 
91
                            reviews]
 
92
        self.approve = approve
 
93
 
 
94
    def get_comment(self, prerequisite_branch):
 
95
        """Determine the initial comment for the merge proposal."""
 
96
        info = ["Source: %s\n" % self.source_branch.lp.bzr_identity]
 
97
        info.append("Target: %s\n" % self.target_branch.lp.bzr_identity)
 
98
        if prerequisite_branch is not None:
 
99
            info.append("Prereq: %s\n" % prerequisite_branch.lp.bzr_identity)
 
100
        for rdata in self.reviews:
 
101
            uniquename = "%s (%s)" % (rdata[0].display_name, rdata[0].name)
 
102
            info.append('Reviewer: %s, type "%s"\n' % (uniquename, rdata[1]))
 
103
        self.source_branch.bzr.lock_read()
 
104
        try:
 
105
            self.target_branch.bzr.lock_read()
 
106
            try:
 
107
                body = self.get_initial_body()
 
108
            finally:
 
109
                self.target_branch.bzr.unlock()
 
110
        finally:
 
111
            self.source_branch.bzr.unlock()
 
112
        initial_comment = msgeditor.edit_commit_message(''.join(info),
 
113
                                                        start_message=body)
 
114
        return initial_comment.strip().encode('utf-8')
 
115
 
 
116
    def get_initial_body(self):
 
117
        """Get a body for the proposal for the user to modify.
 
118
 
 
119
        :return: a str or None.
 
120
        """
 
121
        def list_modified_files():
 
122
            lca_tree = self.source_branch.find_lca_tree(
 
123
                self.target_branch)
 
124
            source_tree = self.source_branch.bzr.basis_tree()
 
125
            files = modified_files(lca_tree, source_tree)
 
126
            return list(files)
 
127
        target_loc = ('bzr+ssh://bazaar.launchpad.net/%s' %
 
128
                       self.target_branch.lp.unique_name)
 
129
        body = None
 
130
        for hook in self.hooks['merge_proposal_body']:
 
131
            body = hook({
 
132
                'tree': self.tree,
 
133
                'target_branch': target_loc,
 
134
                'modified_files_callback': list_modified_files,
 
135
                'old_body': body,
 
136
            })
 
137
        return body
 
138
 
 
139
    def check_proposal(self):
 
140
        """Check that the submission is sensible."""
 
141
        if self.source_branch.lp.self_link == self.target_branch.lp.self_link:
 
142
            raise errors.BzrCommandError(
 
143
                'Source and target branches must be different.')
 
144
        for mp in self.source_branch.lp.landing_targets:
 
145
            if mp.queue_status in ('Merged', 'Rejected'):
 
146
                continue
 
147
            if mp.target_branch.self_link == self.target_branch.lp.self_link:
 
148
                raise errors.BzrCommandError(gettext(
 
149
                    'There is already a branch merge proposal: %s') %
 
150
                    lp_api.canonical_url(mp))
 
151
 
 
152
    def _get_prerequisite_branch(self):
 
153
        hooks = self.hooks['get_prerequisite']
 
154
        prerequisite_branch = None
 
155
        for hook in hooks:
 
156
            prerequisite_branch = hook(
 
157
                {'launchpad': self.launchpad,
 
158
                 'source_branch': self.source_branch,
 
159
                 'target_branch': self.target_branch,
 
160
                 'prerequisite_branch': prerequisite_branch})
 
161
        return prerequisite_branch
 
162
 
 
163
    def call_webservice(self, call, *args, **kwargs):
 
164
        """Make a call to the webservice, wrapping failures.
 
165
        
 
166
        :param call: The call to make.
 
167
        :param *args: *args for the call.
 
168
        :param **kwargs: **kwargs for the call.
 
169
        :return: The result of calling call(*args, *kwargs).
 
170
        """
 
171
        from lazr.restfulclient import errors as restful_errors
 
172
        try:
 
173
            return call(*args, **kwargs)
 
174
        except restful_errors.HTTPError, e:
 
175
            error_lines = []
 
176
            for line in e.content.splitlines():
 
177
                if line.startswith('Traceback (most recent call last):'):
 
178
                    break
 
179
                error_lines.append(line)
 
180
            raise Exception(''.join(error_lines))
 
181
 
 
182
    def create_proposal(self):
 
183
        """Perform the submission."""
 
184
        prerequisite_branch = self._get_prerequisite_branch()
 
185
        if prerequisite_branch is None:
 
186
            prereq = None
 
187
        else:
 
188
            prereq = prerequisite_branch.lp
 
189
            prerequisite_branch.update_lp()
 
190
        self.source_branch.update_lp()
 
191
        reviewers = []
 
192
        review_types = []
 
193
        for reviewer, review_type in self.reviews:
 
194
            review_types.append(review_type)
 
195
            reviewers.append(reviewer.self_link)
 
196
        initial_comment = self.get_comment(prerequisite_branch)
 
197
        mp = self.call_webservice(
 
198
            self.source_branch.lp.createMergeProposal,
 
199
            target_branch=self.target_branch.lp,
 
200
            prerequisite_branch=prereq,
 
201
            initial_comment=initial_comment,
 
202
            commit_message=self.commit_message, reviewers=reviewers,
 
203
            review_types=review_types)
 
204
        if self.approve:
 
205
            self.call_webservice(mp.setStatus, status='Approved')
 
206
        webbrowser.open(lp_api.canonical_url(mp))
 
207
 
 
208
 
 
209
def modified_files(old_tree, new_tree):
 
210
    """Return a list of paths in the new tree with modified contents."""
 
211
    for f, (op, path), c, v, p, n, (ok, k), e in new_tree.iter_changes(
 
212
        old_tree):
 
213
        if c and k == 'file':
 
214
            yield str(path)